Merge pull request #491 from OxygenCobalt/dev

Version 3.1.3
This commit is contained in:
Alexander Capehart 2023-06-26 18:48:32 -06:00 committed by GitHub
commit e7d391b050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 885 additions and 702 deletions

View file

@ -1,5 +1,20 @@
# Changelog # Changelog
## 3.1.3
#### What's New
- 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
- Increased distinction from shuffle on/off icons
#### 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 ## 3.1.2
#### What's Improved #### What's Improved

View file

@ -2,8 +2,8 @@
<h1 align="center"><b>Auxio</b></h1> <h1 align="center"><b>Auxio</b></h1>
<h4 align="center">A simple, rational music player for android.</h4> <h4 align="center">A simple, rational music player for android.</h4>
<p align="center"> <p align="center">
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.2"> <a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.3">
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.2&color=64B5F6&style=flat"> <img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.3&color=64B5F6&style=flat">
</a> </a>
<a href="https://github.com/oxygencobalt/Auxio/releases/"> <a href="https://github.com/oxygencobalt/Auxio/releases/">
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat"> <img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">

View file

@ -10,7 +10,7 @@ plugins {
} }
android { android {
compileSdk 33 compileSdk 34
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify // 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. // it here so that binary stripping will work.
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the // TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
@ -20,11 +20,11 @@ android {
defaultConfig { defaultConfig {
applicationId namespace applicationId namespace
versionName "3.1.2" versionName "3.1.3"
versionCode 32 versionCode 33
minSdk 24 minSdk 24
targetSdk 33 targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -84,16 +84,19 @@ dependencies {
// --- SUPPORT --- // --- SUPPORT ---
// General // General
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.core:core-ktx:1.10.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.activity:activity-ktx:1.7.2"
implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.fragment:fragment-ktx:1.6.0"
// UI // Components
implementation "androidx.recyclerview:recyclerview:1.3.0" // 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.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'androidx.core:core-ktx:1.10.1'
// Lifecycle // Lifecycle
def lifecycle_version = "2.6.1" def lifecycle_version = "2.6.1"
@ -128,11 +131,9 @@ dependencies {
implementation 'io.coil-kt:coil-base:2.4.0' implementation 'io.coil-kt:coil-base:2.4.0'
// Material // 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 // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just
// PR a fix. // 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 // Dependency Injection
implementation "com.google.dagger:dagger:$hilt_version" implementation "com.google.dagger:dagger:$hilt_version"

View file

@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Bluetooth auto-connect functionality (Disabled until permission workflow can be made) --> <!-- Bluetooth auto-connect functionality (Disabled until permission workflow can be made) -->

View file

@ -16,10 +16,14 @@
package com.google.android.material.bottomsheet; package com.google.android.material.bottomsheet;
import com.google.android.material.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener; import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context; import android.content.Context;
@ -32,8 +36,10 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.SparseIntArray;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.RoundedCorner;
import android.view.VelocityTracker; import android.view.VelocityTracker;
import android.view.View; import android.view.View;
import android.view.View.MeasureSpec; import android.view.View.MeasureSpec;
@ -41,13 +47,15 @@ import android.view.ViewConfiguration;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent; import android.view.ViewParent;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import androidx.activity.BackEventCompat;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Px; import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -62,14 +70,13 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
import androidx.core.view.accessibility.AccessibilityViewCommand; import androidx.core.view.accessibility.AccessibilityViewCommand;
import androidx.customview.view.AbsSavedState; import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper; 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;
import com.google.android.material.internal.ViewUtils.RelativePadding; 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.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.ShapeAppearanceModel;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -84,13 +91,11 @@ import java.util.Map;
* <p>To send useful accessibility events, set a title on bottom sheets that are windows or are * <p>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 * window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. * 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<V extends View> extends CoordinatorLayout.Behavior<V> { public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>
implements MaterialBackHandler {
/** Listener for monitoring events about bottom sheets. */ /** Callback for monitoring events about bottom sheets. */
public abstract static class BottomSheetCallback { public abstract static class BottomSheetCallback {
/** /**
@ -203,11 +208,11 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface SaveFlags {} public @interface SaveFlags {}
private static final String TAG = "BottomSheetBehavior"; private static final String TAG = "BackportBottomSheetBehavior";
@SaveFlags private int saveFlags = SAVE_NONE; @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; private static final float HIDE_THRESHOLD = 0.5f;
@ -217,12 +222,21 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
private static final int NO_MAX_SIZE = -1; 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 fitToContents = true;
private boolean updateImportantForAccessibilityOnSiblings = false; private boolean updateImportantForAccessibilityOnSiblings = false;
private float maximumVelocity; private float maximumVelocity;
private int significantVelocityThreshold;
/** Peek height set by the user. */ /** Peek height set by the user. */
private int peekHeight; private int peekHeight;
@ -256,10 +270,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
private int insetBottom; private int insetBottom;
private int insetTop; private int insetTop;
private boolean shouldRemoveExpandedCorners;
/** Default Shape Appearance to be used in bottomsheet */ /** Default Shape Appearance to be used in bottomsheet */
private ShapeAppearanceModel shapeAppearanceModelDefault; private ShapeAppearanceModel shapeAppearanceModelDefault;
private boolean isShapeExpanded; private boolean expandedCornersRemoved;
private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker(); private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker();
@ -304,22 +320,25 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
int parentHeight; int parentHeight;
@Nullable WeakReference<V> viewRef; @Nullable WeakReference<V> viewRef;
@Nullable WeakReference<View> accessibilityDelegateViewRef;
@Nullable WeakReference<View> nestedScrollingChildRef; @Nullable WeakReference<View> nestedScrollingChildRef;
@NonNull private final ArrayList<BottomSheetCallback> callbacks = new ArrayList<>(); @NonNull private final ArrayList<BottomSheetCallback> callbacks = new ArrayList<>();
@Nullable private VelocityTracker velocityTracker; @Nullable private VelocityTracker velocityTracker;
@Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper;
int activePointerId; int activePointerId;
private int initialY; private int initialY = INVALID_POSITION;
boolean touchingScrollingChild; boolean touchingScrollingChild;
@Nullable private Map<View, Integer> importantForAccessibilityMap; @Nullable private Map<View, Integer> importantForAccessibilityMap;
private int expandHalfwayActionId = View.NO_ID; @VisibleForTesting
final SparseIntArray expandHalfwayActionIds = new SparseIntArray();
public BackportBottomSheetBehavior() {} public BackportBottomSheetBehavior() {}
@ -387,6 +406,11 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0)); 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. // Reading out if we are handling padding, so we can apply it to the content.
paddingBottomSystemWindowInsets = paddingBottomSystemWindowInsets =
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false); a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false);
@ -404,6 +428,8 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false); a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false);
marginTopSystemWindowInsets = marginTopSystemWindowInsets =
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false); a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false);
shouldRemoveExpandedCorners =
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_shouldRemoveExpandedCorners, true);
a.recycle(); a.recycle();
ViewConfiguration configuration = ViewConfiguration.get(context); ViewConfiguration configuration = ViewConfiguration.get(context);
@ -440,6 +466,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
// first time we layout with this behavior by checking (viewRef == null). // first time we layout with this behavior by checking (viewRef == null).
viewRef = null; viewRef = null;
viewDragHelper = null; viewDragHelper = null;
bottomContainerBackHelper = null;
} }
@Override @Override
@ -448,6 +475,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
// Release references so we don't run unnecessary codepaths while not attached to a view. // Release references so we don't run unnecessary codepaths while not attached to a view.
viewRef = null; viewRef = null;
viewDragHelper = null; viewDragHelper = null;
bottomContainerBackHelper = null;
} }
@Override @Override
@ -515,7 +543,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
peekHeightMin = peekHeightMin =
parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min); parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min);
setWindowInsetsListener(child); setWindowInsetsListener(child);
ViewCompat.setWindowInsetsAnimationCallback(child, new InsetsAnimationCallback(child));
viewRef = new WeakReference<>(child); viewRef = new WeakReference<>(child);
bottomContainerBackHelper = new MaterialBottomContainerBackHelper(child);
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
// default to android:background declared in styles or layout. // default to android:background declared in styles or layout.
if (materialShapeDrawable != null) { if (materialShapeDrawable != null) {
@ -523,9 +553,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
// Use elevation attr if set on bottomsheet; otherwise, use elevation of child view. // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view.
materialShapeDrawable.setElevation( materialShapeDrawable.setElevation(
elevation == -1 ? ViewCompat.getElevation(child) : elevation); 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) { } else if (backgroundTint != null) {
ViewCompat.setBackgroundTintList(child, backgroundTint); ViewCompat.setBackgroundTintList(child, backgroundTint);
} }
@ -549,11 +576,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
if (parentHeight - childHeight < insetTop) { if (parentHeight - childHeight < insetTop) {
if (paddingTopSystemWindowInsets) { if (paddingTopSystemWindowInsets) {
// If the bottomsheet would land in the middle of the status bar when fully expanded add // 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. // extra space to make sure it goes all the way up or up to max height if it is specified.
childHeight = parentHeight; childHeight = (maxHeight == NO_MAX_SIZE) ? parentHeight : min(parentHeight, maxHeight);
} else { } else {
// If we don't want the bottomsheet to go under the status bar we cap its height // 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); fitToContentsOffset = max(0, parentHeight - childHeight);
@ -571,6 +599,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) { } else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
} }
updateDrawableForTargetState(state, /* animate= */ false);
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
@ -640,6 +669,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
&& state != STATE_DRAGGING && state != STATE_DRAGGING
&& !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY())
&& viewDragHelper != null && viewDragHelper != null
&& initialY != INVALID_POSITION
&& Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop(); && Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop();
} }
@ -723,8 +753,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
} }
} else if (dy < 0) { // Downward } else if (dy < 0) { // Downward
if (!target.canScrollVertically(-1)) { if (!target.canScrollVertically(-1)) {
// MODIFICATION: Add isHideableWhenDragging method if (newTop <= collapsedOffset || canBeHiddenByDragging()) {
if (newTop <= collapsedOffset || (hideable && isHideableWhenDragging())) {
if (!draggable) { if (!draggable) {
// Prevent dragging // Prevent dragging
return; return;
@ -778,7 +807,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
} }
} }
} }
// MODIFICATION: Add isHideableWhenDragging method
} else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) { } else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) {
targetState = STATE_HIDDEN; targetState = STATE_HIDDEN;
} else if (lastNestedScrollDy == 0) { } else if (lastNestedScrollDy == 0) {
@ -888,6 +916,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
// Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents. // 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); setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state);
updateDrawableForTargetState(state, /* animate= */ true);
updateAccessibilityActions(); updateAccessibilityActions();
} }
@ -897,7 +926,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* be adjusted as expected. * be adjusted as expected.
* *
* @param maxWidth The maximum width in pixels to be set * @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() * @see #getMaxWidth()
*/ */
public void setMaxWidth(@Px int maxWidth) { public void setMaxWidth(@Px int maxWidth) {
@ -907,7 +936,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
/** /**
* Returns the bottom sheet's maximum width, or -1 if no maximum width is set. * 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) * @see #setMaxWidth(int)
*/ */
@Px @Px
@ -920,7 +949,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* BottomSheetDialog#show()} in order for the height to be adjusted as expected. * BottomSheetDialog#show()} in order for the height to be adjusted as expected.
* *
* @param maxHeight The maximum height in pixels to be set * @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() * @see #getMaxHeight()
*/ */
public void setMaxHeight(@Px int maxHeight) { public void setMaxHeight(@Px int maxHeight) {
@ -930,7 +959,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
/** /**
* Returns the bottom sheet's maximum height, or -1 if no maximum height is set. * 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) * @see #setMaxHeight(int)
*/ */
@Px @Px
@ -944,7 +973,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link * @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. * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
* @attr ref * @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) { public void setPeekHeight(int peekHeight) {
setPeekHeight(peekHeight, false); setPeekHeight(peekHeight, false);
@ -958,7 +987,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * #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. * @param animate Whether to animate between the old height and the new height.
* @attr ref * @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) { public final void setPeekHeight(int peekHeight, boolean animate) {
boolean layout = false; boolean layout = false;
@ -1001,7 +1030,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the * @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 * sheet is configured to peek automatically at 16:9 ratio keyline
* @attr ref * @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() { public int getPeekHeight() {
return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight; return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight;
@ -1015,7 +1044,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* *
* @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio. * @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio.
* @attr ref * @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( public void setHalfExpandedRatio(
@FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) { @FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) {
@ -1035,7 +1064,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state. * Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state.
* *
* @attr ref * @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) @FloatRange(from = 0.0f, to = 1.0f)
public float getHalfExpandedRatio() { public float getHalfExpandedRatio() {
@ -1050,13 +1079,14 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* @param offset an integer value greater than equal to 0, representing the {@link * @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. * #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state.
* @attr ref * @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) { public void setExpandedOffset(int offset) {
if (offset < 0) { if (offset < 0) {
throw new IllegalArgumentException("offset must be greater than or equal to 0"); throw new IllegalArgumentException("offset must be greater than or equal to 0");
} }
this.expandedOffset = offset; this.expandedOffset = offset;
updateDrawableForTargetState(state, /* animate= */ true);
} }
/** /**
@ -1064,7 +1094,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* pick the offset depending on the height of the content. * pick the offset depending on the height of the content.
* *
* @attr ref * @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() { public int getExpandedOffset() {
return fitToContents return fitToContents
@ -1072,8 +1102,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
: Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop); : Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop);
} }
// MODIFICATION: Add calculateSlideOffset method
/** /**
* Calculates the current offset of the bottom sheet. * Calculates the current offset of the bottom sheet.
* *
@ -1082,26 +1110,21 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* @return The offset of this bottom sheet within [-1,1] range. Offset increases * @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 * 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 * 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() { public float calculateSlideOffset() {
if (viewRef == null) { if (viewRef == null || viewRef.get() == null) {
return Float.MIN_VALUE; return -1;
} }
View bottomSheet = viewRef.get(); return calculateSlideOffsetWithTop(viewRef.get().getTop());
if (bottomSheet != null) {
return calculateSlideOffset(bottomSheet.getTop());
}
return Float.MIN_VALUE;
} }
/** /**
* 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. * @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) { public void setHideable(boolean hideable) {
if (this.hideable != hideable) { if (this.hideable != hideable) {
@ -1118,7 +1141,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* Gets whether this bottom sheet can hide when it is swiped down. * Gets whether this bottom sheet can hide when it is swiped down.
* *
* @return {@code true} if this bottom sheet can hide. * @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() { public boolean isHideable() {
return hideable; return hideable;
@ -1130,7 +1153,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* *
* @param skipCollapsed True if the bottom sheet should skip the collapsed state. * @param skipCollapsed True if the bottom sheet should skip the collapsed state.
* @attr ref * @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) { public void setSkipCollapsed(boolean skipCollapsed) {
this.skipCollapsed = skipCollapsed; this.skipCollapsed = skipCollapsed;
@ -1142,7 +1165,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* *
* @return Whether the bottom sheet should skip the collapsed state. * @return Whether the bottom sheet should skip the collapsed state.
* @attr ref * @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() { public boolean getSkipCollapsed() {
return skipCollapsed; return skipCollapsed;
@ -1153,7 +1176,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* dragging, an app will require to implement a custom way to expand/collapse the bottom sheet * 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 * @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) { public void setDraggable(boolean draggable) {
this.draggable = draggable; this.draggable = draggable;
@ -1163,13 +1186,35 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
return draggable; 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. * 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 * @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}. * #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}.
* @see #getSaveFlags() * @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) { public void setSaveFlags(@SaveFlags int flags) {
this.saveFlags = flags; this.saveFlags = flags;
@ -1178,7 +1223,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
* Returns the save flags. * Returns the save flags.
* *
* @see #setSaveFlags(int) * @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 @SaveFlags
public int getSaveFlags() { public int getSaveFlags() {
@ -1208,9 +1253,9 @@ public class BackportBottomSheetBehavior<V extends View> 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 * @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link
* #removeBottomSheetCallback(BottomSheetCallback)} instead * #removeBottomSheetCallback(BottomSheetCallback)} instead
*/ */
@ -1218,7 +1263,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
public void setBottomSheetCallback(BottomSheetCallback callback) { public void setBottomSheetCallback(BottomSheetCallback callback) {
Log.w( Log.w(
TAG, 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" + " all existing callbacks, including ones set internally by library authors, which"
+ " may result in unintended behavior. This may change in the future. Please use" + " may result in unintended behavior. This may change in the future. Please use"
+ " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your" + " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your"
@ -1230,9 +1275,9 @@ public class BackportBottomSheetBehavior<V extends View> 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) { public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
@ -1241,9 +1286,9 @@ public class BackportBottomSheetBehavior<V extends View> 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) { public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) {
callbacks.remove(callback); callbacks.remove(callback);
@ -1325,6 +1370,26 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
return gestureInsetBottomIgnored; return gestureInsetBottomIgnored;
} }
/**
* Sets whether the bottom sheet should remove its corners when it reaches the expanded state.
*
* <p>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. * Gets the current state of the bottom sheet.
* *
@ -1376,33 +1441,91 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
updateImportantForAccessibility(false); updateImportantForAccessibility(false);
} }
updateDrawableForTargetState(state); updateDrawableForTargetState(state, /* animate= */ true);
for (int i = 0; i < callbacks.size(); i++) { for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onStateChanged(bottomSheet, state); callbacks.get(i).onStateChanged(bottomSheet, state);
} }
updateAccessibilityActions(); updateAccessibilityActions();
} }
private void updateDrawableForTargetState(@State int state) { private void updateDrawableForTargetState(@State int state, boolean animate) {
if (state == STATE_SETTLING) { if (state == STATE_SETTLING) {
// Special case: we want to know which state we're settling to, so wait for another call. // Special case: we want to know which state we're settling to, so wait for another call.
return; return;
} }
boolean expand = state == STATE_EXPANDED; boolean removeCorners = isExpandedAndShouldRemoveCorners();
if (isShapeExpanded != expand) { if (expandedCornersRemoved == removeCorners || materialShapeDrawable == null) {
isShapeExpanded = expand; return;
if (materialShapeDrawable != null && interpolatorAnimator != null) { }
expandedCornersRemoved = removeCorners;
if (animate && interpolatorAnimator != null) {
if (interpolatorAnimator.isRunning()) { if (interpolatorAnimator.isRunning()) {
interpolatorAnimator.reverse(); interpolatorAnimator.reverse();
} else { } else {
float to = expand ? 0f : 1f; float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f;
float from = 1f - to; float from = 1f - to;
interpolatorAnimator.setFloatValues(from, to); interpolatorAnimator.setFloatValues(from, to);
interpolatorAnimator.start(); 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() { private int calculatePeekHeight() {
@ -1432,9 +1555,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio)); this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio));
} }
// MODIFICATION: Add calculateSlideOffset method private float calculateSlideOffsetWithTop(int top) {
private float calculateSlideOffset(int top) {
return return
(top > collapsedOffset || collapsedOffset == getExpandedOffset()) (top > collapsedOffset || collapsedOffset == getExpandedOffset())
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset) ? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
@ -1443,6 +1564,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
private void reset() { private void reset() {
activePointerId = ViewDragHelper.INVALID_POINTER; activePointerId = ViewDragHelper.INVALID_POINTER;
initialY = INVALID_POSITION;
if (velocityTracker != null) { if (velocityTracker != null) {
velocityTracker.recycle(); velocityTracker.recycle();
velocityTracker = null; velocityTracker = null;
@ -1473,6 +1595,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
if (skipCollapsed) { if (skipCollapsed) {
return true; return true;
} }
if (!isHideableWhenDragging()) {
return false;
}
if (child.getTop() < collapsedOffset) { if (child.getTop() < collapsedOffset) {
// It should not hide, but collapse. // It should not hide, but collapse.
return false; return false;
@ -1482,9 +1607,73 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD; 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 @Nullable
@VisibleForTesting @VisibleForTesting
View findScrollingChild(View view) { View findScrollingChild(View view) {
if (view.getVisibility() != View.VISIBLE) {
return null;
}
if (ViewCompat.isNestedScrollingEnabled(view)) { if (ViewCompat.isNestedScrollingEnabled(view)) {
return view; return view;
} }
@ -1524,12 +1713,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
} }
} }
MaterialShapeDrawable getMaterialShapeDrawable() { protected MaterialShapeDrawable getMaterialShapeDrawable() {
return materialShapeDrawable; return materialShapeDrawable;
} }
private void createShapeValueAnimator() { private void createShapeValueAnimator() {
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f); interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f);
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION); interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION);
interpolatorAnimator.addUpdateListener( interpolatorAnimator.addUpdateListener(
new AnimatorUpdateListener() { new AnimatorUpdateListener() {
@ -1650,7 +1839,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
if (settling) { if (settling) {
setStateInternal(STATE_SETTLING); setStateInternal(STATE_SETTLING);
// STATE_SETTLING won't animate the material shape, so do that here with the target state. // 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); stateSettlingTracker.continueSettlingToState(state);
} else { } else {
setStateInternal(state); setStateInternal(state);
@ -1741,11 +1930,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
} }
} }
} }
// MODIFICATION: Add isHideableWhenDragging method } else if (hideable && shouldHide(releasedChild, yvel)) {
} else if (hideable && shouldHide(releasedChild, yvel) && isHideableWhenDragging()) {
// Hide if the view was either released low or it was a significant vertical swipe // Hide if the view was either released low or it was a significant vertical swipe
// otherwise settle to closest expanded state. // 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)) { || releasedLow(releasedChild)) {
targetState = STATE_HIDDEN; targetState = STATE_HIDDEN;
} else if (fitToContents) { } else if (fitToContents) {
@ -1814,9 +2002,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
@Override @Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) { public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// MODIFICATION: Add isHideableWhenDragging method
return MathUtils.clamp( return MathUtils.clamp(
top, getExpandedOffset(), (hideable && isHideableWhenDragging()) ? parentHeight : collapsedOffset); top,
getExpandedOffset(),
getViewVerticalDragRange(child));
} }
@Override @Override
@ -1826,8 +2015,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
@Override @Override
public int getViewVerticalDragRange(@NonNull View child) { public int getViewVerticalDragRange(@NonNull View child) {
// MODIFICATION: Add isHideableWhenDragging method if (canBeHiddenByDragging()) {
if (hideable && isHideableWhenDragging()) {
return parentHeight; return parentHeight;
} else { } else {
return collapsedOffset; return collapsedOffset;
@ -1838,8 +2026,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
void dispatchOnSlide(int top) { void dispatchOnSlide(int top) {
View bottomSheet = viewRef.get(); View bottomSheet = viewRef.get();
if (bottomSheet != null && !callbacks.isEmpty()) { if (bottomSheet != null && !callbacks.isEmpty()) {
// MODIFICATION: Add calculateSlideOffset method float slideOffset = calculateSlideOffsetWithTop(top);
float slideOffset = calculateSlideOffset(top);
for (int i = 0; i < callbacks.size(); i++) { for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onSlide(bottomSheet, slideOffset); callbacks.get(i).onSlide(bottomSheet, slideOffset);
} }
@ -1898,7 +2085,8 @@ public class BackportBottomSheetBehavior<V extends View> 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 * @hide
*/ */
@RestrictTo(LIBRARY_GROUP) @RestrictTo(LIBRARY_GROUP)
@ -1906,6 +2094,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
return true; return true;
} }
private boolean canBeHiddenByDragging() {
return isHideable() && isHideableWhenDragging();
}
/** /**
* Checks whether the bottom sheet should be expanded after it has been released after dragging. * Checks whether the bottom sheet should be expanded after it has been released after dragging.
* *
@ -2067,7 +2259,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
CoordinatorLayout.Behavior<?> behavior = CoordinatorLayout.Behavior<?> behavior =
((CoordinatorLayout.LayoutParams) params).getBehavior(); ((CoordinatorLayout.LayoutParams) params).getBehavior();
if (!(behavior instanceof BackportBottomSheetBehavior)) { 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<V>) behavior; return (BackportBottomSheetBehavior<V>) behavior;
} }
@ -2139,30 +2331,43 @@ public class BackportBottomSheetBehavior<V extends View> 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() { private void updateAccessibilityActions() {
if (viewRef == null) { if (viewRef != null) {
return; updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET);
}
if (accessibilityDelegateViewRef != null) {
updateAccessibilityActions(
accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
} }
V child = viewRef.get();
if (child == null) {
return;
} }
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND);
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS);
if (expandHalfwayActionId != View.NO_ID) { private void updateAccessibilityActions(View view, int viewIndex) {
ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId); if (view == null) {
return;
} }
clearAccessibilityAction(view, viewIndex);
if (!fitToContents && state != STATE_HALF_EXPANDED) { if (!fitToContents && state != STATE_HALF_EXPANDED) {
expandHalfwayActionId = expandHalfwayActionIds.put(
viewIndex,
addAccessibilityActionForState( 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( replaceAccessibilityActionForState(
child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
} }
switch (state) { switch (state) {
@ -2170,36 +2375,54 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
{ {
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED; int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED;
replaceAccessibilityActionForState( replaceAccessibilityActionForState(
child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
break; break;
} }
case STATE_HALF_EXPANDED: case STATE_HALF_EXPANDED:
{ {
replaceAccessibilityActionForState( replaceAccessibilityActionForState(
child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
replaceAccessibilityActionForState( replaceAccessibilityActionForState(
child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
break; break;
} }
case STATE_COLLAPSED: case STATE_COLLAPSED:
{ {
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED; int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED;
replaceAccessibilityActionForState( replaceAccessibilityActionForState(
child, AccessibilityActionCompat.ACTION_EXPAND, nextState); view, AccessibilityActionCompat.ACTION_EXPAND, nextState);
break; 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( private void replaceAccessibilityActionForState(
V child, AccessibilityActionCompat action, @State int state) { View child, AccessibilityActionCompat action, @State int state) {
ViewCompat.replaceAccessibilityAction( ViewCompat.replaceAccessibilityAction(
child, action, null, createAccessibilityViewCommandForState(state)); child, action, null, createAccessibilityViewCommandForState(state));
} }
private int addAccessibilityActionForState( private int addAccessibilityActionForState(
V child, @StringRes int stringResId, @State int state) { View child, @StringRes int stringResId, @State int state) {
return ViewCompat.addAccessibilityAction( return ViewCompat.addAccessibilityAction(
child, child,
child.getResources().getString(stringResId), child.getResources().getString(stringResId),
@ -2216,4 +2439,3 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
}; };
} }
} }

View file

@ -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.
*
* <pre>
* dividerItemDecoration = new MaterialDividerItemDecoration(recyclerView.getContext(),
* layoutManager.getOrientation());
* recyclerView.addItemDecoration(dividerItemDecoration);
* </pre>
*
* 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.
*
* <p>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.
*
* <p>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;
}
}

View file

@ -48,6 +48,8 @@ import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -77,6 +79,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
FrameLayout(context, attrs, defStyleAttr) { FrameLayout(context, attrs, defStyleAttr) {
@Inject lateinit var imageLoader: ImageLoader @Inject lateinit var imageLoader: ImageLoader
@Inject lateinit var uiSettings: UISettings @Inject lateinit var uiSettings: UISettings
@Inject lateinit var imageSettings: ImageSettings
private val image: ImageView private val image: ImageView
@ -384,13 +387,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(songs) .data(songs)
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes)) .error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
.transformations(
RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f))
.target(image) .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. // Dispose of any previous image request and load a new image.
CoilUtils.dispose(image) CoilUtils.dispose(image)
imageLoader.enqueue(request) imageLoader.enqueue(request.build())
contentDescription = desc contentDescription = desc
} }

View file

@ -34,6 +34,8 @@ import org.oxycblt.auxio.util.logD
interface ImageSettings : Settings<ImageSettings.Listener> { interface ImageSettings : Settings<ImageSettings.Listener> {
/** The strategy to use when loading album covers. */ /** The strategy to use when loading album covers. */
val coverMode: CoverMode val coverMode: CoverMode
/** Whether to force all album covers to have a 1:1 aspect ratio. */
val forceSquareCovers: Boolean
interface Listener { interface Listener {
/** Called when [coverMode] changes. */ /** 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)) sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.MEDIA_STORE ?: CoverMode.MEDIA_STORE
override val forceSquareCovers: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
override fun migrate() { override fun migrate() {
// Show album covers and Ignore MediaStore covers were unified in 3.0.0 // Show album covers and Ignore MediaStore covers were unified in 3.0.0
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) || if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||

View file

@ -43,7 +43,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.asDeferred import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -155,7 +154,7 @@ constructor(
// Get the embedded picture from MediaMetadataRetriever, which will return a full // Get the embedded picture from MediaMetadataRetriever, which will return a full
// ByteArray of the cover without any compression artifacts. // 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 // 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? { private suspend fun extractExoplayerCover(album: Album): InputStream? {
@ -212,7 +211,7 @@ constructor(
} }
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult { private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
// Use whatever size coil gives us to create the mosaic. // Use whatever size coil gives us to create the mosaic.
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize = val mosaicFrameSize =
@ -234,7 +233,9 @@ constructor(
// Crop the bitmap down to a square so it leaves no empty space // Crop the bitmap down to a square so it leaves no empty space
// TODO: Work around this // 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) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width x += bitmap.width
@ -259,21 +260,4 @@ constructor(
val size = pxOrElse { 512 } val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size 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
}
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.image package org.oxycblt.auxio.image.extractor
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Bitmap.createBitmap import android.graphics.Bitmap.createBitmap
@ -43,7 +43,7 @@ import kotlin.math.roundToInt
* *
* @author Coil Team, Alexander Capehart (OxygenCobalt) * @author Coil Team, Alexander Capehart (OxygenCobalt)
*/ */
class RoundedCornersTransformation( class RoundedRectTransformation(
@Px private val topLeft: Float = 0f, @Px private val topLeft: Float = 0f,
@Px private val topRight: Float = 0f, @Px private val topRight: Float = 0f,
@Px private val bottomLeft: Float = 0f, @Px private val bottomLeft: Float = 0f,
@ -122,7 +122,7 @@ class RoundedCornersTransformation(
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
return other is RoundedCornersTransformation && return other is RoundedRectTransformation &&
topLeft == other.topLeft && topLeft == other.topLeft &&
topRight == other.topRight && topRight == other.topRight &&
bottomLeft == other.bottomLeft && bottomLeft == other.bottomLeft &&

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}

View file

@ -23,8 +23,6 @@ import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.util.LinkedList import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -339,19 +337,18 @@ constructor(
} }
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) = override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) =
worker.scope.launch { worker.scope.launch { indexWrapper(worker, withCache) }
private suspend fun indexWrapper(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
try { try {
val start = System.currentTimeMillis()
indexImpl(worker, withCache) indexImpl(worker, withCache)
logD(
"Music indexing completed successfully in " +
"${System.currentTimeMillis() - start}ms")
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Got cancelled, propagate upwards to top-level co-routine. // Got cancelled, propagate upwards to top-level co-routine.
logD("Loading routine was cancelled") logD("Loading routine was cancelled")
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
// Music loading process failed due to something we have not handled. // Music loading process failed due to something we have not handled.
// TODO: Still want to display this error eventually
logE("Music indexing failed") logE("Music indexing failed")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
emitIndexingCompletion(e) emitIndexingCompletion(e)
@ -359,20 +356,37 @@ constructor(
} }
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) { 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) == if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) { PackageManager.PERMISSION_DENIED) {
logE("Permissions were not granted") logE("Permissions were not granted")
// No permissions, signal that we can't do anything.
throw NoAudioPermissionException() throw NoAudioPermissionException()
} }
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on // Begin with querying MediaStore and the music cache. The former is needed for Auxio
// how long a media database query will take. // to figure out what songs are (probably) on the device, and the latter will be needed
emitIndexingProgress(IndexingProgress.Indeterminate) // for discovery (described later). These have no shared state, so they are done in
// parallel.
// Do the initial query of the cache and media databases in parallel.
logD("Starting MediaStore query") logD("Starting MediaStore query")
val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.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 = val cache =
if (withCache) { if (withCache) {
logD("Reading cache") logD("Reading cache")
@ -383,59 +397,121 @@ constructor(
logD("Awaiting MediaStore query") logD("Awaiting MediaStore query")
val query = mediaStoreQueryJob.await().getOrThrow() val query = mediaStoreQueryJob.await().getOrThrow()
// Now start processing the queried song information in parallel. Songs that can't be // We now have all the information required to start the "discovery" process. This
// received from the cache are consisted incomplete and pushed to a separate channel // is the point at which Auxio starts scanning each file given from MediaStore and
// that will eventually be processed into completed raw songs. // transforming it into a music library. MediaStore normally
logD("Starting song discovery") logD("Starting discovery")
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) // Not fully populated w/metadata
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) val completeSongs = Channel<RawSong>(Channel.UNLIMITED) // Populated with quality metadata
val processedSongs = Channel<RawSong>(Channel.UNLIMITED) val processedSongs = Channel<RawSong>(Channel.UNLIMITED) // Transformed into SongImpl
logD("Started MediaStore discovery")
// 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 = val mediaStoreJob =
worker.scope.tryAsync { worker.scope.async {
try {
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) 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() incompleteSongs.close()
} }
logD("Started ExoPlayer discovery")
val metadataJob = // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date
worker.scope.tryAsync { // metadata for them, and then forwards it to DeviceLibrary.
logD("Starting tag extraction")
val tagJob =
worker.scope.async {
try {
tagExtractor.consume(incompleteSongs, completeSongs) tagExtractor.consume(incompleteSongs, completeSongs)
} catch (e: Exception) {
completeSongs.close(e)
return@async
}
completeSongs.close() 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") logD("Starting DeviceLibrary creation")
val deviceLibraryJob = val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Default) { worker.scope.async(Dispatchers.Default) {
deviceLibraryFactory.create(completeSongs, processedSongs).also { val deviceLibrary =
processedSongs.close() 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. // 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<RawSong>() val rawSongs = LinkedList<RawSong>()
for (rawSong in processedSongs) { for (rawSong in processedSongs) {
rawSongs.add(rawSong) 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)) 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().getOrThrow()
metadataJob.await().getOrThrow()
// 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()) { if (rawSongs.isEmpty()) {
logE("Music library was empty") logE("Music library was empty")
throw NoMusicException() throw NoMusicException()
} }
// Successfully loaded the library, now save the cache and read playlist information // Now that the library is effectively loaded, we can start the finalization step, which
// in parallel. // 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") 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) 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") 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)
}
// 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) { if (cache == null || cache.invalidated) {
logD("Writing cache [why=${cache?.invalidated}]") logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs) cacheRepository.writeCache(rawSongs)
} }
// Create UserLibrary once we finally get the required components for it.
logD("Awaiting UserLibrary query") logD("Awaiting UserLibrary query")
val rawPlaylists = userLibraryQueryJob.await().getOrThrow() val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
logD("Awaiting DeviceLibrary creation") logD("Awaiting DeviceLibrary creation")
@ -443,14 +519,20 @@ constructor(
logD("Starting UserLibrary creation") logD("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") // Loading process is functionally done, indicate such
logD(
"Successfully indexed music library [device=$deviceLibrary " +
"user=$userLibrary time=${System.currentTimeMillis() - start}]")
emitIndexingCompletion(null) emitIndexingCompletion(null)
// Comparing the library instances is obscenely expensive, do it within the library
val deviceLibraryChanged: Boolean val deviceLibraryChanged: Boolean
val userLibraryChanged: 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) { 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 deviceLibraryChanged = this.deviceLibrary != deviceLibrary
userLibraryChanged = this.userLibrary != userLibrary userLibraryChanged = this.userLibrary != userLibrary
if (!deviceLibraryChanged && !userLibraryChanged) { if (!deviceLibraryChanged && !userLibraryChanged) {
@ -462,27 +544,13 @@ constructor(
this.userLibrary = userLibrary this.userLibrary = userLibrary
} }
// Consumers expect their updates to be on the main thread (notably PlaybackService),
// so switch to it.
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged) 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 <R> 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) { private suspend fun emitIndexingProgress(progress: IndexingProgress) {
yield() yield()
synchronized(this) { synchronized(this) {

View file

@ -151,6 +151,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
rawArtists = rawArtists =
rawAlbumArtists rawAlbumArtists
.ifEmpty { rawIndividualArtists } .ifEmpty { rawIndividualArtists }
.distinctBy { it.key }
.ifEmpty { listOf(RawArtist(null, null)) }) .ifEmpty { listOf(RawArtist(null, null)) })
/** /**
@ -159,7 +160,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
* [RawArtist]. This can be used to group up [Song]s into an [Artist]. * [RawArtist]. This can be used to group up [Song]s into an [Artist].
*/ */
val rawArtists = 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 * The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
@ -169,6 +173,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
rawSong.genreNames rawSong.genreNames
.parseId3GenreNames(musicSettings) .parseId3GenreNames(musicSettings)
.map { RawGenre(it) } .map { RawGenre(it) }
.distinctBy { it.key }
.ifEmpty { listOf(RawGenre()) } .ifEmpty { listOf(RawGenre()) }
/** /**
@ -207,6 +212,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
checkNotNull(_album) { "Malformed song: No album" } checkNotNull(_album) { "Malformed song: No album" }
check(_artists.isNotEmpty()) { "Malformed song: No artists" } check(_artists.isNotEmpty()) { "Malformed song: No artists" }
check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" }
for (i in _artists.indices) { for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with // Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata. // the artist ordering within the song metadata.
@ -217,6 +223,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
} }
check(_genres.isNotEmpty()) { "Malformed song: No genres" } check(_genres.isNotEmpty()) { "Malformed song: No genres" }
check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch" }
for (i in _genres.indices) { for (i in _genres.indices) {
// Non-destructively reorder the linked genres so that they align with // Non-destructively reorder the linked genres so that they align with
// the genre ordering within the song metadata. // the genre ordering within the song metadata.
@ -334,6 +341,7 @@ class AlbumImpl(
fun finalize(): Album { fun finalize(): Album {
check(songs.isNotEmpty()) { "Malformed album: Empty" } check(songs.isNotEmpty()) { "Malformed album: Empty" }
check(_artists.isNotEmpty()) { "Malformed album: No artists" } check(_artists.isNotEmpty()) { "Malformed album: No artists" }
check(_artists.size == rawArtists.size) { "Malformed album: Artist grouping mismatch" }
for (i in _artists.indices) { for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with // Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata. // the artist ordering within the song metadata.

View file

@ -364,7 +364,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns.TRACK, MediaStore.Audio.AudioColumns.TRACK,
// Below API 29, we are restricted to the absolute path (Called DATA by // 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) MediaStore.Audio.AudioColumns.DATA)
// The selector should be configured to convert the given directories instances to their // The selector should be configured to convert the given directories instances to their

View file

@ -149,18 +149,22 @@ private class TagWorkerImpl(
// Artist // Artist
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = 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 } ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
textFrames["TXXX:musicbrainz album artist id"]?.let { textFrames["TXXX:musicbrainz album artist id"]?.let {
rawSong.albumArtistMusicBrainzIds = it rawSong.albumArtistMusicBrainzIds = it
} }
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { (textFrames["TXXX:albumartists"]
rawSong.albumArtistNames = it ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"]
} ?: textFrames["TPE2"])
?.let { rawSong.albumArtistNames = it }
(textFrames["TXXX:albumartistssort"] (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 // This is a non-standard iTunes extension
?: textFrames["TSO2"]) ?: textFrames["TSO2"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
@ -261,17 +265,19 @@ private class TagWorkerImpl(
// Artist // Artist
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
(comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let { (comments["artistssort"]
rawSong.artistSortNames = it ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"])
} ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
(comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let { (comments["albumartists"]
rawSong.albumArtistNames = it ?: comments["album_artists"] ?: comments["album artists"]
} ?: comments["albumartist"])
?.let { rawSong.albumArtistNames = it }
(comments["albumartistssort"] (comments["albumartistssort"]
?: comments["albumartists_sort"] ?: comments["albumartistsort"]) ?: comments["albumartists_sort"] ?: comments["albumartists sort"]
?: comments["albumartistsort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre

View file

@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.logE
* @author Alexander Capehart * @author Alexander Capehart
* *
* TODO: Communicate errors * TODO: Communicate errors
* TODO: How to handle empty playlists that appear because all of their songs have disappeared?
*/ */
interface UserLibrary { interface UserLibrary {
/** The current user-defined playlists. */ /** The current user-defined playlists. */

View file

@ -25,6 +25,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
@ -150,8 +151,8 @@ class PlaybackService :
playbackManager.registerInternalPlayer(this) playbackManager.registerInternalPlayer(this)
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
mediaSessionComponent.registerListener(this) mediaSessionComponent.registerListener(this)
registerReceiver(
systemReceiver, val intentFilter =
IntentFilter().apply { IntentFilter().apply {
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG) addAction(AudioManager.ACTION_HEADSET_PLUG)
@ -162,7 +163,20 @@ class PlaybackService :
addAction(ACTION_SKIP_NEXT) addAction(ACTION_SKIP_NEXT)
addAction(ACTION_EXIT) addAction(ACTION_EXIT)
addAction(WidgetProvider.ACTION_WIDGET_UPDATE) 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") logD("Service created")
} }

View file

@ -94,11 +94,11 @@ interface Settings<L> {
final override fun onSharedPreferenceChanged( final override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences, sharedPreferences: SharedPreferences,
key: String key: String?
) { ) {
// FIXME: Settings initialization firing the listener. // FIXME: Settings initialization firing the listener.
logD("Dispatching settings change $key") logD("Dispatching settings change $key")
onSettingChanged(key, unlikelyToBeNull(listener)) onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener))
} }
/** /**

View file

@ -47,7 +47,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
} }
override fun onSetupPreference(preference: Preference) { 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") logD("Configuring cover mode setting")
preference.onPreferenceChangeListener = preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->

View file

@ -26,11 +26,11 @@ import androidx.preference.PreferenceGroupAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R 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
* separate preference categories. * preference categories.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -41,7 +41,7 @@ constructor(
attributeSet: AttributeSet? = null, attributeSet: AttributeSet? = null,
defStyleAttr: Int = R.attr.materialDividerStyle, defStyleAttr: Int = R.attr.materialDividerStyle,
orientation: Int = LinearLayoutManager.VERTICAL orientation: Int = LinearLayoutManager.VERTICAL
) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) { ) : MaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) = override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) =
try { try {

View file

@ -27,7 +27,8 @@ import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.ImageSettings 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.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.queue.Queue
@ -98,10 +99,19 @@ constructor(
return if (cornerRadius > 0) { return if (cornerRadius > 0) {
// If rounded, reduce the bitmap size further to obtain more pronounced // If rounded, reduce the bitmap size further to obtain more pronounced
// rounded corners. // rounded corners.
builder builder.size(getSafeRemoteViewsImageSize(context, 10f))
.size(getSafeRemoteViewsImageSize(context, 10f)) val cornersTransformation =
.transformations(RoundedCornersTransformation(cornerRadius.toFloat())) RoundedRectTransformation(cornerRadius.toFloat())
if (imageSettings.forceSquareCovers) {
builder.transformations(
SquareCropTransformation.INSTANCE, cornersTransformation)
} else { } else {
builder.transformations(cornersTransformation)
}
} else {
if (imageSettings.forceSquareCovers) {
builder.transformations(SquareCropTransformation.INSTANCE)
}
builder.size(getSafeRemoteViewsImageSize(context)) builder.size(getSafeRemoteViewsImageSize(context))
} }
} }

View file

@ -2,10 +2,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportWidth="960"
android:viewportWidth="24" android:viewportHeight="960"
android:viewportHeight="24"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M9.175,10.575 L4,5.4 5.4,4 10.575,9.175ZM14,20V18H16.6L13.425,14.825L14.85,13.4L18,16.55V14H20V20ZM5.4,20 L4,18.6 16.6,6H14V4H20V10H18V7.4Z" /> android:pathData="M560,800L560,720L664,720L537,593L594,536L720,662L720,560L800,560L800,800L560,800ZM216,800L160,744L664,240L560,240L560,160L800,160L800,400L720,400L720,296L216,800ZM367,423L160,216L216,160L423,367L367,423Z"/>
</vector> </vector>

View file

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorPrimary" android:viewportWidth="960"
android:viewportWidth="24" android:viewportHeight="960"
android:viewportHeight="24"> android:tint="?attr/colorPrimary">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M9.05,10.775 L3.675,5.4 5.45,3.625 10.825,9ZM13.75,20.275V17.725H16L13.275,14.975L15.025,13.2L17.75,15.925V13.725H20.325V20.275ZM5.45,20.375 L3.675,18.6 16,6.275H13.75V3.725H20.325V10.275H17.75V8.05Z" /> android:pathData="M546,817L546,703L627,703L528,603L607,524L706,623L706,543L820,543L820,817L546,817ZM219,823L140,744L627,257L546,257L546,143L820,143L820,417L706,417L706,336L219,823ZM359,435L140,216L219,137L438,356L359,435Z"/>
</vector> </vector>

View file

@ -293,4 +293,6 @@
<string name="def_disc">Няма дыска</string> <string name="def_disc">Няма дыска</string>
<string name="lbl_appears_on">З\'яўляецца на</string> <string name="lbl_appears_on">З\'яўляецца на</string>
<string name="fmt_editing">Рэдагаванне %s</string> <string name="fmt_editing">Рэдагаванне %s</string>
<string name="set_square_covers">Выкарыстоўваць квадратныя вокладкі альбомаў</string>
<string name="set_square_covers_desc">Абрэзаць усе вокладкі альбомаў да суадносін бакоў 1:1</string>
</resources> </resources>

View file

@ -55,9 +55,9 @@
<string name="set_root_title">Nastavení</string> <string name="set_root_title">Nastavení</string>
<string name="set_ui">Vzhled a chování</string> <string name="set_ui">Vzhled a chování</string>
<string name="set_theme">Motiv</string> <string name="set_theme">Motiv</string>
<string name="set_theme_auto">Automatické</string> <string name="set_theme_auto">Automaticky</string>
<string name="set_theme_day">Světlé</string> <string name="set_theme_day">Světlý</string>
<string name="set_theme_night">Tmavé</string> <string name="set_theme_night">Tmavý</string>
<string name="set_accent">Barevné schéma</string> <string name="set_accent">Barevné schéma</string>
<string name="set_black_mode">Černý motiv</string> <string name="set_black_mode">Černý motiv</string>
<string name="set_black_mode_desc">Použít kompletně černý tmavý motiv</string> <string name="set_black_mode_desc">Použít kompletně černý tmavý motiv</string>
@ -304,4 +304,6 @@
<string name="def_disc">Žádný disk</string> <string name="def_disc">Žádný disk</string>
<string name="fmt_editing">Úprava seznamu %s</string> <string name="fmt_editing">Úprava seznamu %s</string>
<string name="lbl_share">Sdílet</string> <string name="lbl_share">Sdílet</string>
<string name="set_square_covers">Vynutit čtvercové obaly alb</string>
<string name="set_square_covers_desc">Oříznout všechny covery alb na poměr stran 1:1</string>
</resources> </resources>

View file

@ -295,4 +295,6 @@
<string name="def_disc">Keine Disc</string> <string name="def_disc">Keine Disc</string>
<string name="lbl_appears_on">Erscheint in</string> <string name="lbl_appears_on">Erscheint in</string>
<string name="fmt_editing">%s bearbeiten</string> <string name="fmt_editing">%s bearbeiten</string>
<string name="set_square_covers">Quadratische Album-Cover erzwingen</string>
<string name="set_square_covers_desc">Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden</string>
</resources> </resources>

View file

@ -299,4 +299,6 @@
<string name="lbl_appears_on">Aparece en</string> <string name="lbl_appears_on">Aparece en</string>
<string name="lbl_share">Compartir</string> <string name="lbl_share">Compartir</string>
<string name="def_disc">Sin disco</string> <string name="def_disc">Sin disco</string>
<string name="set_square_covers">Carátula del álbum Force Square</string>
<string name="set_square_covers_desc">Recorta todas las portadas de los álbumes a una relación de aspecto 1:1</string>
</resources> </resources>

View file

@ -297,4 +297,6 @@
<string name="cdc_aac">Codage audio avancé (AAC)</string> <string name="cdc_aac">Codage audio avancé (AAC)</string>
<string name="def_disc">Aucun disque</string> <string name="def_disc">Aucun disque</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
<string name="set_square_covers">Forcer les pochettes d\'album carrées</string>
<string name="set_square_covers_desc">Recadrer toutes les pochettes d\'album au format 1:1</string>
</resources> </resources>

View file

@ -290,4 +290,6 @@
<string name="lbl_appears_on">Sudjelovanja:</string> <string name="lbl_appears_on">Sudjelovanja:</string>
<string name="lbl_share">Dijeli</string> <string name="lbl_share">Dijeli</string>
<string name="def_disc">Nema diska</string> <string name="def_disc">Nema diska</string>
<string name="set_square_covers">Prisili kvadratične omote albuma</string>
<string name="set_square_covers_desc">Odreži sve omote albuma na omjer 1:1</string>
</resources> </resources>

View file

@ -180,7 +180,7 @@
<string name="set_separators_comma">ਕੌਮਾ (,)</string> <string name="set_separators_comma">ਕੌਮਾ (,)</string>
<string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string> <string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string>
<string name="set_separators_slash">ਸਲੈਸ਼ (/)</string> <string name="set_separators_slash">ਸਲੈਸ਼ (/)</string>
<string name="set_separators_and">Ampersand (&amp;)</string> <string name="set_separators_and">ਐਂਪਰਸੈਂਡ (&amp;)</string>
<string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string> <string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string>
<string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string> <string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string>
<string name="set_playback">ਪਲੇਅਬੈਕ</string> <string name="set_playback">ਪਲੇਅਬੈਕ</string>
@ -287,4 +287,6 @@
<string name="clr_dynamic">ਡਾਇਨੈਮਿਕ</string> <string name="clr_dynamic">ਡਾਇਨੈਮਿਕ</string>
<string name="fmt_editing">%s ਸੋਧ ਰਿਹਾ</string> <string name="fmt_editing">%s ਸੋਧ ਰਿਹਾ</string>
<string name="fmt_indexing">ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)</string> <string name="fmt_indexing">ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)</string>
<string name="set_square_covers">ਵਰਗੀਕ੍ਰਿਤ ਐਲਬਮ ਕਵਰ ਫੋਰਸ ਕਰੋ</string>
<string name="set_square_covers_desc">ਸਾਰੇ ਐਲਬਮ ਕਵਰਾਂ ਨੂੰ 1:1 ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਤੱਕ ਕਾਂਟ-ਛਾਂਟ ਕਰੋ</string>
</resources> </resources>

View file

@ -40,7 +40,7 @@
<string name="err_no_music">Nie znaleziono utworów</string> <string name="err_no_music">Nie znaleziono utworów</string>
<!-- Description Namespace | Accessibility Strings --> <!-- Description Namespace | Accessibility Strings -->
<string name="desc_track_number">Utwór %d</string> <string name="desc_track_number">Utwór %d</string>
<string name="desc_play_pause">Odtwórz bądź zapauzuj</string> <string name="desc_play_pause">Odtwórz albo zapauzuj</string>
<!-- Hint Namespace | EditText Hints --> <!-- Hint Namespace | EditText Hints -->
<string name="lng_search_library">Szukaj w bibliotece…</string> <string name="lng_search_library">Szukaj w bibliotece…</string>
<!-- Color Label namespace | Accent names --> <!-- Color Label namespace | Accent names -->
@ -102,7 +102,7 @@
<string name="lbl_ep_live">Minialbum koncertowy</string> <string name="lbl_ep_live">Minialbum koncertowy</string>
<string name="lbl_ep_remix">Minialbum z remiksami</string> <string name="lbl_ep_remix">Minialbum z remiksami</string>
<string name="lbl_single_live">Koncertowy singiel</string> <string name="lbl_single_live">Koncertowy singiel</string>
<string name="lbl_single_remix">Remiks</string> <string name="lbl_single_remix">Remix</string>
<string name="lbl_compilations">Kompilacje</string> <string name="lbl_compilations">Kompilacje</string>
<string name="lbl_compilation">Kompilacja</string> <string name="lbl_compilation">Kompilacja</string>
<string name="lbl_soundtracks">Ścieżki dźwiękowe</string> <string name="lbl_soundtracks">Ścieżki dźwiękowe</string>
@ -139,7 +139,7 @@
<string name="set_reindex_desc">Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne</string> <string name="set_reindex_desc">Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne</string>
<string name="desc_remove_song">Usuń utwór z kolejki</string> <string name="desc_remove_song">Usuń utwór z kolejki</string>
<string name="set_replay_gain_mode_album">Preferuj album</string> <string name="set_replay_gain_mode_album">Preferuj album</string>
<string name="set_observing">Automatycznie odśwież</string> <string name="set_observing">Automatyczne odświeżanie</string>
<string name="cdc_flac">FLAC</string> <string name="cdc_flac">FLAC</string>
<string name="set_separators_and">Et (&amp;)</string> <string name="set_separators_and">Et (&amp;)</string>
<string name="err_index_failed">Nie udało się zaimportować utworów</string> <string name="err_index_failed">Nie udało się zaimportować utworów</string>
@ -243,7 +243,7 @@
<string name="set_cover_mode_off">Wyłączone</string> <string name="set_cover_mode_off">Wyłączone</string>
<string name="set_cover_mode_media_store">Niska jakość</string> <string name="set_cover_mode_media_store">Niska jakość</string>
<string name="set_cover_mode_quality">Wysoka jakość</string> <string name="set_cover_mode_quality">Wysoka jakość</string>
<string name="set_separators_warning">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.</string> <string name="set_separators_warning">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.</string>
<string name="set_ui_desc">Dostosuj motyw i kolory aplikacji</string> <string name="set_ui_desc">Dostosuj motyw i kolory aplikacji</string>
<string name="set_hide_collaborators">Ukryj wykonawców uczestniczących</string> <string name="set_hide_collaborators">Ukryj wykonawców uczestniczących</string>
<string name="set_content_desc">Zarządzaj importowaniem muzyki i obrazów</string> <string name="set_content_desc">Zarządzaj importowaniem muzyki i obrazów</string>
@ -254,7 +254,7 @@
<string name="set_dirs_list">Foldery</string> <string name="set_dirs_list">Foldery</string>
<string name="set_state">Stan odtwarzania</string> <string name="set_state">Stan odtwarzania</string>
<string name="set_images">Obrazy</string> <string name="set_images">Obrazy</string>
<string name="set_audio_desc">Zarządzanie dźwiękiem i odtwarzaniem muzyki</string> <string name="set_audio_desc">Zarządzaj dźwiękiem i odtwarzaniem muzyki</string>
<string name="lbl_play_selected">Odtwórz wybrane</string> <string name="lbl_play_selected">Odtwórz wybrane</string>
<string name="lbl_shuffle_selected">Wybrane losowo</string> <string name="lbl_shuffle_selected">Wybrane losowo</string>
<string name="fmt_selected">Wybrano %d</string> <string name="fmt_selected">Wybrano %d</string>
@ -288,16 +288,18 @@
<string name="def_song_count">Brak utworów</string> <string name="def_song_count">Brak utworów</string>
<string name="lng_playlist_added">Dodano do playlisty</string> <string name="lng_playlist_added">Dodano do playlisty</string>
<string name="fmt_def_playlist">Playlista %d</string> <string name="fmt_def_playlist">Playlista %d</string>
<string name="lbl_delete">Usuwać</string> <string name="lbl_delete">Usuń</string>
<string name="fmt_deletion_info">Usunąć %s\? Tego nie da się cofnąć.</string> <string name="fmt_deletion_info">Usunąć %s\? Tego nie da się cofnąć.</string>
<string name="lbl_rename">Przemianować</string> <string name="lbl_rename">Zmień nazwę</string>
<string name="lbl_rename_playlist">Przemianować playlistę</string> <string name="lbl_rename_playlist">Zmień nazwę playlisty</string>
<string name="lbl_confirm_delete_playlist">Usunąć playlistę\?</string> <string name="lbl_confirm_delete_playlist">Usunąć playlistę\?</string>
<string name="lbl_edit">Edytować</string> <string name="lbl_edit">Edytuj</string>
<string name="lbl_appears_on">Pojawia się</string> <string name="lbl_appears_on">Pojawia się na</string>
<string name="lbl_share">Udział</string> <string name="lbl_share">Udostępnij</string>
<string name="lng_playlist_renamed">Zmieniono nazwę playlisty</string> <string name="lng_playlist_renamed">Zmieniono nazwę playlisty</string>
<string name="lng_playlist_deleted">Playlista usunięta</string> <string name="lng_playlist_deleted">Usunięto playlistę</string>
<string name="def_disc">Brak dysku</string> <string name="def_disc">Brak nr. płyty</string>
<string name="fmt_editing">Edytowanie %s</string> <string name="fmt_editing">Edytowanie %s</string>
<string name="set_square_covers_desc">Przytnij okładki do formatu 1:1</string>
<string name="set_square_covers">Wymuś kwadratowe okładki</string>
</resources> </resources>

View file

@ -302,4 +302,6 @@
<string name="fmt_editing">Редактирование %s</string> <string name="fmt_editing">Редактирование %s</string>
<string name="def_disc">Нет диска</string> <string name="def_disc">Нет диска</string>
<string name="lbl_share">Поделиться</string> <string name="lbl_share">Поделиться</string>
<string name="set_square_covers">Использовать квадратные обложки альбомов</string>
<string name="set_square_covers_desc">Обрезать все обложки альбомов до соотношения сторон 1:1</string>
</resources> </resources>

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lbl_retry">Försök igen</string>
<string name="lbl_indexer">Musik laddar</string>
<string name="lbl_indexing">Laddar musik</string>
<string name="lbl_all_songs">Alla låtar</string>
<string name="lbl_albums">Album</string>
<string name="lbl_album">Albumet</string>
<string name="lbl_album_remix">Remix-album</string>
<string name="lbl_eps">EP</string>
<string name="lbl_ep">EP</string>
<string name="lbl_ep_live">Live-EP</string>
<string name="lbl_ep_remix">Remix-EP</string>
<string name="lbl_singles">Singlar</string>
<string name="lbl_single_remix">Remix-singel</string>
<string name="lbl_compilation">Sammanställning</string>
<string name="lbl_compilation_remix">Remix-sammanställning</string>
<string name="lbl_soundtracks">Ljudspår</string>
<string name="lbl_soundtrack">Ljudspår</string>
<string name="lbl_mixtapes">Blandband</string>
<string name="lbl_mixes">DJ-mixar</string>
<string name="lbl_live_group">Live</string>
<string name="lbl_remix_group">Remixar</string>
<string name="lbl_appears_on">Framträder på</string>
<string name="lbl_artist">Konstnär</string>
<string name="lbl_artists">Konstnär</string>
<string name="lbl_genres">Genrer</string>
<string name="lbl_playlist">Spellista</string>
<string name="lbl_playlists">Spellistor</string>
<string name="lbl_new_playlist">Ny spellista</string>
<string name="lbl_rename_playlist">Byt namn på spellista</string>
<string name="lbl_confirm_delete_playlist">Ta bort spellista\?</string>
<string name="lbl_search">Sök</string>
<string name="lbl_filter">Filtrera</string>
<string name="lbl_name">Namn</string>
<string name="lbl_date">Datum</string>
<string name="lbl_duration">Längd</string>
<string name="lbl_song_count">Antal låtar</string>
<string name="lbl_track">Spår</string>
<string name="lbl_date_added">Datum tillagt</string>
<string name="lbl_sort_asc">Stigande</string>
<string name="lbl_sort_dec">Fallande</string>
<string name="lbl_playback">Nu spelar</string>
<string name="lbl_equalizer">Utjämnare</string>
<string name="lbl_play">Spela</string>
<string name="lbl_play_selected">Spela utvalda</string>
<string name="lbl_shuffle">Blanda</string>
<string name="lbl_queue"></string>
<string name="lbl_play_next">Spela nästa</string>
<string name="lbl_playlist_add">Lägg till spellista</string>
<string name="lbl_go_artist">Gå till konstnär</string>
<string name="lbl_go_album">Gå till album</string>
<string name="lbl_song_detail">Visa egenskaper</string>
<string name="lbl_share">Dela</string>
<string name="lbl_props">Egenskaper för låt</string>
<string name="lbl_relative_path">Överordnad mapp</string>
<string name="lbl_format">Format</string>
<string name="lbl_size">Storlek</string>
<string name="lbl_sample_rate">Samplingsfrekvens</string>
<string name="lbl_shuffle_shortcut_short">Blanda</string>
<string name="lbl_shuffle_shortcut_long">Blanda alla</string>
<string name="lbl_ok">Okej</string>
<string name="lbl_cancel">Avbryt</string>
<string name="lbl_save">Spara</string>
<string name="lbl_state_restored">Tillstånd återstallde</string>
<string name="lbl_about">Om</string>
<string name="lbl_code">Källkod</string>
<string name="lbl_wiki">Wiki</string>
<string name="lbl_licenses">Licenser</string>
<string name="lng_widget">Visa och kontrollera musikuppspelning</string>
<string name="lng_indexing">Laddar ditt musikbibliotek…</string>
<string name="lng_observing">Övervakning ditt musikbibliotek för ändringar…</string>
<string name="lng_queue_added">Tillagd till kö</string>
<string name="lng_playlist_created">Spellista skapade</string>
<string name="lng_playlist_added">Tillagd till spellista</string>
<string name="lng_search_library">Sök ditt musikbibliotek…</string>
<string name="set_root_title">Inställningar</string>
<string name="set_ui">Utseende</string>
<string name="set_ui_desc">Ändra tema och färger på appen</string>
<string name="set_theme_auto">Automatisk</string>
<string name="set_theme_day">Ljust</string>
<string name="set_black_mode">Svart tema</string>
<string name="set_round_mode">Rundläge</string>
<string name="lbl_grant">Bevilja</string>
<string name="info_app_desc">En enkel, rationell musikspelare för Android.</string>
<string name="lbl_observing">Övervakar musikbiblioteket</string>
<string name="lbl_songs">Låtar</string>
<string name="lbl_album_live">Live-album</string>
<string name="lbl_delete">Ta bort</string>
<string name="lbl_compilation_live">Live-sammanställning</string>
<string name="lbl_single">Singel</string>
<string name="lbl_single_live">Live-singel</string>
<string name="lbl_compilations">Sammanställningar</string>
<string name="lbl_mixtape">Blandband</string>
<string name="lbl_mix">DJ-mix</string>
<string name="lbl_genre">Genre</string>
<string name="lbl_rename">Byt namn</string>
<string name="lbl_edit">Redigera</string>
<string name="lbl_filter_all">Alla</string>
<string name="lbl_disc">Disk</string>
<string name="lbl_sort">Sortera</string>
<string name="lbl_shuffle_selected">Blanda utvalda</string>
<string name="lbl_queue_add">Lägg till kö</string>
<string name="lbl_file_name">Filnamn</string>
<string name="lbl_add">Lägg till</string>
<string name="lbl_state_wiped">Tillstånd tog bort</string>
<string name="lbl_bitrate">Bithastighet</string>
<string name="lbl_reset">Återställ</string>
<string name="lbl_state_saved">Tillstånd sparat</string>
<string name="lbl_version">Version</string>
<string name="lbl_library_counts">Statistik över beroende</string>
<string name="lng_playlist_renamed">Bytt namn av spellista</string>
<string name="lng_playlist_deleted">Spellista tog bort</string>
<string name="lng_author">Utvecklad av Alexander Capeheart</string>
<string name="set_theme">Tema</string>
<string name="set_theme_night">Mörkt</string>
<string name="set_accent">Färgschema</string>
<string name="set_black_mode_desc">Använda rent svart för det mörka temat</string>
<string name="set_round_mode_desc">Aktivera rundade hörn på ytterligare element i användargränssnittet (kräver att albumomslag är rundade)</string>
<string name="set_personalize">Anpassa</string>
<string name="set_lib_tabs_desc">Ändra synlighet och ordningsföljd av bibliotekflikar</string>
<string name="set_bar_action">Anpassad åtgärd för uppspelningsfält</string>
<string name="set_notif_action">Anpassad aviseringsåtgärd</string>
<string name="set_action_mode_next">Hoppa till nästa</string>
<string name="set_action_mode_repeat">Upprepningsmodus</string>
<string name="set_behavior">Beteende</string>
<string name="set_detail_song_playback_mode">När spelar från artikeluppgifter</string>
<string name="set_playback_mode_genre">Spela från genre</string>
<string name="set_keep_shuffle">Komma ihåg blandningsstatus</string>
<string name="set_keep_shuffle_desc">Behåll blandning på när spelar en ny låt</string>
<string name="set_content">Kontent</string>
<string name="set_content_desc">Kontrollera hur musik och bilar laddas</string>
<string name="set_music">Musik</string>
<string name="set_observing">Automatisk omladdning</string>
<string name="set_exclude_non_music">Inkludera bara musik</string>
<string name="set_exclude_non_music_desc">Ignorera ljudfiler som inte är musik, t.ex. podkaster</string>
<string name="set_separators">Värdeavskiljare</string>
<string name="set_separators_plus">Plus (+)</string>
<string name="set_intelligent_sorting">Intelligent sortering</string>
<string name="set_intelligent_sorting_desc">Sorterar namn som börjar med siffror eller ord som \"the\" korrekt (fungerar bäst med engelskspråkig music)</string>
<string name="set_hide_collaborators">Dölj medarbetare</string>
<string name="set_display">Skärm</string>
<string name="set_lib_tabs">Bibliotekflikar</string>
<string name="set_library_song_playback_mode">När spelar från biblioteket</string>
<string name="set_playback_mode_none">Spela från visad artikel</string>
<string name="set_playback_mode_songs">Spela från alla låtar</string>
<string name="set_playback_mode_artist">Spela från konstnär</string>
<string name="set_playback_mode_album">Spela från album</string>
<string name="set_separators_semicolon">Semikolon (;)</string>
<string name="set_observing_desc">Ladda om musikbiblioteket när det ändras (kräver permanent meddelande)</string>
<string name="set_separators_comma">Komma (,)</string>
<string name="set_separators_slash">Snedstreck (/)</string>
<string name="set_separators_desc">Konfigurera tecken som separerar flera värden i taggar</string>
<string name="set_separators_warning">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 (\\).</string>
<string name="set_personalize_desc">Anpassa UI-kontroller och beteende</string>
</resources>

View file

@ -299,4 +299,6 @@
<string name="fmt_editing">Редагування %s</string> <string name="fmt_editing">Редагування %s</string>
<string name="def_disc">Немає диску</string> <string name="def_disc">Немає диску</string>
<string name="lbl_appears_on">З\'являється на</string> <string name="lbl_appears_on">З\'являється на</string>
<string name="set_square_covers_desc">Обрізання обкладинки альбомів до співвідношення сторін 1:1</string>
<string name="set_square_covers">Примусові квадратні обкладинки</string>
</resources> </resources>

View file

@ -293,4 +293,6 @@
<string name="lbl_share">分享</string> <string name="lbl_share">分享</string>
<string name="def_disc">无唱片</string> <string name="def_disc">无唱片</string>
<string name="fmt_editing">正在编辑 %s</string> <string name="fmt_editing">正在编辑 %s</string>
<string name="set_square_covers">强制使用方形专辑封面</string>
<string name="set_square_covers_desc">将所有专辑封面裁剪至 1:1 宽高比</string>
</resources> </resources>

View file

@ -20,7 +20,6 @@
<dimen name="size_corners_medium">16dp</dimen> <dimen name="size_corners_medium">16dp</dimen>
<dimen name="size_corners_mid_large">24dp</dimen> <dimen name="size_corners_mid_large">24dp</dimen>
<dimen name="size_btn">48dp</dimen> <dimen name="size_btn">48dp</dimen>
<dimen name="size_accent_item">56dp</dimen> <dimen name="size_accent_item">56dp</dimen>
<dimen name="size_bottom_sheet_bar">64dp</dimen> <dimen name="size_bottom_sheet_bar">64dp</dimen>

View file

@ -15,6 +15,7 @@
<string name="set_key_observing" translatable="false">auxio_observing</string> <string name="set_key_observing" translatable="false">auxio_observing</string>
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string> <string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string> <string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string> <string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string> <string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
<string name="set_key_separators" translatable="false">auxio_separators</string> <string name="set_key_separators" translatable="false">auxio_separators</string>

View file

@ -239,6 +239,8 @@
<string name="set_cover_mode_off">Off</string> <string name="set_cover_mode_off">Off</string>
<string name="set_cover_mode_media_store">Fast</string> <string name="set_cover_mode_media_store">Fast</string>
<string name="set_cover_mode_quality">High quality</string> <string name="set_cover_mode_quality">High quality</string>
<string name="set_square_covers">Force square album covers</string>
<string name="set_square_covers_desc">Crop all album covers to a 1:1 aspect ratio</string>
<string name="set_audio">Audio</string> <string name="set_audio">Audio</string>
<string name="set_audio_desc">Configure sound and playback behavior</string> <string name="set_audio_desc">Configure sound and playback behavior</string>

View file

@ -216,7 +216,7 @@
<item name="android:paddingEnd">@dimen/spacing_small</item> <item name="android:paddingEnd">@dimen/spacing_small</item>
<item name="android:paddingTop">@dimen/spacing_small</item> <item name="android:paddingTop">@dimen/spacing_small</item>
<item name="android:paddingBottom">@dimen/spacing_small</item> <item name="android:paddingBottom">@dimen/spacing_small</item>
<!-- Intentional to prevent button from spamming the log console over --> <!-- Intentional to prevent button from spamming the log console -->
<item name="iconTint">@color/m3_text_button_foreground_color_selector</item> <item name="iconTint">@color/m3_text_button_foreground_color_selector</item>
</style> </style>

View file

@ -43,5 +43,11 @@
app:key="@string/set_key_cover_mode" app:key="@string/set_key_cover_mode"
app:title="@string/set_cover_mode" /> app:title="@string/set_cover_mode" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="@string/set_key_square_covers"
app:summary="@string/set_square_covers_desc"
app:title="@string/set_square_covers" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -1,7 +1,7 @@
buildscript { buildscript {
ext { ext {
kotlin_version = '1.8.21' kotlin_version = '1.8.22'
navigation_version = "2.5.3" navigation_version = "2.6.0"
hilt_version = '2.46.1' hilt_version = '2.46.1'
} }

View file

@ -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.

View file

@ -0,0 +1 @@
En enkel, rationell musikspelare