From 6b43a65e569134d2d160f80b1c3ad101d2402461 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 15 Jun 2023 13:05:16 -0600 Subject: [PATCH 01/18] build: update to api 34 Update the app to target API 34, alongside all dependencies that expect such. This finally fixes some long-standing issues in newer version of MDC, thus allowing me to remove the divider backport as well. More work is required for predictive back (don't even know where I start with that...), but this is a good start. --- app/build.gradle | 8 +- ...BackportMaterialDividerItemDecoration.java | 413 ------------------ .../org/oxycblt/auxio/settings/Settings.kt | 4 +- .../ui/PreferenceHeaderItemDecoration.kt | 6 +- build.gradle | 4 +- 5 files changed, 11 insertions(+), 424 deletions(-) delete mode 100644 app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java diff --git a/app/build.gradle b/app/build.gradle index bd85eca83..ca424fca4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { } android { - compileSdk 33 + compileSdk 34 // NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify // it here so that binary stripping will work. // TODO: Eventually you might just want to start vendoring the FFMpeg extension so the @@ -24,7 +24,7 @@ android { versionCode 31 minSdk 24 - targetSdk 33 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -87,7 +87,7 @@ dependencies { implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.core:core-ktx:1.10.1" implementation "androidx.activity:activity-ktx:1.7.2" - implementation "androidx.fragment:fragment-ktx:1.5.7" + implementation "androidx.fragment:fragment-ktx:1.6.0" // UI implementation "androidx.recyclerview:recyclerview:1.3.0" @@ -132,7 +132,7 @@ dependencies { // in a version that I can build with // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. - implementation "com.google.android.material:material:1.8.0-alpha01" + implementation "com.google.android.material:material:1.10.0-alpha04" // Dependency Injection implementation "com.google.dagger:dagger:$hilt_version" 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 deleted file mode 100644 index 26de5108b..000000000 --- a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.material.divider; - -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 android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; - -import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; -import androidx.annotation.DimenRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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; - -/** - * MaterialDividerItemDecoration is a {@link RecyclerView.ItemDecoration}, similar to a {@link - * androidx.recyclerview.widget.DividerItemDecoration}, that can be used as a divider between items of - * a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and {@link #VERTICAL} - * orientations. - * - *
- *     dividerItemDecoration = new MaterialDividerItemDecoration(recyclerView.getContext(),
- *             layoutManager.getOrientation());
- *     recyclerView.addItemDecoration(dividerItemDecoration);
- * 
- * - * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently - * obtainable in the currently used MDC library. - */ -public class BackportMaterialDividerItemDecoration extends ItemDecoration { - public static final int HORIZONTAL = LinearLayout.HORIZONTAL; - public static final int VERTICAL = LinearLayout.VERTICAL; - - private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_MaterialDivider; - - @NonNull private Drawable dividerDrawable; - private int thickness; - @ColorInt private int color; - private int orientation; - private int insetStart; - private int insetEnd; - private boolean lastItemDecorated; - - private final Rect tempRect = new Rect(); - - public BackportMaterialDividerItemDecoration(@NonNull Context context, int orientation) { - this(context, null, orientation); - } - - public BackportMaterialDividerItemDecoration( - @NonNull Context context, @Nullable AttributeSet attrs, int orientation) { - this(context, attrs, R.attr.materialDividerStyle, orientation); - } - - public BackportMaterialDividerItemDecoration( - @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int orientation) { - TypedArray attributes = - ThemeEnforcement.obtainStyledAttributes( - context, attrs, R.styleable.MaterialDivider, defStyleAttr, DEF_STYLE_RES); - - color = - MaterialResources.getColorStateList( - context, attributes, R.styleable.MaterialDivider_dividerColor) - .getDefaultColor(); - thickness = - attributes.getDimensionPixelSize( - R.styleable.MaterialDivider_dividerThickness, - context.getResources().getDimensionPixelSize(R.dimen.material_divider_thickness)); - insetStart = - attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetStart, 0); - insetEnd = attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetEnd, 0); - lastItemDecorated = - attributes.getBoolean(R.styleable.MaterialDivider_lastItemDecorated, true); - - attributes.recycle(); - - dividerDrawable = new ShapeDrawable(); - setDividerColor(color); - setOrientation(orientation); - } - - /** - * Sets the orientation for this divider. This should be called if {@link - * RecyclerView.LayoutManager} changes orientation. - * - *

A {@link #HORIZONTAL} orientation will draw a vertical divider, and a {@link #VERTICAL} - * orientation a horizontal divider. - * - * @param orientation The orientation of the {@link RecyclerView} this divider is associated with: - * {@link #HORIZONTAL} or {@link #VERTICAL} - */ - public void setOrientation(int orientation) { - if (orientation != HORIZONTAL && orientation != VERTICAL) { - throw new IllegalArgumentException( - "Invalid orientation: " + orientation + ". It should be either HORIZONTAL or VERTICAL"); - } - this.orientation = orientation; - } - - public int getOrientation() { - return orientation; - } - - /** - * Sets the thickness of the divider. - * - * @param thickness The thickness value to be set. - * @see #getDividerThickness() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness - */ - public void setDividerThickness(@Px int thickness) { - this.thickness = thickness; - } - - /** - * Sets the thickness of the divider. - * - * @param thicknessId The id of the thickness dimension resource to be set. - * @see #getDividerThickness() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness - */ - public void setDividerThicknessResource(@NonNull Context context, @DimenRes int thicknessId) { - setDividerThickness(context.getResources().getDimensionPixelSize(thicknessId)); - } - - /** - * Returns the thickness set on the divider. - * - * @see #setDividerThickness(int) - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness - */ - @Px - public int getDividerThickness() { - return thickness; - } - - /** - * Sets the color of the divider. - * - * @param color The color to be set. - * @see #getDividerColor() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor - */ - public void setDividerColor(@ColorInt int color) { - this.color = color; - dividerDrawable = DrawableCompat.wrap(dividerDrawable); - DrawableCompat.setTint(dividerDrawable, color); - } - - /** - * Sets the color of the divider. - * - * @param colorId The id of the color resource to be set. - * @see #getDividerColor() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor - */ - public void setDividerColorResource(@NonNull Context context, @ColorRes int colorId) { - setDividerColor(ContextCompat.getColor(context, colorId)); - } - - /** - * Returns the divider color. - * - * @see #setDividerColor(int) - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor - */ - @ColorInt - public int getDividerColor() { - return color; - } - - /** - * Sets the start inset of the divider. - * - * @param insetStart The start inset to be set. - * @see #getDividerInsetStart() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart - */ - public void setDividerInsetStart(@Px int insetStart) { - this.insetStart = insetStart; - } - - /** - * Sets the start inset of the divider. - * - * @param insetStartId The id of the inset dimension resource to be set. - * @see #getDividerInsetStart() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart - */ - public void setDividerInsetStartResource(@NonNull Context context, @DimenRes int insetStartId) { - setDividerInsetStart(context.getResources().getDimensionPixelOffset(insetStartId)); - } - - /** - * Returns the divider's start inset. - * - * @see #setDividerInsetStart(int) - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart - */ - @Px - public int getDividerInsetStart() { - return insetStart; - } - - /** - * Sets the end inset of the divider. - * - * @param insetEnd The end inset to be set. - * @see #getDividerInsetEnd() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd - */ - public void setDividerInsetEnd(@Px int insetEnd) { - this.insetEnd = insetEnd; - } - - /** - * Sets the end inset of the divider. - * - * @param insetEndId The id of the inset dimension resource to be set. - * @see #getDividerInsetEnd() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd - */ - public void setDividerInsetEndResource(@NonNull Context context, @DimenRes int insetEndId) { - setDividerInsetEnd(context.getResources().getDimensionPixelOffset(insetEndId)); - } - - /** - * Returns the divider's end inset. - * - * @see #setDividerInsetEnd(int) - * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd - */ - @Px - public int getDividerInsetEnd() { - return insetEnd; - } - - /** - * Sets whether the class should draw a divider after the last item of a {@link RecyclerView}. - * - * @param lastItemDecorated whether there's a divider after the last item of a recycler view. - * @see #isLastItemDecorated() - * @attr ref com.google.android.material.R.styleable#MaterialDivider_lastItemDecorated - */ - public void setLastItemDecorated(boolean lastItemDecorated) { - this.lastItemDecorated = lastItemDecorated; - } - - /** - * Whether there's a divider after the last item of a {@link RecyclerView}. - * - * @see #setLastItemDecorated(boolean) - * @attr ref com.google.android.material.R.styleable#MaterialDivider_shouldDecorateLastItem - */ - public boolean isLastItemDecorated() { - return lastItemDecorated; - } - - @Override - public void onDraw( - @NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - if (parent.getLayoutManager() == null) { - return; - } - if (orientation == VERTICAL) { - drawForVerticalOrientation(canvas, parent); - } else { - drawForHorizontalOrientation(canvas, parent); - } - } - - /** - * Draws a divider for the vertical orientation of the recycler view. The divider itself will be - * horizontal. - */ - private void drawForVerticalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) { - canvas.save(); - int left; - int right; - if (parent.getClipToPadding()) { - left = parent.getPaddingLeft(); - right = parent.getWidth() - parent.getPaddingRight(); - canvas.clipRect( - left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom()); - } else { - left = 0; - right = parent.getWidth(); - } - boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL; - left += isRtl ? insetEnd : insetStart; - right -= isRtl ? insetStart : insetEnd; - - int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = parent.getChildAt(i); - if (shouldDrawDivider(parent, child)) { - parent.getDecoratedBoundsWithMargins(child, tempRect); - // Take into consideration any translationY added to the view. - int bottom = tempRect.bottom + Math.round(child.getTranslationY()); - int top = bottom - thickness; - dividerDrawable.setBounds(left, top, right, bottom); - dividerDrawable.draw(canvas); - } - } - canvas.restore(); - } - - /** - * Draws a divider for the horizontal orientation of the recycler view. The divider itself will be - * vertical. - */ - private void drawForHorizontalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) { - canvas.save(); - int top; - int bottom; - if (parent.getClipToPadding()) { - top = parent.getPaddingTop(); - bottom = parent.getHeight() - parent.getPaddingBottom(); - canvas.clipRect( - parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom); - } else { - top = 0; - bottom = parent.getHeight(); - } - top += insetStart; - bottom -= insetEnd; - - int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = parent.getChildAt(i); - if (shouldDrawDivider(parent, child)) { - parent.getDecoratedBoundsWithMargins(child, tempRect); - // Take into consideration any translationX added to the view. - int right = tempRect.right + Math.round(child.getTranslationX()); - int left = right - thickness; - dividerDrawable.setBounds(left, top, right, bottom); - dividerDrawable.draw(canvas); - } - } - canvas.restore(); - } - - @Override - public void getItemOffsets( - @NonNull Rect outRect, - @NonNull View view, - @NonNull RecyclerView parent, - @NonNull RecyclerView.State state) { - outRect.set(0, 0, 0, 0); - // Only add offset if there's a divider displayed. - if (shouldDrawDivider(parent, view)) { - if (orientation == VERTICAL) { - outRect.bottom = thickness; - } else { - outRect.right = thickness; - } - } - } - - private boolean shouldDrawDivider(@NonNull RecyclerView parent, @NonNull View child) { - int position = parent.getChildAdapterPosition(child); - RecyclerView.Adapter adapter = parent.getAdapter(); - boolean isLastItem = adapter != null && position == adapter.getItemCount() - 1; - - return position != RecyclerView.NO_POSITION - && (!isLastItem || lastItemDecorated) - && shouldDrawDivider(position, adapter); - } - - /** - * Whether a divider should be drawn below the current item that is being drawn. - * - *

Note: if lasItemDecorated is false, the divider below the last item will never be drawn even - * if this method returns true. - * - * @param position the position of the current item being drawn. - * @param adapter the {@link RecyclerView.Adapter} associated with the item being drawn. - */ - protected boolean shouldDrawDivider(int position, @Nullable RecyclerView.Adapter adapter) { - return true; - } -} 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 947362fe7..8db338dd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -94,11 +94,11 @@ interface Settings { final override fun onSharedPreferenceChanged( sharedPreferences: SharedPreferences, - key: String + key: String? ) { // FIXME: Settings initialization firing the listener. logD("Dispatching settings change $key") - onSettingChanged(key, unlikelyToBeNull(listener)) + onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener)) } /** 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 ff54bcfac..3e51b444e 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 @@ -26,10 +26,10 @@ 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 com.google.android.material.divider.MaterialDividerItemDecoration /** - * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly + * A [MaterialDividerItemDecoration] that sets up the divider configuration to correctly * separate preference categories. * * @author Alexander Capehart (OxygenCobalt) @@ -41,7 +41,7 @@ constructor( attributeSet: AttributeSet? = null, defStyleAttr: Int = R.attr.materialDividerStyle, orientation: Int = LinearLayoutManager.VERTICAL -) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) { +) : MaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) { @SuppressLint("RestrictedApi") override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) = try { diff --git a/build.gradle b/build.gradle index dbb61d537..5c4648215 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - kotlin_version = '1.8.21' - navigation_version = "2.5.3" + kotlin_version = '1.8.22' + navigation_version = "2.6.0" hilt_version = '2.46.1' } From 321bbcf03f6389df659d8b7bd6613c0d6c88f8ea Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 15 Jun 2023 13:28:04 -0600 Subject: [PATCH 02/18] all: fix api 34 crashes Fix general API 34 crashes that I could notice immediately. Can really test any further since the API 34 AVDs are currently potatos running at 3 FPS for some reason. --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 ++ .../auxio/playback/system/PlaybackService.kt | 20 ++++++++++++++++--- .../ui/PreferenceHeaderItemDecoration.kt | 4 ++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ca424fca4..1aa7432e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,8 +84,8 @@ dependencies { // --- SUPPORT --- // General - implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.core:core-ktx:1.10.1" + implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.fragment:fragment-ktx:1.6.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd7b28a4a..ba4b27028 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + 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 26629030b..ffcc84b41 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 @@ -25,6 +25,7 @@ import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.media.audiofx.AudioEffect +import android.os.Build import android.os.IBinder import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -150,8 +151,8 @@ class PlaybackService : playbackManager.registerInternalPlayer(this) musicRepository.addUpdateListener(this) mediaSessionComponent.registerListener(this) - registerReceiver( - systemReceiver, + + val intentFilter = IntentFilter().apply { addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(AudioManager.ACTION_HEADSET_PLUG) @@ -162,7 +163,20 @@ class PlaybackService : addAction(ACTION_SKIP_NEXT) addAction(ACTION_EXIT) addAction(WidgetProvider.ACTION_WIDGET_UPDATE) - }) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + registerReceiver( + systemReceiver, + intentFilter, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + RECEIVER_NOT_EXPORTED + } else { + 0 + }) + } else { + registerReceiver(systemReceiver, intentFilter) + } logD("Service created") } 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 3e51b444e..c959d0b3e 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 @@ -29,8 +29,8 @@ import com.google.android.material.R import com.google.android.material.divider.MaterialDividerItemDecoration /** - * A [MaterialDividerItemDecoration] that sets up the divider configuration to correctly - * separate preference categories. + * A [MaterialDividerItemDecoration] that sets up the divider configuration to correctly separate + * preference categories. * * @author Alexander Capehart (OxygenCobalt) */ From f1470af586bd8e0f00b81d89f6ccc691812329f0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 20 Jun 2023 16:59:29 -0600 Subject: [PATCH 03/18] info: add api34 switch to changelog Resolves #354. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f8092d2..f9b79c7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## dev + +#### What's New +- Updated to Android 14 + ## 3.1.2 #### What's Improved From 903a3e561aa69174b935256f3cfdefe162912286 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 20 Jun 2023 17:25:37 -0600 Subject: [PATCH 04/18] image: add option to restore 1:1 crop behavior Add an option to restore the old 1:1 crop behavior to the app. Some people think this looks better, some people like to have youtube thumbnails in their APICs. Can't really be opinionated here. --- .../java/org/oxycblt/auxio/image/CoverView.kt | 17 ++++-- .../org/oxycblt/auxio/image/ImageSettings.kt | 5 ++ .../auxio/image/extractor/CoverExtractor.kt | 26 ++------- .../RoundedRectTransformation.kt} | 8 +-- .../extractor/SquareCropTransformation.kt | 58 +++++++++++++++++++ .../categories/MusicPreferenceFragment.kt | 3 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 18 ++++-- app/src/main/res/values/settings.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences_music.xml | 6 ++ 10 files changed, 110 insertions(+), 34 deletions(-) rename app/src/main/java/org/oxycblt/auxio/image/{RoundedCornersTransformation.kt => extractor/RoundedRectTransformation.kt} (96%) create mode 100644 app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index b2912595c..1ec38068e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -48,6 +48,8 @@ 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.RoundedRectTransformation +import org.oxycblt.auxio.image.extractor.SquareCropTransformation import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -77,6 +79,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr FrameLayout(context, attrs, defStyleAttr) { @Inject lateinit var imageLoader: ImageLoader @Inject lateinit var uiSettings: UISettings + @Inject lateinit var imageSettings: ImageSettings private val image: ImageView @@ -384,13 +387,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr ImageRequest.Builder(context) .data(songs) .error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes)) - .transformations( - RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f)) .target(image) - .build() + + val cornersTransformation = + RoundedRectTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f) + if (imageSettings.forceSquareCovers) { + request.transformations(SquareCropTransformation.INSTANCE, cornersTransformation) + } else { + request.transformations(cornersTransformation) + } + // Dispose of any previous image request and load a new image. CoilUtils.dispose(image) - imageLoader.enqueue(request) + imageLoader.enqueue(request.build()) contentDescription = desc } 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 a4c13c4c9..1a9a01b24 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -34,6 +34,8 @@ import org.oxycblt.auxio.util.logD interface ImageSettings : Settings { /** The strategy to use when loading album covers. */ val coverMode: CoverMode + /** Whether to force all album covers to have a 1:1 aspect ratio. */ + val forceSquareCovers: Boolean interface Listener { /** Called when [coverMode] changes. */ @@ -49,6 +51,9 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) ?: CoverMode.MEDIA_STORE + override val forceSquareCovers: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false) + override fun migrate() { // Show album covers and Ignore MediaStore covers were unified in 3.0.0 if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) || 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 6ca126256..537d8e874 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,7 +43,6 @@ 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 @@ -155,7 +154,7 @@ constructor( // Get the embedded picture from MediaMetadataRetriever, which will return a full // ByteArray of the cover without any compression artifacts. // If its null [i.e there is no embedded cover], than just ignore it and move on - return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } + embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } } private suspend fun extractExoplayerCover(album: Album): InputStream? { @@ -212,7 +211,7 @@ constructor( } /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ - private fun createMosaic(streams: List, size: Size): FetchResult { + private suspend 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 = @@ -234,7 +233,9 @@ constructor( // Crop the bitmap down to a square so it leaves no empty space // TODO: Work around this - val bitmap = cropBitmap(BitmapFactory.decodeStream(stream), mosaicFrameSize) + val bitmap = + SquareCropTransformation.INSTANCE.transform( + BitmapFactory.decodeStream(stream), mosaicFrameSize) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) x += bitmap.width @@ -259,21 +260,4 @@ constructor( 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/RoundedCornersTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt rename to app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt index c25770ec4..c8d3ee145 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * RoundedCornersTransformation.kt is part of Auxio. + * RoundedRectTransformation.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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.image +package org.oxycblt.auxio.image.extractor import android.graphics.Bitmap import android.graphics.Bitmap.createBitmap @@ -43,7 +43,7 @@ import kotlin.math.roundToInt * * @author Coil Team, Alexander Capehart (OxygenCobalt) */ -class RoundedCornersTransformation( +class RoundedRectTransformation( @Px private val topLeft: Float = 0f, @Px private val topRight: Float = 0f, @Px private val bottomLeft: Float = 0f, @@ -122,7 +122,7 @@ class RoundedCornersTransformation( override fun equals(other: Any?): Boolean { if (this === other) return true - return other is RoundedCornersTransformation && + return other is RoundedRectTransformation && topLeft == other.topLeft && topRight == other.topRight && bottomLeft == other.bottomLeft && diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt new file mode 100644 index 000000000..57f03dbef --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Auxio Project + * SquareCropTransformation.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 SquareCropTransformation : Transformation { + override val cacheKey: String + get() = "SquareCropTransformation" + + 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 = SquareCropTransformation() + } +} 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 b0b59d02b..9e1e83af5 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 @@ -47,7 +47,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) } override fun onSetupPreference(preference: Preference) { - if (preference.key == getString(R.string.set_key_cover_mode)) { + if (preference.key == getString(R.string.set_key_cover_mode) || + preference.key == getString(R.string.set_key_square_covers)) { logD("Configuring cover mode setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> 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 1cb3f599c..451dcabd7 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -27,7 +27,8 @@ 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.RoundedCornersTransformation +import org.oxycblt.auxio.image.extractor.RoundedRectTransformation +import org.oxycblt.auxio.image.extractor.SquareCropTransformation import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.queue.Queue @@ -98,10 +99,19 @@ constructor( return if (cornerRadius > 0) { // If rounded, reduce the bitmap size further to obtain more pronounced // rounded corners. - builder - .size(getSafeRemoteViewsImageSize(context, 10f)) - .transformations(RoundedCornersTransformation(cornerRadius.toFloat())) + builder.size(getSafeRemoteViewsImageSize(context, 10f)) + val cornersTransformation = + RoundedRectTransformation(cornerRadius.toFloat()) + if (imageSettings.forceSquareCovers) { + builder.transformations( + SquareCropTransformation.INSTANCE, cornersTransformation) + } else { + builder.transformations(cornersTransformation) + } } else { + if (imageSettings.forceSquareCovers) { + builder.transformations(SquareCropTransformation.INSTANCE) + } builder.size(getSafeRemoteViewsImageSize(context)) } } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index faab5d5cd..f3c956766 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -15,6 +15,7 @@ auxio_observing auxio_music_dirs auxio_cover_mode + auxio_square_covers auxio_include_dirs auxio_exclude_non_music auxio_separators diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 676133e13..95e6c348e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,6 +239,8 @@ Off Fast High quality + Force square album covers + Crop all album covers to a 1:1 aspect ratio Audio Configure sound and playback behavior diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml index a46bb1025..86a3a6b03 100644 --- a/app/src/main/res/xml/preferences_music.xml +++ b/app/src/main/res/xml/preferences_music.xml @@ -43,5 +43,11 @@ app:key="@string/set_key_cover_mode" app:title="@string/set_cover_mode" /> + + \ No newline at end of file From 0042f42ced2320f599c451700c609b1e46858ba2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 20 Jun 2023 21:49:57 -0600 Subject: [PATCH 05/18] playback: fix queue sheet scroll behavior Fix an issue where an upwards nested scroll past the top of the queue list would not actually collapse the sheet. This was apparently caused by something introduced in recyclerview 1.3.0-alpha01. No idea why. Roll back to the working version. --- CHANGELOG.md | 5 +++++ app/build.gradle | 16 +++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b79c7e4..15003e252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ #### What's New - Updated to Android 14 +- Added option to re-enable old album cover cropping behavior + +#### What's Fixed +- Fixed an issue where the queue sheet would not collapse when scrolling +the song list in some cases ## 3.1.2 diff --git a/app/build.gradle b/app/build.gradle index 9d30bb38c..ff160bdd6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ android { versionName "3.1.2" versionCode 32 - minSdk 24 + minSdk 21 targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -80,6 +80,7 @@ dependencies { def coroutines_version = '1.7.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' // --- SUPPORT --- @@ -89,11 +90,14 @@ dependencies { implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.fragment:fragment-ktx:1.6.0" - // UI - implementation "androidx.recyclerview:recyclerview:1.3.0" + // Components + // Deliberately kept on 1.2.1 to prevent a bug where the queue sheet will not collapse on + // certain upwards scrolling events + // TODO: Report this issue and hope for a timely fix + // noinspection GradleDependency + implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" - implementation 'androidx.core:core-ktx:1.10.1' + implementation "androidx.viewpager2:viewpager2:1.0.0" // Lifecycle def lifecycle_version = "2.6.1" @@ -128,8 +132,6 @@ dependencies { 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 - // in a version that I can build with // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. implementation "com.google.android.material:material:1.10.0-alpha04" From 8bcc86c972c906ca758244b91d7f29ae24949bc5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 20 Jun 2023 22:17:08 -0600 Subject: [PATCH 06/18] build: fix redundant recipes --- app/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ff160bdd6..09c4dba6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ android { versionName "3.1.2" versionCode 32 - minSdk 21 + minSdk 24 targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -80,7 +80,6 @@ dependencies { def coroutines_version = '1.7.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' // --- SUPPORT --- From 036d952085b4740c94dacddd7efe053f1934aec2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 23 Jun 2023 08:43:21 -0600 Subject: [PATCH 07/18] music: avoid hanging on discovery errors Do not hang when an error halts the discovery process. This was an oversight with the previous band-aid fix regarding handling errors in music loading. If something failed, the channels would not close, resulting in the main loop consuming the channel hanging. There's probably a deeper issue causing this in 3.1.2, but with this fix I can actually start digging for it. --- .../oxycblt/auxio/music/MusicRepository.kt | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 513786f87..15cb81511 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -372,7 +372,14 @@ constructor( // Do the initial query of the cache and media databases in parallel. logD("Starting MediaStore query") - val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } + val mediaStoreQueryJob = worker.scope.async { + val query = try { + mediaStoreExtractor.query() + } catch (e: Exception) { + return@async Result.failure(e) + } + Result.success(query) + } val cache = if (withCache) { logD("Reading cache") @@ -392,22 +399,39 @@ constructor( val processedSongs = Channel(Channel.UNLIMITED) logD("Started MediaStore discovery") val mediaStoreJob = - worker.scope.tryAsync { - mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) + worker.scope.async { + try { + mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) + } catch (e: Exception) { + incompleteSongs.close(e) + return@async + } incompleteSongs.close() } - logD("Started ExoPlayer discovery") + + logD("Started ExoPlayer tag extraction") val metadataJob = - worker.scope.tryAsync { - tagExtractor.consume(incompleteSongs, completeSongs) + worker.scope.async { + try { + tagExtractor.consume(incompleteSongs, completeSongs) + } catch (e: Exception) { + completeSongs.close(e) + return@async + } completeSongs.close() } + logD("Starting DeviceLibrary creation") val deviceLibraryJob = - worker.scope.tryAsync(Dispatchers.Default) { - deviceLibraryFactory.create(completeSongs, processedSongs).also { - processedSongs.close() + worker.scope.async(Dispatchers.Default) { + val deviceLibrary = try { + deviceLibraryFactory.create(completeSongs, processedSongs) + } catch (e: Exception) { + processedSongs.close(e) + return@async Result.failure(e) } + processedSongs.close() + Result.success(deviceLibrary) } // Await completed raw songs as they are processed. @@ -418,8 +442,8 @@ constructor( } 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() + mediaStoreJob.await() + metadataJob.await() if (rawSongs.isEmpty()) { logE("Music library was empty") @@ -431,7 +455,14 @@ constructor( logD("Discovered ${rawSongs.size} songs, starting finalization") emitIndexingProgress(IndexingProgress.Indeterminate) logD("Starting UserLibrary query") - val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() } + val userLibraryQueryJob = worker.scope.async { + val rawPlaylists = try { + userLibraryFactory.query() + } catch (e: Exception) { + return@async Result.failure(e) + } + Result.success(rawPlaylists) + } if (cache == null || cache.invalidated) { logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) @@ -446,8 +477,6 @@ constructor( logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") emitIndexingCompletion(null) - // Comparing the library instances is obscenely expensive, do it within the library - val deviceLibraryChanged: Boolean val userLibraryChanged: Boolean synchronized(this) { @@ -462,27 +491,12 @@ constructor( this.userLibrary = userLibrary } + // Listeners are expecting a callback in the main thread, switch withContext(Dispatchers.Main) { dispatchLibraryChange(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 - ) = - async(context) { - try { - Result.success(block()) - } catch (e: Exception) { - Result.failure(e) - } - } - private suspend fun emitIndexingProgress(progress: IndexingProgress) { yield() synchronized(this) { From 19e8536323fec22461056697591c59eb7618de2a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 23 Jun 2023 08:49:08 -0600 Subject: [PATCH 08/18] music: fix testing artifacts in loading process Fix testing lines created when working on the prior commit. --- .../oxycblt/auxio/music/MusicRepository.kt | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 15cb81511..2c6ad5bbb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -23,8 +23,6 @@ import android.content.pm.PackageManager import androidx.core.content.ContextCompat import java.util.LinkedList import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -372,14 +370,16 @@ constructor( // Do the initial query of the cache and media databases in parallel. logD("Starting MediaStore query") - val mediaStoreQueryJob = worker.scope.async { - val query = try { - mediaStoreExtractor.query() - } catch (e: Exception) { - return@async Result.failure(e) + val mediaStoreQueryJob = + worker.scope.async { + val query = + try { + mediaStoreExtractor.query() + } catch (e: Exception) { + return@async Result.failure(e) + } + Result.success(query) } - Result.success(query) - } val cache = if (withCache) { logD("Reading cache") @@ -424,12 +424,13 @@ constructor( logD("Starting DeviceLibrary creation") val deviceLibraryJob = worker.scope.async(Dispatchers.Default) { - val deviceLibrary = try { - deviceLibraryFactory.create(completeSongs, processedSongs) - } catch (e: Exception) { - processedSongs.close(e) - return@async Result.failure(e) - } + val deviceLibrary = + try { + deviceLibraryFactory.create(completeSongs, processedSongs) + } catch (e: Exception) { + processedSongs.close(e) + return@async Result.failure(e) + } processedSongs.close() Result.success(deviceLibrary) } @@ -455,14 +456,16 @@ constructor( logD("Discovered ${rawSongs.size} songs, starting finalization") emitIndexingProgress(IndexingProgress.Indeterminate) logD("Starting UserLibrary query") - val userLibraryQueryJob = worker.scope.async { - val rawPlaylists = try { - userLibraryFactory.query() - } catch (e: Exception) { - return@async Result.failure(e) + val userLibraryQueryJob = + worker.scope.async { + val rawPlaylists = + try { + userLibraryFactory.query() + } catch (e: Exception) { + return@async Result.failure(e) + } + Result.success(rawPlaylists) } - Result.success(rawPlaylists) - } if (cache == null || cache.invalidated) { logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) From ed7b4e1410add256f82f8207f805f1d507c0c511 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 09:04:21 -0600 Subject: [PATCH 09/18] music: recognize spaced artist tags Recognize artists sort, albumartists sort, and album artists tags. These are written by mediafile, so they are probably also written by other software. --- CHANGELOG.md | 3 ++ .../oxycblt/auxio/music/metadata/TagWorker.kt | 30 +++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15003e252..9213d0a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Updated to Android 14 - Added option to re-enable old album cover cropping behavior +#### What's Improved +- `album artists` and `(album)artists sort` are now recognized + #### What's Fixed - Fixed an issue where the queue sheet would not collapse when scrolling the song list in some cases 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 5d9eebcbc..fae02585e 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 @@ -149,18 +149,22 @@ private class TagWorkerImpl( // Artist textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } - (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"]) + (textFrames["TXXX:artistssort"] + ?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"] + ?: textFrames["TSOP"]) ?.let { rawSong.artistSortNames = it } // Album artist textFrames["TXXX:musicbrainz album artist id"]?.let { rawSong.albumArtistMusicBrainzIds = it } - (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { - rawSong.albumArtistNames = it - } + (textFrames["TXXX:albumartists"] + ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"] + ?: textFrames["TPE2"]) + ?.let { rawSong.albumArtistNames = it } (textFrames["TXXX:albumartistssort"] - ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"] + ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"] + ?: textFrames["TXXX:albumartistsort"] // This is a non-standard iTunes extension ?: textFrames["TSO2"]) ?.let { rawSong.albumArtistSortNames = it } @@ -261,17 +265,19 @@ private class TagWorkerImpl( // Artist comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } - (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let { - rawSong.artistSortNames = it - } + (comments["artistssort"] + ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"]) + ?.let { rawSong.artistSortNames = it } // Album artist comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } - (comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let { - rawSong.albumArtistNames = it - } + (comments["albumartists"] + ?: comments["album_artists"] ?: comments["album artists"] + ?: comments["albumartist"]) + ?.let { rawSong.albumArtistNames = it } (comments["albumartistssort"] - ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) + ?: comments["albumartists_sort"] ?: comments["albumartists sort"] + ?: comments["albumartistsort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre From 992457f3618650f81c78d626fa67cde5cf4a04db Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 10:34:31 -0600 Subject: [PATCH 10/18] music: document indexing process Further document the music indexing process. It's so aggressively parallelized as to require some more extensive comments to actually make it clear what's going on. --- CHANGELOG.md | 1 + .../oxycblt/auxio/music/MusicRepository.kt | 138 ++++++++++++------ 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9213d0a41..6a3575b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ #### What's Fixed - Fixed an issue where the queue sheet would not collapse when scrolling the song list in some cases +- Fixed music loading hanging if it encountered an error in certain places ## 3.1.2 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 2c6ad5bbb..233872447 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -337,49 +337,56 @@ constructor( } override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) = - worker.scope.launch { - try { - val start = System.currentTimeMillis() - indexImpl(worker, withCache) - logD( - "Music indexing completed successfully in " + - "${System.currentTimeMillis() - start}ms") - } catch (e: CancellationException) { - // Got cancelled, propagate upwards to top-level co-routine. - logD("Loading routine was cancelled") - throw e - } catch (e: Exception) { - // Music loading process failed due to something we have not handled. - logE("Music indexing failed") - logE(e.stackTraceToString()) - emitIndexingCompletion(e) - } + worker.scope.launch { indexWrapper(worker, withCache) } + + private suspend fun indexWrapper(worker: MusicRepository.IndexingWorker, withCache: Boolean) { + try { + indexImpl(worker, withCache) + } catch (e: CancellationException) { + // Got cancelled, propagate upwards to top-level co-routine. + logD("Loading routine was cancelled") + throw e + } catch (e: Exception) { + // Music loading process failed due to something we have not handled. + // TODO: Still want to display this error eventually + logE("Music indexing failed") + logE(e.stackTraceToString()) + emitIndexingCompletion(e) } + } private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) { + val start = System.currentTimeMillis() + // Make sure we have permissions before going forward. Theoretically this would be better + // done at the UI level, but that intertwines logic and display too much. if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) == PackageManager.PERMISSION_DENIED) { logE("Permissions were not granted") - // No permissions, signal that we can't do anything. throw NoAudioPermissionException() } - // Start initializing the extractors. Use an indeterminate state, as there is no ETA on - // how long a media database query will take. - emitIndexingProgress(IndexingProgress.Indeterminate) - - // Do the initial query of the cache and media databases in parallel. + // Begin with querying MediaStore and the music cache. The former is needed for Auxio + // to figure out what songs are (probably) on the device, and the latter will be needed + // for discovery (described later). These have no shared state, so they are done in + // parallel. logD("Starting MediaStore query") + emitIndexingProgress(IndexingProgress.Indeterminate) val mediaStoreQueryJob = worker.scope.async { val query = try { mediaStoreExtractor.query() } catch (e: Exception) { + // Normally, errors in an async call immediately bubble up to the Looper + // and crash the app. Thus, we have to wrap any error into a Result + // and then manually forward it to the try block that indexImpl is + // called from. return@async Result.failure(e) } Result.success(query) } + // Since this main thread is a co-routine, we can do operations in parallel in a way + // identical to calling async. val cache = if (withCache) { logD("Reading cache") @@ -390,27 +397,36 @@ constructor( logD("Awaiting MediaStore query") val query = mediaStoreQueryJob.await().getOrThrow() - // Now start processing the queried song information in parallel. Songs that can't be - // received from the cache are consisted incomplete and pushed to a separate channel - // that will eventually be processed into completed raw songs. - logD("Starting song discovery") - val completeSongs = Channel(Channel.UNLIMITED) - val incompleteSongs = Channel(Channel.UNLIMITED) - val processedSongs = Channel(Channel.UNLIMITED) - logD("Started MediaStore discovery") + // We now have all the information required to start the "discovery" process. This + // is the point at which Auxio starts scanning each file given from MediaStore and + // transforming it into a music library. MediaStore normally + logD("Starting discovery") + val incompleteSongs = Channel(Channel.UNLIMITED) // Not fully populated w/metadata + val completeSongs = Channel(Channel.UNLIMITED) // Populated with quality metadata + val processedSongs = Channel(Channel.UNLIMITED) // Transformed into SongImpl + + // MediaStoreExtractor discovers all music on the device, and forwards them to either + // DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata + // does not exist. In the latter situation, it also applies it's own (inferior) metadata. + logD("Starting MediaStore discovery") val mediaStoreJob = worker.scope.async { try { mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) } catch (e: Exception) { + // To prevent a deadlock, we want to close the channel with an exception + // to cascade to and cancel all other routines before finally bubbling up + // to the main extractor loop. incompleteSongs.close(e) return@async } incompleteSongs.close() } - logD("Started ExoPlayer tag extraction") - val metadataJob = + // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date + // metadata for them, and then forwards it to DeviceLibrary. + logD("Starting tag extraction") + val tagJob = worker.scope.async { try { tagExtractor.consume(incompleteSongs, completeSongs) @@ -421,6 +437,8 @@ constructor( completeSongs.close() } + // DeviceLibrary constructs music parent instances as song information is provided, + // and then forwards them to the primary loading loop. logD("Starting DeviceLibrary creation") val deviceLibraryJob = worker.scope.async(Dispatchers.Default) { @@ -435,26 +453,43 @@ constructor( Result.success(deviceLibrary) } - // Await completed raw songs as they are processed. + // We could keep track of a total here, but we also need to collate this RawSong information + // for when we write the cache later on in the finalization step. val rawSongs = LinkedList() for (rawSong in processedSongs) { rawSongs.add(rawSong) + // Since discovery takes up the bulk of the music loading process, we switch to + // indicating a defined amount of loaded songs in comparison to the projected amount + // of songs that were queried. emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) } - logD("Awaiting discovery completion") - // These should be no-ops, but we need the error state to see if we should keep going. - mediaStoreJob.await() - metadataJob.await() + // This shouldn't occur, but keep them around just in case there's a regression. + // Note that DeviceLibrary might still actually be doing work (specifically parent + // processing), so we don't check if it's deadlocked. + check(!mediaStoreJob.isActive) { "MediaStore discovery is deadlocked" } + check(!tagJob.isActive) { "Tag extraction is deadlocked" } + + // Deliberately done after the involved initialization step to make it less likely + // that the short-circuit occurs so quickly as to break the UI. + // TODO: Do not error, instead just wipe the entire library. if (rawSongs.isEmpty()) { logE("Music library was empty") throw NoMusicException() } - // Successfully loaded the library, now save the cache and read playlist information - // in parallel. + // Now that the library is effectively loaded, we can start the finalization step, which + // involves writing new cache information and creating more music data that is derived + // from the library (e.g playlists) logD("Discovered ${rawSongs.size} songs, starting finalization") + + // We have no idea how long the cache will take, and the playlist construction + // will be too fast to indicate, so switch back to an indeterminate state. emitIndexingProgress(IndexingProgress.Indeterminate) + + // The UserLibrary job is split into a query and construction step, a la MediaStore. + // This way, we can start working on playlists even as DeviceLibrary might still be + // working on parent information. logD("Starting UserLibrary query") val userLibraryQueryJob = worker.scope.async { @@ -466,10 +501,17 @@ constructor( } Result.success(rawPlaylists) } + + // The cache might not exist, or we might have encountered a song not present in it. + // Both situations require us to rewrite the cache in bulk. This is also done parallel + // since the playlist read will probably take some time. + // TODO: Read/write from the cache incrementally instead of in bulk? if (cache == null || cache.invalidated) { logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) } + + // Create UserLibrary once we finally get the required components for it. logD("Awaiting UserLibrary query") val rawPlaylists = userLibraryQueryJob.await().getOrThrow() logD("Awaiting DeviceLibrary creation") @@ -477,12 +519,14 @@ constructor( logD("Starting UserLibrary creation") val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) - logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") - emitIndexingCompletion(null) - val deviceLibraryChanged: Boolean val userLibraryChanged: Boolean + // We want to make sure that all reads and writes are synchronized due to the sheer + // amount of consumers of MusicRepository. + // TODO: Would Atomics not be a better fit here? synchronized(this) { + // It's possible that this reload might have changed nothing, so make sure that + // hasn't happened before dispatching a change to all consumers. deviceLibraryChanged = this.deviceLibrary != deviceLibrary userLibraryChanged = this.userLibrary != userLibrary if (!deviceLibraryChanged && !userLibraryChanged) { @@ -494,7 +538,13 @@ constructor( this.userLibrary = userLibrary } - // Listeners are expecting a callback in the main thread, switch + // We are finally done. Indicate that loading is no longer occurring, and dispatch the + // results of the loading process to consumers. + logD("Successfully indexed music library [device=$deviceLibrary " + + "user=$userLibrary time=${System.currentTimeMillis() - start}]") + emitIndexingCompletion(null) + // Consumers expect their updates to be on the main thread (notably PlaybackService), + // so switch to it. withContext(Dispatchers.Main) { dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged) } From 29162820ae422c99354aabef1f8c276169070304 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 17:30:06 -0600 Subject: [PATCH 11/18] ui: update bottomsheetbehavior Update BottomSheetBehavior to the latest commit in 1.10.0-alpha04. This adds a few new APIs that I still can't really use. I could in the future. --- .../BackportBottomSheetBehavior.java | 446 +++++++++++++----- 1 file changed, 334 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index c6560c151..ab55a48dc 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,10 +16,14 @@ 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; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; @@ -32,8 +36,10 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; +import android.util.SparseIntArray; import android.util.TypedValue; import android.view.MotionEvent; +import android.view.RoundedCorner; import android.view.VelocityTracker; import android.view.View; import android.view.View.MeasureSpec; @@ -41,13 +47,15 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; +import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; - +import androidx.activity.BackEventCompat; import androidx.annotation.FloatRange; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; @@ -62,14 +70,13 @@ 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.motion.MaterialBackHandler; +import com.google.android.material.motion.MaterialBottomContainerBackHelper; 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; @@ -84,13 +91,11 @@ import java.util.Map; *

To send useful accessibility events, set a title on bottom sheets that are windows or are * window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for * BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. - * - * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently - * obtainable in the currently used MDC library. */ -public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior { +public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior + implements MaterialBackHandler { - /** Listener for monitoring events about bottom sheets. */ + /** Callback for monitoring events about bottom sheets. */ public abstract static class BottomSheetCallback { /** @@ -203,11 +208,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Retention(RetentionPolicy.SOURCE) public @interface SaveFlags {} - private static final String TAG = "BottomSheetBehavior"; + private static final String TAG = "BackportBottomSheetBehavior"; @SaveFlags private int saveFlags = SAVE_NONE; - private static final int SIGNIFICANT_VEL_THRESHOLD = 500; + @VisibleForTesting static final int DEFAULT_SIGNIFICANT_VEL_THRESHOLD = 500; private static final float HIDE_THRESHOLD = 0.5f; @@ -217,12 +222,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private static final int NO_MAX_SIZE = -1; + private static final int VIEW_INDEX_BOTTOM_SHEET = 0; + + private static final int INVALID_POSITION = -1; + + @VisibleForTesting + static final int VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW = 1; + private boolean fitToContents = true; private boolean updateImportantForAccessibilityOnSiblings = false; private float maximumVelocity; + private int significantVelocityThreshold; + /** Peek height set by the user. */ private int peekHeight; @@ -256,10 +270,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private int insetBottom; private int insetTop; + private boolean shouldRemoveExpandedCorners; + /** Default Shape Appearance to be used in bottomsheet */ private ShapeAppearanceModel shapeAppearanceModelDefault; - private boolean isShapeExpanded; + private boolean expandedCornersRemoved; private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker(); @@ -304,22 +320,25 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo int parentHeight; @Nullable WeakReference viewRef; + @Nullable WeakReference accessibilityDelegateViewRef; @Nullable WeakReference nestedScrollingChildRef; @NonNull private final ArrayList callbacks = new ArrayList<>(); @Nullable private VelocityTracker velocityTracker; + @Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper; int activePointerId; - private int initialY; + private int initialY = INVALID_POSITION; boolean touchingScrollingChild; @Nullable private Map importantForAccessibilityMap; - private int expandHalfwayActionId = View.NO_ID; + @VisibleForTesting + final SparseIntArray expandHalfwayActionIds = new SparseIntArray(); public BackportBottomSheetBehavior() {} @@ -387,6 +406,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0)); } + setSignificantVelocityThreshold( + a.getInt( + R.styleable.BottomSheetBehavior_Layout_behavior_significantVelocityThreshold, + DEFAULT_SIGNIFICANT_VEL_THRESHOLD)); + // Reading out if we are handling padding, so we can apply it to the content. paddingBottomSystemWindowInsets = a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false); @@ -404,6 +428,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false); marginTopSystemWindowInsets = a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false); + shouldRemoveExpandedCorners = + a.getBoolean(R.styleable.BottomSheetBehavior_Layout_shouldRemoveExpandedCorners, true); a.recycle(); ViewConfiguration configuration = ViewConfiguration.get(context); @@ -440,6 +466,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // first time we layout with this behavior by checking (viewRef == null). viewRef = null; viewDragHelper = null; + bottomContainerBackHelper = null; } @Override @@ -448,6 +475,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Release references so we don't run unnecessary codepaths while not attached to a view. viewRef = null; viewDragHelper = null; + bottomContainerBackHelper = null; } @Override @@ -515,7 +543,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo peekHeightMin = parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min); setWindowInsetsListener(child); + ViewCompat.setWindowInsetsAnimationCallback(child, new InsetsAnimationCallback(child)); viewRef = new WeakReference<>(child); + bottomContainerBackHelper = new MaterialBottomContainerBackHelper(child); // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will // default to android:background declared in styles or layout. if (materialShapeDrawable != null) { @@ -523,9 +553,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view. materialShapeDrawable.setElevation( elevation == -1 ? ViewCompat.getElevation(child) : elevation); - // Update the material shape based on initial state. - isShapeExpanded = state == STATE_EXPANDED; - materialShapeDrawable.setInterpolation(isShapeExpanded ? 0f : 1f); } else if (backgroundTint != null) { ViewCompat.setBackgroundTintList(child, backgroundTint); } @@ -549,11 +576,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (parentHeight - childHeight < insetTop) { if (paddingTopSystemWindowInsets) { // If the bottomsheet would land in the middle of the status bar when fully expanded add - // extra space to make sure it goes all the way. - childHeight = parentHeight; + // extra space to make sure it goes all the way up or up to max height if it is specified. + childHeight = (maxHeight == NO_MAX_SIZE) ? parentHeight : min(parentHeight, maxHeight); } else { // If we don't want the bottomsheet to go under the status bar we cap its height - childHeight = parentHeight - insetTop; + int insetHeight = parentHeight - insetTop; + childHeight = (maxHeight == NO_MAX_SIZE) ? insetHeight : min(insetHeight, maxHeight); } } fitToContentsOffset = max(0, parentHeight - childHeight); @@ -571,6 +599,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } else if (state == STATE_DRAGGING || state == STATE_SETTLING) { ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); } + updateDrawableForTargetState(state, /* animate= */ false); nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); @@ -640,6 +669,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo && state != STATE_DRAGGING && !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && viewDragHelper != null + && initialY != INVALID_POSITION && Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop(); } @@ -723,8 +753,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } else if (dy < 0) { // Downward if (!target.canScrollVertically(-1)) { - // MODIFICATION: Add isHideableWhenDragging method - if (newTop <= collapsedOffset || (hideable && isHideableWhenDragging())) { + if (newTop <= collapsedOffset || canBeHiddenByDragging()) { if (!draggable) { // Prevent dragging return; @@ -778,7 +807,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } } - // MODIFICATION: Add isHideableWhenDragging method } else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) { targetState = STATE_HIDDEN; } else if (lastNestedScrollDy == 0) { @@ -888,6 +916,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo // Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents. setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state); + updateDrawableForTargetState(state, /* animate= */ true); updateAccessibilityActions(); } @@ -897,7 +926,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * be adjusted as expected. * * @param maxWidth The maximum width in pixels to be set - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth * @see #getMaxWidth() */ public void setMaxWidth(@Px int maxWidth) { @@ -907,7 +936,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo /** * Returns the bottom sheet's maximum width, or -1 if no maximum width is set. * - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth * @see #setMaxWidth(int) */ @Px @@ -920,7 +949,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * BottomSheetDialog#show()} in order for the height to be adjusted as expected. * * @param maxHeight The maximum height in pixels to be set - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight * @see #getMaxHeight() */ public void setMaxHeight(@Px int maxHeight) { @@ -930,7 +959,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo /** * Returns the bottom sheet's maximum height, or -1 if no maximum height is set. * - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight * @see #setMaxHeight(int) */ @Px @@ -944,7 +973,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public void setPeekHeight(int peekHeight) { setPeekHeight(peekHeight, false); @@ -958,7 +987,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @param animate Whether to animate between the old height and the new height. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public final void setPeekHeight(int peekHeight, boolean animate) { boolean layout = false; @@ -1001,7 +1030,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the * sheet is configured to peek automatically at 16:9 ratio keyline * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight */ public int getPeekHeight() { return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight; @@ -1015,7 +1044,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio */ public void setHalfExpandedRatio( @FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) { @@ -1035,7 +1064,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state. * * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio */ @FloatRange(from = 0.0f, to = 1.0f) public float getHalfExpandedRatio() { @@ -1050,13 +1079,14 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @param offset an integer value greater than equal to 0, representing the {@link * #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset */ public void setExpandedOffset(int offset) { if (offset < 0) { throw new IllegalArgumentException("offset must be greater than or equal to 0"); } this.expandedOffset = offset; + updateDrawableForTargetState(state, /* animate= */ true); } /** @@ -1064,7 +1094,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * pick the offset depending on the height of the content. * * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset */ public int getExpandedOffset() { return fitToContents @@ -1072,8 +1102,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo : Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop); } - // MODIFICATION: Add calculateSlideOffset method - /** * Calculates the current offset of the bottom sheet. * @@ -1082,26 +1110,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * @return The offset of this bottom sheet within [-1,1] range. Offset increases * as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and * expanded states and from -1 to 0 it is between hidden and collapsed states. Returns - * {@code Float.MIN_VALUE} if the bottom sheet is not laid out. + * -1 if the bottom sheet is not laid out (therefore it's hidden). */ public float calculateSlideOffset() { - if (viewRef == null) { - return Float.MIN_VALUE; + if (viewRef == null || viewRef.get() == null) { + return -1; } - View bottomSheet = viewRef.get(); - if (bottomSheet != null) { - return calculateSlideOffset(bottomSheet.getTop()); - } - - return Float.MIN_VALUE; + return calculateSlideOffsetWithTop(viewRef.get().getTop()); } /** - * Sets whether this bottom sheet can hide when it is swiped down. + * Sets whether this bottom sheet can hide. * * @param hideable {@code true} to make this bottom sheet hideable. - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable */ public void setHideable(boolean hideable) { if (this.hideable != hideable) { @@ -1118,7 +1141,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Gets whether this bottom sheet can hide when it is swiped down. * * @return {@code true} if this bottom sheet can hide. - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable */ public boolean isHideable() { return hideable; @@ -1130,7 +1153,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @param skipCollapsed True if the bottom sheet should skip the collapsed state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed */ public void setSkipCollapsed(boolean skipCollapsed) { this.skipCollapsed = skipCollapsed; @@ -1142,7 +1165,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * * @return Whether the bottom sheet should skip the collapsed state. * @attr ref - * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed */ public boolean getSkipCollapsed() { return skipCollapsed; @@ -1153,7 +1176,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * dragging, an app will require to implement a custom way to expand/collapse the bottom sheet * * @param draggable {@code false} to prevent dragging the sheet to collapse and expand - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_draggable + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_draggable */ public void setDraggable(boolean draggable) { this.draggable = draggable; @@ -1163,13 +1186,35 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return draggable; } + /* + * Sets the velocity threshold considered significant enough to trigger a slide + * to the next stable state. + * + * @param significantVelocityThreshold The velocity threshold that warrants a vertical swipe. + * @see #getSignificantVelocityThreshold() + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold + */ + public void setSignificantVelocityThreshold(int significantVelocityThreshold) { + this.significantVelocityThreshold = significantVelocityThreshold; + } + + /* + * Returns the significant velocity threshold. + * + * @see #setSignificantVelocityThreshold(int) + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold + */ + public int getSignificantVelocityThreshold() { + return this.significantVelocityThreshold; + } + /** * Sets save flags to be preserved in bottomsheet on configuration change. * * @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link * #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}. * @see #getSaveFlags() - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags */ public void setSaveFlags(@SaveFlags int flags) { this.saveFlags = flags; @@ -1178,7 +1223,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo * Returns the save flags. * * @see #setSaveFlags(int) - * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags + * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags */ @SaveFlags public int getSaveFlags() { @@ -1208,9 +1253,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Sets a listener to be notified of bottom sheet events. + * Sets a callback to be notified of bottom sheet events. * - * @param callback The listener to notify when bottom sheet events occur. + * @param callback The callback to notify when bottom sheet events occur. * @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link * #removeBottomSheetCallback(BottomSheetCallback)} instead */ @@ -1218,7 +1263,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo public void setBottomSheetCallback(BottomSheetCallback callback) { Log.w( TAG, - "BottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes" + "BackportBottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes" + " all existing callbacks, including ones set internally by library authors, which" + " may result in unintended behavior. This may change in the future. Please use" + " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your" @@ -1230,9 +1275,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Adds a listener to be notified of bottom sheet events. + * Adds a callback to be notified of bottom sheet events. * - * @param callback The listener to notify when bottom sheet events occur. + * @param callback The callback to notify when bottom sheet events occur. */ public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) { if (!callbacks.contains(callback)) { @@ -1241,9 +1286,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Removes a previously added listener. + * Removes a previously added callback. * - * @param callback The listener to remove. + * @param callback The callback to remove. */ public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) { callbacks.remove(callback); @@ -1325,6 +1370,26 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return gestureInsetBottomIgnored; } + /** + * Sets whether the bottom sheet should remove its corners when it reaches the expanded state. + * + *

If false, the bottom sheet will only remove its corners if it is expanded and reaches the + * top of the screen. + */ + public void setShouldRemoveExpandedCorners(boolean shouldRemoveExpandedCorners) { + if (this.shouldRemoveExpandedCorners != shouldRemoveExpandedCorners) { + this.shouldRemoveExpandedCorners = shouldRemoveExpandedCorners; + updateDrawableForTargetState(getState(), /* animate= */ true); + } + } + + /** + * Returns whether the bottom sheet will remove its corners when it reaches the expanded state. + */ + public boolean isShouldRemoveExpandedCorners() { + return shouldRemoveExpandedCorners; + } + /** * Gets the current state of the bottom sheet. * @@ -1376,33 +1441,91 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo updateImportantForAccessibility(false); } - updateDrawableForTargetState(state); + updateDrawableForTargetState(state, /* animate= */ true); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onStateChanged(bottomSheet, state); } updateAccessibilityActions(); } - private void updateDrawableForTargetState(@State int state) { + private void updateDrawableForTargetState(@State int state, boolean animate) { if (state == STATE_SETTLING) { // Special case: we want to know which state we're settling to, so wait for another call. return; } - boolean expand = state == STATE_EXPANDED; - if (isShapeExpanded != expand) { - isShapeExpanded = expand; - if (materialShapeDrawable != null && interpolatorAnimator != null) { - if (interpolatorAnimator.isRunning()) { - interpolatorAnimator.reverse(); - } else { - float to = expand ? 0f : 1f; - float from = 1f - to; - interpolatorAnimator.setFloatValues(from, to); - interpolatorAnimator.start(); + boolean removeCorners = isExpandedAndShouldRemoveCorners(); + if (expandedCornersRemoved == removeCorners || materialShapeDrawable == null) { + return; + } + expandedCornersRemoved = removeCorners; + if (animate && interpolatorAnimator != null) { + if (interpolatorAnimator.isRunning()) { + interpolatorAnimator.reverse(); + } else { + float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f; + float from = 1f - to; + interpolatorAnimator.setFloatValues(from, to); + interpolatorAnimator.start(); + } + } else { + if (interpolatorAnimator != null && interpolatorAnimator.isRunning()) { + interpolatorAnimator.cancel(); + } + materialShapeDrawable.setInterpolation( + expandedCornersRemoved ? calculateInterpolationWithCornersRemoved() : 1f); + } + } + + private float calculateInterpolationWithCornersRemoved() { + if (materialShapeDrawable != null + && viewRef != null + && viewRef.get() != null + && VERSION.SDK_INT >= VERSION_CODES.S) { + V view = viewRef.get(); + // Only use device corner radius if sheet is touching top of screen. + if (isAtTopOfScreen()) { + final WindowInsets insets = view.getRootWindowInsets(); + if (insets != null) { + float topLeftInterpolation = + calculateCornerInterpolation( + materialShapeDrawable.getTopLeftCornerResolvedSize(), + insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)); + float topRightInterpolation = + calculateCornerInterpolation( + materialShapeDrawable.getTopRightCornerResolvedSize(), + insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)); + return Math.max(topLeftInterpolation, topRightInterpolation); } } } + return 0; + } + + @RequiresApi(VERSION_CODES.S) + private float calculateCornerInterpolation( + float materialShapeDrawableCornerSize, @Nullable RoundedCorner deviceRoundedCorner) { + if (deviceRoundedCorner != null) { + float deviceCornerRadius = deviceRoundedCorner.getRadius(); + if (deviceCornerRadius > 0 && materialShapeDrawableCornerSize > 0) { + return deviceCornerRadius / materialShapeDrawableCornerSize; + } + } + return 0; + } + + private boolean isAtTopOfScreen() { + if (viewRef == null || viewRef.get() == null) { + return false; + } + int[] location = new int[2]; + viewRef.get().getLocationOnScreen(location); + return location[1] == 0; + } + + private boolean isExpandedAndShouldRemoveCorners() { + // Only remove corners when it's full screen. + return state == STATE_EXPANDED && (shouldRemoveExpandedCorners || isAtTopOfScreen()); } private int calculatePeekHeight() { @@ -1432,9 +1555,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio)); } - // MODIFICATION: Add calculateSlideOffset method - - private float calculateSlideOffset(int top) { + private float calculateSlideOffsetWithTop(int top) { return (top > collapsedOffset || collapsedOffset == getExpandedOffset()) ? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset) @@ -1443,6 +1564,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo private void reset() { activePointerId = ViewDragHelper.INVALID_POINTER; + initialY = INVALID_POSITION; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; @@ -1473,6 +1595,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (skipCollapsed) { return true; } + if (!isHideableWhenDragging()) { + return false; + } if (child.getTop() < collapsedOffset) { // It should not hide, but collapse. return false; @@ -1482,9 +1607,73 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD; } + @Override + public void startBackProgress(@NonNull BackEventCompat backEvent) { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.startBackProgress(backEvent); + } + + @Override + public void updateBackProgress(@NonNull BackEventCompat backEvent) { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.updateBackProgress(backEvent); + } + + @Override + public void handleBackInvoked() { + if (bottomContainerBackHelper == null) { + return; + } + BackEventCompat backEvent = bottomContainerBackHelper.onHandleBackInvoked(); + if (backEvent == null || VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) { + // If using traditional button system nav or if pre-U, just hide or collapse the bottom sheet. + setState(hideable ? STATE_HIDDEN : STATE_COLLAPSED); + return; + } + if (hideable) { + bottomContainerBackHelper.finishBackProgressNotPersistent( + backEvent, + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Hide immediately following the built-in predictive back slide down animation. + setStateInternal(STATE_HIDDEN); + if (viewRef != null && viewRef.get() != null) { + viewRef.get().requestLayout(); + } + } + }); + } else { + bottomContainerBackHelper.finishBackProgressPersistent( + backEvent, /* animatorListener= */ null); + setState(STATE_COLLAPSED); + } + } + + @Override + public void cancelBackProgress() { + if (bottomContainerBackHelper == null) { + return; + } + bottomContainerBackHelper.cancelBackProgress(); + } + + @VisibleForTesting + @Nullable + MaterialBottomContainerBackHelper getBackHelper() { + return bottomContainerBackHelper; + } + @Nullable @VisibleForTesting View findScrollingChild(View view) { + if (view.getVisibility() != View.VISIBLE) { + return null; + } if (ViewCompat.isNestedScrollingEnabled(view)) { return view; } @@ -1524,12 +1713,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } - MaterialShapeDrawable getMaterialShapeDrawable() { + protected MaterialShapeDrawable getMaterialShapeDrawable() { return materialShapeDrawable; } private void createShapeValueAnimator() { - interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f); + interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f); interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION); interpolatorAnimator.addUpdateListener( new AnimatorUpdateListener() { @@ -1650,7 +1839,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo if (settling) { setStateInternal(STATE_SETTLING); // STATE_SETTLING won't animate the material shape, so do that here with the target state. - updateDrawableForTargetState(state); + updateDrawableForTargetState(state, /* animate= */ true); stateSettlingTracker.continueSettlingToState(state); } else { setStateInternal(state); @@ -1741,11 +1930,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } } - // MODIFICATION: Add isHideableWhenDragging method - } else if (hideable && shouldHide(releasedChild, yvel) && isHideableWhenDragging()) { + } else if (hideable && shouldHide(releasedChild, yvel)) { // Hide if the view was either released low or it was a significant vertical swipe // otherwise settle to closest expanded state. - if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD) + if ((Math.abs(xvel) < Math.abs(yvel) && yvel > significantVelocityThreshold) || releasedLow(releasedChild)) { targetState = STATE_HIDDEN; } else if (fitToContents) { @@ -1814,9 +2002,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { - // MODIFICATION: Add isHideableWhenDragging method return MathUtils.clamp( - top, getExpandedOffset(), (hideable && isHideableWhenDragging()) ? parentHeight : collapsedOffset); + top, + getExpandedOffset(), + getViewVerticalDragRange(child)); } @Override @@ -1826,8 +2015,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo @Override public int getViewVerticalDragRange(@NonNull View child) { - // MODIFICATION: Add isHideableWhenDragging method - if (hideable && isHideableWhenDragging()) { + if (canBeHiddenByDragging()) { return parentHeight; } else { return collapsedOffset; @@ -1838,8 +2026,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo void dispatchOnSlide(int top) { View bottomSheet = viewRef.get(); if (bottomSheet != null && !callbacks.isEmpty()) { - // MODIFICATION: Add calculateSlideOffset method - float slideOffset = calculateSlideOffset(top); + float slideOffset = calculateSlideOffsetWithTop(top); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onSlide(bottomSheet, slideOffset); } @@ -1898,7 +2085,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } /** - * Checks whether hiding gestures should be enabled if {@code isHideable} is true. + * Checks whether hiding gestures should be enabled while {@code isHideable} is set to true. + * * @hide */ @RestrictTo(LIBRARY_GROUP) @@ -1906,6 +2094,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return true; } + private boolean canBeHiddenByDragging() { + return isHideable() && isHideableWhenDragging(); + } + /** * Checks whether the bottom sheet should be expanded after it has been released after dragging. * @@ -2067,7 +2259,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (!(behavior instanceof BackportBottomSheetBehavior)) { - throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior"); + throw new IllegalArgumentException("The view is not associated with BackportBottomSheetBehavior"); } return (BackportBottomSheetBehavior) behavior; } @@ -2139,30 +2331,43 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo } } + void setAccessibilityDelegateView(@Nullable View accessibilityDelegateView) { + if (accessibilityDelegateView == null && accessibilityDelegateViewRef != null) { + clearAccessibilityAction( + accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); + accessibilityDelegateViewRef = null; + return; + } + accessibilityDelegateViewRef = new WeakReference<>(accessibilityDelegateView); + updateAccessibilityActions(accessibilityDelegateView, VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); + } + private void updateAccessibilityActions() { - if (viewRef == null) { - return; + if (viewRef != null) { + updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET); } - V child = viewRef.get(); - if (child == null) { - return; + if (accessibilityDelegateViewRef != null) { + updateAccessibilityActions( + accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW); } - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND); - ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS); + } - if (expandHalfwayActionId != View.NO_ID) { - ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId); + private void updateAccessibilityActions(View view, int viewIndex) { + if (view == null) { + return; } + clearAccessibilityAction(view, viewIndex); + if (!fitToContents && state != STATE_HALF_EXPANDED) { - expandHalfwayActionId = + expandHalfwayActionIds.put( + viewIndex, addAccessibilityActionForState( - child, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED); + view, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED)); } - if (hideable && state != STATE_HIDDEN) { + if ((hideable && isHideableWhenDragging()) && state != STATE_HIDDEN) { replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); + view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); } switch (state) { @@ -2170,36 +2375,54 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo { int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED; replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); + view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); break; } case STATE_HALF_EXPANDED: { replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); + view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); + view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); break; } case STATE_COLLAPSED: { int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED; replaceAccessibilityActionForState( - child, AccessibilityActionCompat.ACTION_EXPAND, nextState); + view, AccessibilityActionCompat.ACTION_EXPAND, nextState); break; } - default: // fall out + case STATE_HIDDEN: + case STATE_DRAGGING: + case STATE_SETTLING: + // Accessibility actions are not applicable, do nothing + } + } + + private void clearAccessibilityAction(View view, int viewIndex) { + if (view == null) { + return; + } + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_EXPAND); + ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_DISMISS); + + int expandHalfwayActionId = expandHalfwayActionIds.get(viewIndex, View.NO_ID); + if (expandHalfwayActionId != View.NO_ID) { + ViewCompat.removeAccessibilityAction(view, expandHalfwayActionId); + expandHalfwayActionIds.delete(viewIndex); } } private void replaceAccessibilityActionForState( - V child, AccessibilityActionCompat action, @State int state) { + View child, AccessibilityActionCompat action, @State int state) { ViewCompat.replaceAccessibilityAction( child, action, null, createAccessibilityViewCommandForState(state)); } private int addAccessibilityActionForState( - V child, @StringRes int stringResId, @State int state) { + View child, @StringRes int stringResId, @State int state) { return ViewCompat.addAccessibilityAction( child, child.getResources().getString(stringResId), @@ -2216,4 +2439,3 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo }; } } - From 2adb34dffed4badbc577b6283b9ac11e3ce46025 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 17:30:56 -0600 Subject: [PATCH 12/18] music: prevent deadlock on no-op refreshes Fix a regression where the loading process will never stop on a no-op refresh operation. This was an oversight made in the redocumentation of the loading process. --- .../java/org/oxycblt/auxio/music/MusicRepository.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 233872447..6e45c5ae9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -468,7 +468,7 @@ constructor( // Note that DeviceLibrary might still actually be doing work (specifically parent // processing), so we don't check if it's deadlocked. check(!mediaStoreJob.isActive) { "MediaStore discovery is deadlocked" } - check(!tagJob.isActive) { "Tag extraction is deadlocked" } + check(!tagJob.isActive) { "Tag extraction is deadlocked" } // Deliberately done after the involved initialization step to make it less likely // that the short-circuit occurs so quickly as to break the UI. @@ -519,6 +519,12 @@ constructor( logD("Starting UserLibrary creation") val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) + // Loading process is functionally done, indicate such + logD( + "Successfully indexed music library [device=$deviceLibrary " + + "user=$userLibrary time=${System.currentTimeMillis() - start}]") + emitIndexingCompletion(null) + val deviceLibraryChanged: Boolean val userLibraryChanged: Boolean // We want to make sure that all reads and writes are synchronized due to the sheer @@ -538,11 +544,6 @@ constructor( this.userLibrary = userLibrary } - // We are finally done. Indicate that loading is no longer occurring, and dispatch the - // results of the loading process to consumers. - logD("Successfully indexed music library [device=$deviceLibrary " + - "user=$userLibrary time=${System.currentTimeMillis() - start}]") - emitIndexingCompletion(null) // Consumers expect their updates to be on the main thread (notably PlaybackService), // so switch to it. withContext(Dispatchers.Main) { From 5bda85fe3632ecc2e9ade4cafcec69f91d019d76 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 24 Jun 2023 17:45:36 -0600 Subject: [PATCH 13/18] all: minor cleanup --- .../main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt | 2 ++ .../main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt | 2 +- app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt | 1 + app/src/main/res/values/dimens.xml | 1 - app/src/main/res/values/styles_ui.xml | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) 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 9cb4d70b5..d2a614be7 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 @@ -39,6 +39,7 @@ 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.playback.replaygain.ReplayGainAdjustment +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -217,6 +218,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son } check(_genres.isNotEmpty()) { "Malformed song: No genres" } + logD("$this $rawGenres $_genres]") for (i in _genres.indices) { // Non-destructively reorder the linked genres so that they align with // the genre ordering within the song metadata. 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 42e29eed2..11a357f45 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 @@ -364,7 +364,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet arrayOf( MediaStore.Audio.AudioColumns.TRACK, // Below API 29, we are restricted to the absolute path (Called DATA by - // MedaStore) when working with audio files. + // MediaStore) when working with audio files. MediaStore.Audio.AudioColumns.DATA) // The selector should be configured to convert the given directories instances to their 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 bb1dda066..5e0e7ca55 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 @@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.logE * @author Alexander Capehart * * TODO: Communicate errors + * TODO: How to handle empty playlists that appear because all of their songs have disappeared? */ interface UserLibrary { /** The current user-defined playlists. */ diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7dffd1917..5295acb05 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -20,7 +20,6 @@ 16dp 24dp - 48dp 56dp 64dp diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 97611ce8d..5ee16b401 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -216,7 +216,7 @@ @dimen/spacing_small @dimen/spacing_small @dimen/spacing_small - + @color/m3_text_button_foreground_color_selector From 6f6a3d8d31ea97c2e3e10a69c51758a46b10dfdf Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 25 Jun 2023 16:46:21 -0600 Subject: [PATCH 14/18] music: handle duplicate artist/genre values Remove functionally duplicate artist/genre values that were read from a file. This caused a indexer crash in 3.1.2 due to the switch to music sets, which no longer made duplicate values group the song twice. This then cascaded to a failure in song finalization, as it expects there to be the same amount of artists/genres as raw artists/genres. --- .../org/oxycblt/auxio/music/device/DeviceMusicImpl.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 d2a614be7..9654cb187 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 @@ -152,6 +152,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son rawArtists = rawAlbumArtists .ifEmpty { rawIndividualArtists } + .distinctBy { it.key } .ifEmpty { listOf(RawArtist(null, null)) }) /** @@ -160,7 +161,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son * [RawArtist]. This can be used to group up [Song]s into an [Artist]. */ val rawArtists = - rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) } + rawIndividualArtists + .ifEmpty { rawAlbumArtists } + .distinctBy { it.key } + .ifEmpty { listOf(RawArtist()) } /** * The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a @@ -170,6 +174,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son rawSong.genreNames .parseId3GenreNames(musicSettings) .map { RawGenre(it) } + .distinctBy { it.key } .ifEmpty { listOf(RawGenre()) } /** @@ -208,6 +213,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son checkNotNull(_album) { "Malformed song: No album" } check(_artists.isNotEmpty()) { "Malformed song: No artists" } + check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. @@ -218,7 +224,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son } check(_genres.isNotEmpty()) { "Malformed song: No genres" } - logD("$this $rawGenres $_genres]") + check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch"} for (i in _genres.indices) { // Non-destructively reorder the linked genres so that they align with // the genre ordering within the song metadata. @@ -336,6 +342,7 @@ class AlbumImpl( fun finalize(): Album { check(songs.isNotEmpty()) { "Malformed album: Empty" } check(_artists.isNotEmpty()) { "Malformed album: No artists" } + check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. From a1ab6d139a675a878847f6188b85ffd8fb53c097 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 25 Jun 2023 16:50:01 -0600 Subject: [PATCH 15/18] ui: increase shuffle on icon thickness Aggressively increase the shuffle icon thickness when it's indicated as on. Should allow the on state to be discerned easier. --- app/src/main/res/drawable/ic_shuffle_off_24.xml | 12 ++++++------ app/src/main/res/drawable/ic_shuffle_on_24.xml | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/drawable/ic_shuffle_off_24.xml b/app/src/main/res/drawable/ic_shuffle_off_24.xml index ec36a047c..d73a3c489 100644 --- a/app/src/main/res/drawable/ic_shuffle_off_24.xml +++ b/app/src/main/res/drawable/ic_shuffle_off_24.xml @@ -2,10 +2,10 @@ - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + diff --git a/app/src/main/res/drawable/ic_shuffle_on_24.xml b/app/src/main/res/drawable/ic_shuffle_on_24.xml index 8a64e052f..1e4330f55 100644 --- a/app/src/main/res/drawable/ic_shuffle_on_24.xml +++ b/app/src/main/res/drawable/ic_shuffle_on_24.xml @@ -1,10 +1,11 @@ + - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + From f8a0a42dd3a9c01ad927b61dd7fb457394e8dd02 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 26 Jun 2023 17:56:48 -0600 Subject: [PATCH 16/18] music: finalize duplicate tags fix Officially finalize the fix for #484, at least I think. Resolves #484. --- CHANGELOG.md | 1 + .../java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3575b7f..219bd405b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### What's Improved - `album artists` and `(album)artists sort` are now recognized +- Increased distinction from shuffle on/off icons #### What's Fixed - Fixed an issue where the queue sheet would not collapse when scrolling 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 9654cb187..354f6520f 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 @@ -39,7 +39,6 @@ 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.playback.replaygain.ReplayGainAdjustment -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -224,7 +223,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son } check(_genres.isNotEmpty()) { "Malformed song: No genres" } - check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch"} + check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch" } for (i in _genres.indices) { // Non-destructively reorder the linked genres so that they align with // the genre ordering within the song metadata. @@ -342,7 +341,7 @@ class AlbumImpl( fun finalize(): Album { check(songs.isNotEmpty()) { "Malformed album: Empty" } check(_artists.isNotEmpty()) { "Malformed album: No artists" } - check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" } + check(_artists.size == rawArtists.size) { "Malformed album: Artist grouping mismatch" } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. From 4840742c4ee707da993f58f655537cefa38cf11a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 26 Jun 2023 18:01:35 -0600 Subject: [PATCH 17/18] build: bump to 3.1.3 Bump to version 3.1.3 (33). --- CHANGELOG.md | 2 +- README.md | 4 ++-- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/33.txt | 4 ++++ 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 219bd405b..2063c3684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## dev +## 3.1.3 #### What's New - Updated to Android 14 diff --git a/README.md b/README.md index ccb7bcb0d..79a600389 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version Releases diff --git a/app/build.gradle b/app/build.gradle index 09c4dba6d..0a14a0b74 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { defaultConfig { applicationId namespace - versionName "3.1.2" - versionCode 32 + versionName "3.1.3" + versionCode 33 minSdk 24 targetSdk 34 diff --git a/fastlane/metadata/android/en-US/changelogs/33.txt b/fastlane/metadata/android/en-US/changelogs/33.txt new file mode 100644 index 000000000..5915176d0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33.txt @@ -0,0 +1,4 @@ +Auxio 3.1.0 introduces playlisting functionality, with more features coming soon. +This release fixes an issue where some users would experience an infinite loading +screen, along other quality-of-life improvements. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.3. \ No newline at end of file From d1e29d5caecf1dca70543465ab267580cc15e918 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 27 Jun 2023 02:08:27 +0200 Subject: [PATCH 18/18] Translations update from Hosted Weblate (#481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added translation using Weblate (Swedish) * Translated using Weblate (Swedish) Currently translated at 97.0% (33 of 34 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/sv/ * Translated using Weblate (Swedish) Currently translated at 35.2% (98 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/sv/ * Translated using Weblate (Swedish) Currently translated at 46.7% (130 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/sv/ * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/zh_Hans/ * Translated using Weblate (Czech) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cs/ * Translated using Weblate (German) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/es/ * Translated using Weblate (Polish) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pl/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ * Translated using Weblate (Punjabi) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pa/ * Translated using Weblate (French) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ru/ * Translated using Weblate (Croatian) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hr/ * Translated using Weblate (Belarusian) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/be/ * Translated using Weblate (Polish) Currently translated at 100.0% (280 of 280 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pl/ --------- Co-authored-by: sköldpadda Co-authored-by: Eric Co-authored-by: Fjuro Co-authored-by: qwerty287 Co-authored-by: gallegonovato Co-authored-by: Maciej Klupp Co-authored-by: BMT[UA] Co-authored-by: ShareASmile Co-authored-by: J. Lavoie Co-authored-by: Макар Разин Co-authored-by: Milo Ivir --- app/src/main/res/values-be/strings.xml | 2 + app/src/main/res/values-cs/strings.xml | 8 +- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-hr/strings.xml | 2 + app/src/main/res/values-pa/strings.xml | 6 +- app/src/main/res/values-pl/strings.xml | 28 ++-- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-sv/strings.xml | 156 ++++++++++++++++++ app/src/main/res/values-uk/strings.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 2 + .../metadata/android/sv/short_description.txt | 1 + 13 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 fastlane/metadata/android/sv/short_description.txt diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 561b05892..ed22aa75f 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -293,4 +293,6 @@ Няма дыска З\'яўляецца на Рэдагаванне %s + Выкарыстоўваць квадратныя вокладкі альбомаў + Абрэзаць усе вокладкі альбомаў да суадносін бакоў 1:1 \ 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 956f74699..6abd46909 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -55,9 +55,9 @@ Nastavení Vzhled a chování Motiv - Automatické - Světlé - Tmavé + Automaticky + Světlý + Tmavý Barevné schéma Černý motiv Použít kompletně černý tmavý motiv @@ -304,4 +304,6 @@ Žádný disk Úprava seznamu %s Sdílet + Vynutit čtvercové obaly alb + Oříznout všechny covery alb na poměr stran 1:1 \ 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 03ca85350..2f944921a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -295,4 +295,6 @@ Keine Disc Erscheint in %s bearbeiten + Quadratische Album-Cover erzwingen + Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden \ 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 3a7e94380..9fd873733 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -299,4 +299,6 @@ Aparece en Compartir Sin disco + Carátula del álbum Force Square + Recorta todas las portadas de los álbumes a una relación de aspecto 1:1 \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 55adf537f..ce20da39c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -297,4 +297,6 @@ Codage audio avancé (AAC) Aucun disque %1$s, %2$s + Forcer les pochettes d\'album carrées + Recadrer toutes les pochettes d\'album au format 1:1 \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 7382cab1d..4809c780e 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -290,4 +290,6 @@ Sudjelovanja: Dijeli Nema diska + Prisili kvadratične omote albuma + Odreži sve omote albuma na omjer 1:1 \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index f51c29ee1..592ef9dc7 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -180,7 +180,7 @@ ਕੌਮਾ (,) ਸੈਮੀਕੋਲਨ (;) ਸਲੈਸ਼ (/) - Ampersand (&) + ਐਂਪਰਸੈਂਡ (&) ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ ਪਲੇਅਬੈਕ @@ -219,7 +219,7 @@ Matroska ਆਡੀਓ ਗੂੜ੍ਹਾ ਜ੍ਹਾਮਣੀ Ogg ਆਡੀਓ - % d: ਗੀਤ ਲੋਡ ਕੀਤੇ + %d: ਗੀਤ ਲੋਡ ਕੀਤੇ %d ਐਲਬਮ %d ਐਲਬਮਾਂ @@ -287,4 +287,6 @@ ਡਾਇਨੈਮਿਕ %s ਸੋਧ ਰਿਹਾ ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d) + ਵਰਗੀਕ੍ਰਿਤ ਐਲਬਮ ਕਵਰ ਫੋਰਸ ਕਰੋ + ਸਾਰੇ ਐਲਬਮ ਕਵਰਾਂ ਨੂੰ 1:1 ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਤੱਕ ਕਾਂਟ-ਛਾਂਟ ਕਰੋ \ 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 8016b72f2..d433b61c4 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -40,7 +40,7 @@ Nie znaleziono utworów Utwór %d - Odtwórz bądź zapauzuj + Odtwórz albo zapauzuj Szukaj w bibliotece… @@ -102,7 +102,7 @@ Minialbum koncertowy Minialbum z remiksami Koncertowy singiel - Remiks + Remix Kompilacje Kompilacja Ścieżki dźwiękowe @@ -139,7 +139,7 @@ Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne Usuń utwór z kolejki Preferuj album - Automatycznie odśwież + Automatyczne odświeżanie FLAC Et (&) Nie udało się zaimportować utworów @@ -243,7 +243,7 @@ Wyłączone Niska jakość Wysoka jakość - Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów - tak, jakby posiadały wiele wartości. Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami traktowanymi jako oddzielające. + Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów (tak, jakby posiadały wiele wartości). Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami oddzielającymi. Dostosuj motyw i kolory aplikacji Ukryj wykonawców uczestniczących Zarządzaj importowaniem muzyki i obrazów @@ -254,7 +254,7 @@ Foldery Stan odtwarzania Obrazy - Zarządzanie dźwiękiem i odtwarzaniem muzyki + Zarządzaj dźwiękiem i odtwarzaniem muzyki Odtwórz wybrane Wybrane losowo Wybrano %d @@ -288,16 +288,18 @@ Brak utworów Dodano do playlisty Playlista %d - Usuwać + Usuń Usunąć %s\? Tego nie da się cofnąć. - Przemianować - Przemianować playlistę + Zmień nazwę + Zmień nazwę playlisty Usunąć playlistę\? - Edytować - Pojawia się - Udział + Edytuj + Pojawia się na + Udostępnij Zmieniono nazwę playlisty - Playlista usunięta - Brak dysku + Usunięto playlistę + Brak nr. płyty Edytowanie %s + Przytnij okładki do formatu 1:1 + Wymuś kwadratowe okładki \ 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 6091d3ea4..43daae2e2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -302,4 +302,6 @@ Редактирование %s Нет диска Поделиться + Использовать квадратные обложки альбомов + Обрезать все обложки альбомов до соотношения сторон 1:1 \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..7acaab1b1 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,156 @@ + + + Försök igen + Musik laddar + Laddar musik + Alla låtar + Album + Albumet + Remix-album + EP + EP + Live-EP + Remix-EP + Singlar + Remix-singel + Sammanställning + Remix-sammanställning + Ljudspår + Ljudspår + Blandband + DJ-mixar + Live + Remixar + Framträder på + Konstnär + Konstnär + Genrer + Spellista + Spellistor + Ny spellista + Byt namn på spellista + Ta bort spellista\? + Sök + Filtrera + Namn + Datum + Längd + Antal låtar + Spår + Datum tillagt + Stigande + Fallande + Nu spelar + Utjämnare + Spela + Spela utvalda + Blanda + + Spela nästa + Lägg till spellista + Gå till konstnär + Gå till album + Visa egenskaper + Dela + Egenskaper för låt + Överordnad mapp + Format + Storlek + Samplingsfrekvens + Blanda + Blanda alla + Okej + Avbryt + Spara + Tillstånd återstallde + Om + Källkod + Wiki + Licenser + Visa och kontrollera musikuppspelning + Laddar ditt musikbibliotek… + Övervakning ditt musikbibliotek för ändringar… + Tillagd till kö + Spellista skapade + Tillagd till spellista + Sök ditt musikbibliotek… + Inställningar + Utseende + Ändra tema och färger på appen + Automatisk + Ljust + Svart tema + Rundläge + Bevilja + En enkel, rationell musikspelare för Android. + Övervakar musikbiblioteket + Låtar + Live-album + Ta bort + Live-sammanställning + Singel + Live-singel + Sammanställningar + Blandband + DJ-mix + Genre + Byt namn + Redigera + Alla + Disk + Sortera + Blanda utvalda + Lägg till kö + Filnamn + Lägg till + Tillstånd tog bort + Bithastighet + Återställ + Tillstånd sparat + Version + Statistik över beroende + Bytt namn av spellista + Spellista tog bort + Utvecklad av Alexander Capeheart + Tema + Mörkt + Färgschema + Använda rent svart för det mörka temat + Aktivera rundade hörn på ytterligare element i användargränssnittet (kräver att albumomslag är rundade) + Anpassa + Ändra synlighet och ordningsföljd av bibliotekflikar + Anpassad åtgärd för uppspelningsfält + Anpassad aviseringsåtgärd + Hoppa till nästa + Upprepningsmodus + Beteende + När spelar från artikeluppgifter + Spela från genre + Komma ihåg blandningsstatus + Behåll blandning på när spelar en ny låt + Kontent + Kontrollera hur musik och bilar laddas + Musik + Automatisk omladdning + Inkludera bara musik + Ignorera ljudfiler som inte är musik, t.ex. podkaster + Värdeavskiljare + Plus (+) + Intelligent sortering + Sorterar namn som börjar med siffror eller ord som \"the\" korrekt (fungerar bäst med engelskspråkig music) + Dölj medarbetare + Skärm + Bibliotekflikar + När spelar från biblioteket + Spela från visad artikel + Spela från alla låtar + Spela från konstnär + Spela från album + Semikolon (;) + Ladda om musikbiblioteket när det ändras (kräver permanent meddelande) + Komma (,) + Snedstreck (/) + Konfigurera tecken som separerar flera värden i taggar + Advarsel: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\). + Anpassa UI-kontroller och beteende + \ 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 a9a5c1e6a..8f7435242 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -299,4 +299,6 @@ Редагування %s Немає диску З\'являється на + Обрізання обкладинки альбомів до співвідношення сторін 1:1 + Примусові квадратні обкладинки \ 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 52243b67a..4a5290943 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -293,4 +293,6 @@ 分享 无唱片 正在编辑 %s + 强制使用方形专辑封面 + 将所有专辑封面裁剪至 1:1 宽高比 \ No newline at end of file diff --git a/fastlane/metadata/android/sv/short_description.txt b/fastlane/metadata/android/sv/short_description.txt new file mode 100644 index 000000000..107ba2121 --- /dev/null +++ b/fastlane/metadata/android/sv/short_description.txt @@ -0,0 +1 @@ +En enkel, rationell musikspelare