diff --git a/app/build.gradle b/app/build.gradle
index d0ddac776..0a14a0b74 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,7 +10,7 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify
// it here so that binary stripping will work.
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
@@ -20,11 +20,11 @@ android {
defaultConfig {
applicationId namespace
- versionName "3.1.2"
- versionCode 32
+ versionName "3.1.3"
+ versionCode 33
minSdk 24
- targetSdk 33
+ targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -84,16 +84,19 @@ dependencies {
// --- SUPPORT ---
// General
- implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.core:core-ktx:1.10.1"
+ implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.activity:activity-ktx:1.7.2"
- implementation "androidx.fragment:fragment-ktx:1.5.7"
+ implementation "androidx.fragment:fragment-ktx:1.6.0"
- // UI
- implementation "androidx.recyclerview:recyclerview:1.3.0"
+ // Components
+ // Deliberately kept on 1.2.1 to prevent a bug where the queue sheet will not collapse on
+ // certain upwards scrolling events
+ // TODO: Report this issue and hope for a timely fix
+ // noinspection GradleDependency
+ implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
- implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
- implementation 'androidx.core:core-ktx:1.10.1'
+ implementation "androidx.viewpager2:viewpager2:1.0.0"
// Lifecycle
def lifecycle_version = "2.6.1"
@@ -128,11 +131,9 @@ dependencies {
implementation 'io.coil-kt:coil-base:2.4.0'
// Material
- // TODO: Stuck on 1.8.0-alpha01 until ripple bug with tab layout is actually available
- // in a version that I can build with
// TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just
// PR a fix.
- implementation "com.google.android.material:material:1.8.0-alpha01"
+ implementation "com.google.android.material:material:1.10.0-alpha04"
// Dependency Injection
implementation "com.google.dagger:dagger:$hilt_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fd7b28a4a..ba4b27028 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java
index c6560c151..ab55a48dc 100644
--- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java
+++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java
@@ -16,10 +16,14 @@
package com.google.android.material.bottomsheet;
+import com.google.android.material.R;
+
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.lang.Math.max;
import static java.lang.Math.min;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
@@ -32,8 +36,10 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.MotionEvent;
+import android.view.RoundedCorner;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View.MeasureSpec;
@@ -41,13 +47,15 @@ import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent;
+import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
-
+import androidx.activity.BackEventCompat;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
@@ -62,14 +70,13 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
import androidx.core.view.accessibility.AccessibilityViewCommand;
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper;
-
-import com.google.android.material.R;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.ViewUtils.RelativePadding;
+import com.google.android.material.motion.MaterialBackHandler;
+import com.google.android.material.motion.MaterialBottomContainerBackHelper;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
@@ -84,13 +91,11 @@ import java.util.Map;
* To send useful accessibility events, set a title on bottom sheets that are windows or are
* window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}.
- *
- * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
- * obtainable in the currently used MDC library.
*/
-public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior {
+public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior
+ implements MaterialBackHandler {
- /** Listener for monitoring events about bottom sheets. */
+ /** Callback for monitoring events about bottom sheets. */
public abstract static class BottomSheetCallback {
/**
@@ -203,11 +208,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
@Retention(RetentionPolicy.SOURCE)
public @interface SaveFlags {}
- private static final String TAG = "BottomSheetBehavior";
+ private static final String TAG = "BackportBottomSheetBehavior";
@SaveFlags private int saveFlags = SAVE_NONE;
- private static final int SIGNIFICANT_VEL_THRESHOLD = 500;
+ @VisibleForTesting static final int DEFAULT_SIGNIFICANT_VEL_THRESHOLD = 500;
private static final float HIDE_THRESHOLD = 0.5f;
@@ -217,12 +222,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
private static final int NO_MAX_SIZE = -1;
+ private static final int VIEW_INDEX_BOTTOM_SHEET = 0;
+
+ private static final int INVALID_POSITION = -1;
+
+ @VisibleForTesting
+ static final int VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW = 1;
+
private boolean fitToContents = true;
private boolean updateImportantForAccessibilityOnSiblings = false;
private float maximumVelocity;
+ private int significantVelocityThreshold;
+
/** Peek height set by the user. */
private int peekHeight;
@@ -256,10 +270,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
private int insetBottom;
private int insetTop;
+ private boolean shouldRemoveExpandedCorners;
+
/** Default Shape Appearance to be used in bottomsheet */
private ShapeAppearanceModel shapeAppearanceModelDefault;
- private boolean isShapeExpanded;
+ private boolean expandedCornersRemoved;
private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker();
@@ -304,22 +320,25 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
int parentHeight;
@Nullable WeakReference viewRef;
+ @Nullable WeakReference accessibilityDelegateViewRef;
@Nullable WeakReference nestedScrollingChildRef;
@NonNull private final ArrayList callbacks = new ArrayList<>();
@Nullable private VelocityTracker velocityTracker;
+ @Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper;
int activePointerId;
- private int initialY;
+ private int initialY = INVALID_POSITION;
boolean touchingScrollingChild;
@Nullable private Map importantForAccessibilityMap;
- private int expandHalfwayActionId = View.NO_ID;
+ @VisibleForTesting
+ final SparseIntArray expandHalfwayActionIds = new SparseIntArray();
public BackportBottomSheetBehavior() {}
@@ -387,6 +406,11 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0));
}
+ setSignificantVelocityThreshold(
+ a.getInt(
+ R.styleable.BottomSheetBehavior_Layout_behavior_significantVelocityThreshold,
+ DEFAULT_SIGNIFICANT_VEL_THRESHOLD));
+
// Reading out if we are handling padding, so we can apply it to the content.
paddingBottomSystemWindowInsets =
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false);
@@ -404,6 +428,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false);
marginTopSystemWindowInsets =
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false);
+ shouldRemoveExpandedCorners =
+ a.getBoolean(R.styleable.BottomSheetBehavior_Layout_shouldRemoveExpandedCorners, true);
a.recycle();
ViewConfiguration configuration = ViewConfiguration.get(context);
@@ -440,6 +466,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
// first time we layout with this behavior by checking (viewRef == null).
viewRef = null;
viewDragHelper = null;
+ bottomContainerBackHelper = null;
}
@Override
@@ -448,6 +475,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
// Release references so we don't run unnecessary codepaths while not attached to a view.
viewRef = null;
viewDragHelper = null;
+ bottomContainerBackHelper = null;
}
@Override
@@ -515,7 +543,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
peekHeightMin =
parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min);
setWindowInsetsListener(child);
+ ViewCompat.setWindowInsetsAnimationCallback(child, new InsetsAnimationCallback(child));
viewRef = new WeakReference<>(child);
+ bottomContainerBackHelper = new MaterialBottomContainerBackHelper(child);
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
// default to android:background declared in styles or layout.
if (materialShapeDrawable != null) {
@@ -523,9 +553,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
// Use elevation attr if set on bottomsheet; otherwise, use elevation of child view.
materialShapeDrawable.setElevation(
elevation == -1 ? ViewCompat.getElevation(child) : elevation);
- // Update the material shape based on initial state.
- isShapeExpanded = state == STATE_EXPANDED;
- materialShapeDrawable.setInterpolation(isShapeExpanded ? 0f : 1f);
} else if (backgroundTint != null) {
ViewCompat.setBackgroundTintList(child, backgroundTint);
}
@@ -549,11 +576,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
if (parentHeight - childHeight < insetTop) {
if (paddingTopSystemWindowInsets) {
// If the bottomsheet would land in the middle of the status bar when fully expanded add
- // extra space to make sure it goes all the way.
- childHeight = parentHeight;
+ // extra space to make sure it goes all the way up or up to max height if it is specified.
+ childHeight = (maxHeight == NO_MAX_SIZE) ? parentHeight : min(parentHeight, maxHeight);
} else {
// If we don't want the bottomsheet to go under the status bar we cap its height
- childHeight = parentHeight - insetTop;
+ int insetHeight = parentHeight - insetTop;
+ childHeight = (maxHeight == NO_MAX_SIZE) ? insetHeight : min(insetHeight, maxHeight);
}
}
fitToContentsOffset = max(0, parentHeight - childHeight);
@@ -571,6 +599,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
}
+ updateDrawableForTargetState(state, /* animate= */ false);
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
@@ -640,6 +669,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
&& state != STATE_DRAGGING
&& !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY())
&& viewDragHelper != null
+ && initialY != INVALID_POSITION
&& Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop();
}
@@ -723,8 +753,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
} else if (dy < 0) { // Downward
if (!target.canScrollVertically(-1)) {
- // MODIFICATION: Add isHideableWhenDragging method
- if (newTop <= collapsedOffset || (hideable && isHideableWhenDragging())) {
+ if (newTop <= collapsedOffset || canBeHiddenByDragging()) {
if (!draggable) {
// Prevent dragging
return;
@@ -778,7 +807,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
}
}
- // MODIFICATION: Add isHideableWhenDragging method
} else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) {
targetState = STATE_HIDDEN;
} else if (lastNestedScrollDy == 0) {
@@ -888,6 +916,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
// Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents.
setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state);
+ updateDrawableForTargetState(state, /* animate= */ true);
updateAccessibilityActions();
}
@@ -897,7 +926,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* be adjusted as expected.
*
* @param maxWidth The maximum width in pixels to be set
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth
* @see #getMaxWidth()
*/
public void setMaxWidth(@Px int maxWidth) {
@@ -907,7 +936,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
/**
* Returns the bottom sheet's maximum width, or -1 if no maximum width is set.
*
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth
* @see #setMaxWidth(int)
*/
@Px
@@ -920,7 +949,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* BottomSheetDialog#show()} in order for the height to be adjusted as expected.
*
* @param maxHeight The maximum height in pixels to be set
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight
* @see #getMaxHeight()
*/
public void setMaxHeight(@Px int maxHeight) {
@@ -930,7 +959,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
/**
* Returns the bottom sheet's maximum height, or -1 if no maximum height is set.
*
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight
* @see #setMaxHeight(int)
*/
@Px
@@ -944,7 +973,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
*/
public void setPeekHeight(int peekHeight) {
setPeekHeight(peekHeight, false);
@@ -958,7 +987,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
* @param animate Whether to animate between the old height and the new height.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
*/
public final void setPeekHeight(int peekHeight, boolean animate) {
boolean layout = false;
@@ -1001,7 +1030,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the
* sheet is configured to peek automatically at 16:9 ratio keyline
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
*/
public int getPeekHeight() {
return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight;
@@ -1015,7 +1044,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
*
* @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio
*/
public void setHalfExpandedRatio(
@FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) {
@@ -1035,7 +1064,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state.
*
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio
*/
@FloatRange(from = 0.0f, to = 1.0f)
public float getHalfExpandedRatio() {
@@ -1050,13 +1079,14 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* @param offset an integer value greater than equal to 0, representing the {@link
* #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset
*/
public void setExpandedOffset(int offset) {
if (offset < 0) {
throw new IllegalArgumentException("offset must be greater than or equal to 0");
}
this.expandedOffset = offset;
+ updateDrawableForTargetState(state, /* animate= */ true);
}
/**
@@ -1064,7 +1094,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* pick the offset depending on the height of the content.
*
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset
*/
public int getExpandedOffset() {
return fitToContents
@@ -1072,8 +1102,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
: Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop);
}
- // MODIFICATION: Add calculateSlideOffset method
-
/**
* Calculates the current offset of the bottom sheet.
*
@@ -1082,26 +1110,21 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* @return The offset of this bottom sheet within [-1,1] range. Offset increases
* as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and
* expanded states and from -1 to 0 it is between hidden and collapsed states. Returns
- * {@code Float.MIN_VALUE} if the bottom sheet is not laid out.
+ * -1 if the bottom sheet is not laid out (therefore it's hidden).
*/
public float calculateSlideOffset() {
- if (viewRef == null) {
- return Float.MIN_VALUE;
+ if (viewRef == null || viewRef.get() == null) {
+ return -1;
}
- View bottomSheet = viewRef.get();
- if (bottomSheet != null) {
- return calculateSlideOffset(bottomSheet.getTop());
- }
-
- return Float.MIN_VALUE;
+ return calculateSlideOffsetWithTop(viewRef.get().getTop());
}
/**
- * Sets whether this bottom sheet can hide when it is swiped down.
+ * Sets whether this bottom sheet can hide.
*
* @param hideable {@code true} to make this bottom sheet hideable.
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable
*/
public void setHideable(boolean hideable) {
if (this.hideable != hideable) {
@@ -1118,7 +1141,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* Gets whether this bottom sheet can hide when it is swiped down.
*
* @return {@code true} if this bottom sheet can hide.
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable
*/
public boolean isHideable() {
return hideable;
@@ -1130,7 +1153,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
*
* @param skipCollapsed True if the bottom sheet should skip the collapsed state.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed
*/
public void setSkipCollapsed(boolean skipCollapsed) {
this.skipCollapsed = skipCollapsed;
@@ -1142,7 +1165,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
*
* @return Whether the bottom sheet should skip the collapsed state.
* @attr ref
- * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
+ * com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed
*/
public boolean getSkipCollapsed() {
return skipCollapsed;
@@ -1153,7 +1176,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* dragging, an app will require to implement a custom way to expand/collapse the bottom sheet
*
* @param draggable {@code false} to prevent dragging the sheet to collapse and expand
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_draggable
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_draggable
*/
public void setDraggable(boolean draggable) {
this.draggable = draggable;
@@ -1163,13 +1186,35 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
return draggable;
}
+ /*
+ * Sets the velocity threshold considered significant enough to trigger a slide
+ * to the next stable state.
+ *
+ * @param significantVelocityThreshold The velocity threshold that warrants a vertical swipe.
+ * @see #getSignificantVelocityThreshold()
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold
+ */
+ public void setSignificantVelocityThreshold(int significantVelocityThreshold) {
+ this.significantVelocityThreshold = significantVelocityThreshold;
+ }
+
+ /*
+ * Returns the significant velocity threshold.
+ *
+ * @see #setSignificantVelocityThreshold(int)
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold
+ */
+ public int getSignificantVelocityThreshold() {
+ return this.significantVelocityThreshold;
+ }
+
/**
* Sets save flags to be preserved in bottomsheet on configuration change.
*
* @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link
* #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}.
* @see #getSaveFlags()
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags
*/
public void setSaveFlags(@SaveFlags int flags) {
this.saveFlags = flags;
@@ -1178,7 +1223,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
* Returns the save flags.
*
* @see #setSaveFlags(int)
- * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
+ * @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags
*/
@SaveFlags
public int getSaveFlags() {
@@ -1208,9 +1253,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
/**
- * Sets a listener to be notified of bottom sheet events.
+ * Sets a callback to be notified of bottom sheet events.
*
- * @param callback The listener to notify when bottom sheet events occur.
+ * @param callback The callback to notify when bottom sheet events occur.
* @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link
* #removeBottomSheetCallback(BottomSheetCallback)} instead
*/
@@ -1218,7 +1263,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
public void setBottomSheetCallback(BottomSheetCallback callback) {
Log.w(
TAG,
- "BottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes"
+ "BackportBottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes"
+ " all existing callbacks, including ones set internally by library authors, which"
+ " may result in unintended behavior. This may change in the future. Please use"
+ " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your"
@@ -1230,9 +1275,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
/**
- * Adds a listener to be notified of bottom sheet events.
+ * Adds a callback to be notified of bottom sheet events.
*
- * @param callback The listener to notify when bottom sheet events occur.
+ * @param callback The callback to notify when bottom sheet events occur.
*/
public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) {
if (!callbacks.contains(callback)) {
@@ -1241,9 +1286,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
/**
- * Removes a previously added listener.
+ * Removes a previously added callback.
*
- * @param callback The listener to remove.
+ * @param callback The callback to remove.
*/
public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) {
callbacks.remove(callback);
@@ -1325,6 +1370,26 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
return gestureInsetBottomIgnored;
}
+ /**
+ * Sets whether the bottom sheet should remove its corners when it reaches the expanded state.
+ *
+ * If false, the bottom sheet will only remove its corners if it is expanded and reaches the
+ * top of the screen.
+ */
+ public void setShouldRemoveExpandedCorners(boolean shouldRemoveExpandedCorners) {
+ if (this.shouldRemoveExpandedCorners != shouldRemoveExpandedCorners) {
+ this.shouldRemoveExpandedCorners = shouldRemoveExpandedCorners;
+ updateDrawableForTargetState(getState(), /* animate= */ true);
+ }
+ }
+
+ /**
+ * Returns whether the bottom sheet will remove its corners when it reaches the expanded state.
+ */
+ public boolean isShouldRemoveExpandedCorners() {
+ return shouldRemoveExpandedCorners;
+ }
+
/**
* Gets the current state of the bottom sheet.
*
@@ -1376,33 +1441,91 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
updateImportantForAccessibility(false);
}
- updateDrawableForTargetState(state);
+ updateDrawableForTargetState(state, /* animate= */ true);
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onStateChanged(bottomSheet, state);
}
updateAccessibilityActions();
}
- private void updateDrawableForTargetState(@State int state) {
+ private void updateDrawableForTargetState(@State int state, boolean animate) {
if (state == STATE_SETTLING) {
// Special case: we want to know which state we're settling to, so wait for another call.
return;
}
- boolean expand = state == STATE_EXPANDED;
- if (isShapeExpanded != expand) {
- isShapeExpanded = expand;
- if (materialShapeDrawable != null && interpolatorAnimator != null) {
- if (interpolatorAnimator.isRunning()) {
- interpolatorAnimator.reverse();
- } else {
- float to = expand ? 0f : 1f;
- float from = 1f - to;
- interpolatorAnimator.setFloatValues(from, to);
- interpolatorAnimator.start();
+ boolean removeCorners = isExpandedAndShouldRemoveCorners();
+ if (expandedCornersRemoved == removeCorners || materialShapeDrawable == null) {
+ return;
+ }
+ expandedCornersRemoved = removeCorners;
+ if (animate && interpolatorAnimator != null) {
+ if (interpolatorAnimator.isRunning()) {
+ interpolatorAnimator.reverse();
+ } else {
+ float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f;
+ float from = 1f - to;
+ interpolatorAnimator.setFloatValues(from, to);
+ interpolatorAnimator.start();
+ }
+ } else {
+ if (interpolatorAnimator != null && interpolatorAnimator.isRunning()) {
+ interpolatorAnimator.cancel();
+ }
+ materialShapeDrawable.setInterpolation(
+ expandedCornersRemoved ? calculateInterpolationWithCornersRemoved() : 1f);
+ }
+ }
+
+ private float calculateInterpolationWithCornersRemoved() {
+ if (materialShapeDrawable != null
+ && viewRef != null
+ && viewRef.get() != null
+ && VERSION.SDK_INT >= VERSION_CODES.S) {
+ V view = viewRef.get();
+ // Only use device corner radius if sheet is touching top of screen.
+ if (isAtTopOfScreen()) {
+ final WindowInsets insets = view.getRootWindowInsets();
+ if (insets != null) {
+ float topLeftInterpolation =
+ calculateCornerInterpolation(
+ materialShapeDrawable.getTopLeftCornerResolvedSize(),
+ insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT));
+ float topRightInterpolation =
+ calculateCornerInterpolation(
+ materialShapeDrawable.getTopRightCornerResolvedSize(),
+ insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT));
+ return Math.max(topLeftInterpolation, topRightInterpolation);
}
}
}
+ return 0;
+ }
+
+ @RequiresApi(VERSION_CODES.S)
+ private float calculateCornerInterpolation(
+ float materialShapeDrawableCornerSize, @Nullable RoundedCorner deviceRoundedCorner) {
+ if (deviceRoundedCorner != null) {
+ float deviceCornerRadius = deviceRoundedCorner.getRadius();
+ if (deviceCornerRadius > 0 && materialShapeDrawableCornerSize > 0) {
+ return deviceCornerRadius / materialShapeDrawableCornerSize;
+ }
+ }
+ return 0;
+ }
+
+ private boolean isAtTopOfScreen() {
+ if (viewRef == null || viewRef.get() == null) {
+ return false;
+ }
+ int[] location = new int[2];
+ viewRef.get().getLocationOnScreen(location);
+ return location[1] == 0;
+ }
+
+ private boolean isExpandedAndShouldRemoveCorners() {
+ // Only remove corners when it's full screen.
+ return state == STATE_EXPANDED && (shouldRemoveExpandedCorners || isAtTopOfScreen());
}
private int calculatePeekHeight() {
@@ -1432,9 +1555,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio));
}
- // MODIFICATION: Add calculateSlideOffset method
-
- private float calculateSlideOffset(int top) {
+ private float calculateSlideOffsetWithTop(int top) {
return
(top > collapsedOffset || collapsedOffset == getExpandedOffset())
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
@@ -1443,6 +1564,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
private void reset() {
activePointerId = ViewDragHelper.INVALID_POINTER;
+ initialY = INVALID_POSITION;
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
@@ -1473,6 +1595,9 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
if (skipCollapsed) {
return true;
}
+ if (!isHideableWhenDragging()) {
+ return false;
+ }
if (child.getTop() < collapsedOffset) {
// It should not hide, but collapse.
return false;
@@ -1482,9 +1607,73 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
}
+ @Override
+ public void startBackProgress(@NonNull BackEventCompat backEvent) {
+ if (bottomContainerBackHelper == null) {
+ return;
+ }
+ bottomContainerBackHelper.startBackProgress(backEvent);
+ }
+
+ @Override
+ public void updateBackProgress(@NonNull BackEventCompat backEvent) {
+ if (bottomContainerBackHelper == null) {
+ return;
+ }
+ bottomContainerBackHelper.updateBackProgress(backEvent);
+ }
+
+ @Override
+ public void handleBackInvoked() {
+ if (bottomContainerBackHelper == null) {
+ return;
+ }
+ BackEventCompat backEvent = bottomContainerBackHelper.onHandleBackInvoked();
+ if (backEvent == null || VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // If using traditional button system nav or if pre-U, just hide or collapse the bottom sheet.
+ setState(hideable ? STATE_HIDDEN : STATE_COLLAPSED);
+ return;
+ }
+ if (hideable) {
+ bottomContainerBackHelper.finishBackProgressNotPersistent(
+ backEvent,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Hide immediately following the built-in predictive back slide down animation.
+ setStateInternal(STATE_HIDDEN);
+ if (viewRef != null && viewRef.get() != null) {
+ viewRef.get().requestLayout();
+ }
+ }
+ });
+ } else {
+ bottomContainerBackHelper.finishBackProgressPersistent(
+ backEvent, /* animatorListener= */ null);
+ setState(STATE_COLLAPSED);
+ }
+ }
+
+ @Override
+ public void cancelBackProgress() {
+ if (bottomContainerBackHelper == null) {
+ return;
+ }
+ bottomContainerBackHelper.cancelBackProgress();
+ }
+
+ @VisibleForTesting
+ @Nullable
+ MaterialBottomContainerBackHelper getBackHelper() {
+ return bottomContainerBackHelper;
+ }
+
@Nullable
@VisibleForTesting
View findScrollingChild(View view) {
+ if (view.getVisibility() != View.VISIBLE) {
+ return null;
+ }
if (ViewCompat.isNestedScrollingEnabled(view)) {
return view;
}
@@ -1524,12 +1713,12 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
}
- MaterialShapeDrawable getMaterialShapeDrawable() {
+ protected MaterialShapeDrawable getMaterialShapeDrawable() {
return materialShapeDrawable;
}
private void createShapeValueAnimator() {
- interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f);
+ interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f);
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION);
interpolatorAnimator.addUpdateListener(
new AnimatorUpdateListener() {
@@ -1650,7 +1839,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
if (settling) {
setStateInternal(STATE_SETTLING);
// STATE_SETTLING won't animate the material shape, so do that here with the target state.
- updateDrawableForTargetState(state);
+ updateDrawableForTargetState(state, /* animate= */ true);
stateSettlingTracker.continueSettlingToState(state);
} else {
setStateInternal(state);
@@ -1741,11 +1930,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
}
}
- // MODIFICATION: Add isHideableWhenDragging method
- } else if (hideable && shouldHide(releasedChild, yvel) && isHideableWhenDragging()) {
+ } else if (hideable && shouldHide(releasedChild, yvel)) {
// Hide if the view was either released low or it was a significant vertical swipe
// otherwise settle to closest expanded state.
- if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD)
+ if ((Math.abs(xvel) < Math.abs(yvel) && yvel > significantVelocityThreshold)
|| releasedLow(releasedChild)) {
targetState = STATE_HIDDEN;
} else if (fitToContents) {
@@ -1814,9 +2002,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
- // MODIFICATION: Add isHideableWhenDragging method
return MathUtils.clamp(
- top, getExpandedOffset(), (hideable && isHideableWhenDragging()) ? parentHeight : collapsedOffset);
+ top,
+ getExpandedOffset(),
+ getViewVerticalDragRange(child));
}
@Override
@@ -1826,8 +2015,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
@Override
public int getViewVerticalDragRange(@NonNull View child) {
- // MODIFICATION: Add isHideableWhenDragging method
- if (hideable && isHideableWhenDragging()) {
+ if (canBeHiddenByDragging()) {
return parentHeight;
} else {
return collapsedOffset;
@@ -1838,8 +2026,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
void dispatchOnSlide(int top) {
View bottomSheet = viewRef.get();
if (bottomSheet != null && !callbacks.isEmpty()) {
- // MODIFICATION: Add calculateSlideOffset method
- float slideOffset = calculateSlideOffset(top);
+ float slideOffset = calculateSlideOffsetWithTop(top);
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onSlide(bottomSheet, slideOffset);
}
@@ -1898,7 +2085,8 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
/**
- * Checks whether hiding gestures should be enabled if {@code isHideable} is true.
+ * Checks whether hiding gestures should be enabled while {@code isHideable} is set to true.
+ *
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -1906,6 +2094,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
return true;
}
+ private boolean canBeHiddenByDragging() {
+ return isHideable() && isHideableWhenDragging();
+ }
+
/**
* Checks whether the bottom sheet should be expanded after it has been released after dragging.
*
@@ -2067,7 +2259,7 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
CoordinatorLayout.Behavior> behavior =
((CoordinatorLayout.LayoutParams) params).getBehavior();
if (!(behavior instanceof BackportBottomSheetBehavior)) {
- throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior");
+ throw new IllegalArgumentException("The view is not associated with BackportBottomSheetBehavior");
}
return (BackportBottomSheetBehavior) behavior;
}
@@ -2139,30 +2331,43 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
}
}
+ void setAccessibilityDelegateView(@Nullable View accessibilityDelegateView) {
+ if (accessibilityDelegateView == null && accessibilityDelegateViewRef != null) {
+ clearAccessibilityAction(
+ accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
+ accessibilityDelegateViewRef = null;
+ return;
+ }
+ accessibilityDelegateViewRef = new WeakReference<>(accessibilityDelegateView);
+ updateAccessibilityActions(accessibilityDelegateView, VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
+ }
+
private void updateAccessibilityActions() {
- if (viewRef == null) {
- return;
+ if (viewRef != null) {
+ updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET);
}
- V child = viewRef.get();
- if (child == null) {
- return;
+ if (accessibilityDelegateViewRef != null) {
+ updateAccessibilityActions(
+ accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
}
- ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
- ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND);
- ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS);
+ }
- if (expandHalfwayActionId != View.NO_ID) {
- ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId);
+ private void updateAccessibilityActions(View view, int viewIndex) {
+ if (view == null) {
+ return;
}
+ clearAccessibilityAction(view, viewIndex);
+
if (!fitToContents && state != STATE_HALF_EXPANDED) {
- expandHalfwayActionId =
+ expandHalfwayActionIds.put(
+ viewIndex,
addAccessibilityActionForState(
- child, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED);
+ view, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED));
}
- if (hideable && state != STATE_HIDDEN) {
+ if ((hideable && isHideableWhenDragging()) && state != STATE_HIDDEN) {
replaceAccessibilityActionForState(
- child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
+ view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
}
switch (state) {
@@ -2170,36 +2375,54 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
{
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED;
replaceAccessibilityActionForState(
- child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
+ view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
break;
}
case STATE_HALF_EXPANDED:
{
replaceAccessibilityActionForState(
- child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
+ view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
replaceAccessibilityActionForState(
- child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
+ view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
break;
}
case STATE_COLLAPSED:
{
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED;
replaceAccessibilityActionForState(
- child, AccessibilityActionCompat.ACTION_EXPAND, nextState);
+ view, AccessibilityActionCompat.ACTION_EXPAND, nextState);
break;
}
- default: // fall out
+ case STATE_HIDDEN:
+ case STATE_DRAGGING:
+ case STATE_SETTLING:
+ // Accessibility actions are not applicable, do nothing
+ }
+ }
+
+ private void clearAccessibilityAction(View view, int viewIndex) {
+ if (view == null) {
+ return;
+ }
+ ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
+ ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_EXPAND);
+ ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_DISMISS);
+
+ int expandHalfwayActionId = expandHalfwayActionIds.get(viewIndex, View.NO_ID);
+ if (expandHalfwayActionId != View.NO_ID) {
+ ViewCompat.removeAccessibilityAction(view, expandHalfwayActionId);
+ expandHalfwayActionIds.delete(viewIndex);
}
}
private void replaceAccessibilityActionForState(
- V child, AccessibilityActionCompat action, @State int state) {
+ View child, AccessibilityActionCompat action, @State int state) {
ViewCompat.replaceAccessibilityAction(
child, action, null, createAccessibilityViewCommandForState(state));
}
private int addAccessibilityActionForState(
- V child, @StringRes int stringResId, @State int state) {
+ View child, @StringRes int stringResId, @State int state) {
return ViewCompat.addAccessibilityAction(
child,
child.getResources().getString(stringResId),
@@ -2216,4 +2439,3 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
};
}
}
-
diff --git a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
deleted file mode 100644
index 26de5108b..000000000
--- a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.material.divider;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.ShapeDrawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.ColorRes;
-import androidx.annotation.DimenRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.Px;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.view.ViewCompat;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
-
-import com.google.android.material.R;
-import com.google.android.material.internal.ThemeEnforcement;
-import com.google.android.material.resources.MaterialResources;
-
-/**
- * MaterialDividerItemDecoration is a {@link RecyclerView.ItemDecoration}, similar to a {@link
- * androidx.recyclerview.widget.DividerItemDecoration}, that can be used as a divider between items of
- * a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and {@link #VERTICAL}
- * orientations.
- *
- *
- * dividerItemDecoration = new MaterialDividerItemDecoration(recyclerView.getContext(),
- * layoutManager.getOrientation());
- * recyclerView.addItemDecoration(dividerItemDecoration);
- *
- *
- * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
- * obtainable in the currently used MDC library.
- */
-public class BackportMaterialDividerItemDecoration extends ItemDecoration {
- public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
- public static final int VERTICAL = LinearLayout.VERTICAL;
-
- private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_MaterialDivider;
-
- @NonNull private Drawable dividerDrawable;
- private int thickness;
- @ColorInt private int color;
- private int orientation;
- private int insetStart;
- private int insetEnd;
- private boolean lastItemDecorated;
-
- private final Rect tempRect = new Rect();
-
- public BackportMaterialDividerItemDecoration(@NonNull Context context, int orientation) {
- this(context, null, orientation);
- }
-
- public BackportMaterialDividerItemDecoration(
- @NonNull Context context, @Nullable AttributeSet attrs, int orientation) {
- this(context, attrs, R.attr.materialDividerStyle, orientation);
- }
-
- public BackportMaterialDividerItemDecoration(
- @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int orientation) {
- TypedArray attributes =
- ThemeEnforcement.obtainStyledAttributes(
- context, attrs, R.styleable.MaterialDivider, defStyleAttr, DEF_STYLE_RES);
-
- color =
- MaterialResources.getColorStateList(
- context, attributes, R.styleable.MaterialDivider_dividerColor)
- .getDefaultColor();
- thickness =
- attributes.getDimensionPixelSize(
- R.styleable.MaterialDivider_dividerThickness,
- context.getResources().getDimensionPixelSize(R.dimen.material_divider_thickness));
- insetStart =
- attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetStart, 0);
- insetEnd = attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetEnd, 0);
- lastItemDecorated =
- attributes.getBoolean(R.styleable.MaterialDivider_lastItemDecorated, true);
-
- attributes.recycle();
-
- dividerDrawable = new ShapeDrawable();
- setDividerColor(color);
- setOrientation(orientation);
- }
-
- /**
- * Sets the orientation for this divider. This should be called if {@link
- * RecyclerView.LayoutManager} changes orientation.
- *
- * A {@link #HORIZONTAL} orientation will draw a vertical divider, and a {@link #VERTICAL}
- * orientation a horizontal divider.
- *
- * @param orientation The orientation of the {@link RecyclerView} this divider is associated with:
- * {@link #HORIZONTAL} or {@link #VERTICAL}
- */
- public void setOrientation(int orientation) {
- if (orientation != HORIZONTAL && orientation != VERTICAL) {
- throw new IllegalArgumentException(
- "Invalid orientation: " + orientation + ". It should be either HORIZONTAL or VERTICAL");
- }
- this.orientation = orientation;
- }
-
- public int getOrientation() {
- return orientation;
- }
-
- /**
- * Sets the thickness of the divider.
- *
- * @param thickness The thickness value to be set.
- * @see #getDividerThickness()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
- */
- public void setDividerThickness(@Px int thickness) {
- this.thickness = thickness;
- }
-
- /**
- * Sets the thickness of the divider.
- *
- * @param thicknessId The id of the thickness dimension resource to be set.
- * @see #getDividerThickness()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
- */
- public void setDividerThicknessResource(@NonNull Context context, @DimenRes int thicknessId) {
- setDividerThickness(context.getResources().getDimensionPixelSize(thicknessId));
- }
-
- /**
- * Returns the thickness set on the divider.
- *
- * @see #setDividerThickness(int)
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
- */
- @Px
- public int getDividerThickness() {
- return thickness;
- }
-
- /**
- * Sets the color of the divider.
- *
- * @param color The color to be set.
- * @see #getDividerColor()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
- */
- public void setDividerColor(@ColorInt int color) {
- this.color = color;
- dividerDrawable = DrawableCompat.wrap(dividerDrawable);
- DrawableCompat.setTint(dividerDrawable, color);
- }
-
- /**
- * Sets the color of the divider.
- *
- * @param colorId The id of the color resource to be set.
- * @see #getDividerColor()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
- */
- public void setDividerColorResource(@NonNull Context context, @ColorRes int colorId) {
- setDividerColor(ContextCompat.getColor(context, colorId));
- }
-
- /**
- * Returns the divider color.
- *
- * @see #setDividerColor(int)
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
- */
- @ColorInt
- public int getDividerColor() {
- return color;
- }
-
- /**
- * Sets the start inset of the divider.
- *
- * @param insetStart The start inset to be set.
- * @see #getDividerInsetStart()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
- */
- public void setDividerInsetStart(@Px int insetStart) {
- this.insetStart = insetStart;
- }
-
- /**
- * Sets the start inset of the divider.
- *
- * @param insetStartId The id of the inset dimension resource to be set.
- * @see #getDividerInsetStart()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
- */
- public void setDividerInsetStartResource(@NonNull Context context, @DimenRes int insetStartId) {
- setDividerInsetStart(context.getResources().getDimensionPixelOffset(insetStartId));
- }
-
- /**
- * Returns the divider's start inset.
- *
- * @see #setDividerInsetStart(int)
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
- */
- @Px
- public int getDividerInsetStart() {
- return insetStart;
- }
-
- /**
- * Sets the end inset of the divider.
- *
- * @param insetEnd The end inset to be set.
- * @see #getDividerInsetEnd()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
- */
- public void setDividerInsetEnd(@Px int insetEnd) {
- this.insetEnd = insetEnd;
- }
-
- /**
- * Sets the end inset of the divider.
- *
- * @param insetEndId The id of the inset dimension resource to be set.
- * @see #getDividerInsetEnd()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
- */
- public void setDividerInsetEndResource(@NonNull Context context, @DimenRes int insetEndId) {
- setDividerInsetEnd(context.getResources().getDimensionPixelOffset(insetEndId));
- }
-
- /**
- * Returns the divider's end inset.
- *
- * @see #setDividerInsetEnd(int)
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
- */
- @Px
- public int getDividerInsetEnd() {
- return insetEnd;
- }
-
- /**
- * Sets whether the class should draw a divider after the last item of a {@link RecyclerView}.
- *
- * @param lastItemDecorated whether there's a divider after the last item of a recycler view.
- * @see #isLastItemDecorated()
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_lastItemDecorated
- */
- public void setLastItemDecorated(boolean lastItemDecorated) {
- this.lastItemDecorated = lastItemDecorated;
- }
-
- /**
- * Whether there's a divider after the last item of a {@link RecyclerView}.
- *
- * @see #setLastItemDecorated(boolean)
- * @attr ref com.google.android.material.R.styleable#MaterialDivider_shouldDecorateLastItem
- */
- public boolean isLastItemDecorated() {
- return lastItemDecorated;
- }
-
- @Override
- public void onDraw(
- @NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
- if (parent.getLayoutManager() == null) {
- return;
- }
- if (orientation == VERTICAL) {
- drawForVerticalOrientation(canvas, parent);
- } else {
- drawForHorizontalOrientation(canvas, parent);
- }
- }
-
- /**
- * Draws a divider for the vertical orientation of the recycler view. The divider itself will be
- * horizontal.
- */
- private void drawForVerticalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
- canvas.save();
- int left;
- int right;
- if (parent.getClipToPadding()) {
- left = parent.getPaddingLeft();
- right = parent.getWidth() - parent.getPaddingRight();
- canvas.clipRect(
- left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
- } else {
- left = 0;
- right = parent.getWidth();
- }
- boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL;
- left += isRtl ? insetEnd : insetStart;
- right -= isRtl ? insetStart : insetEnd;
-
- int childCount = parent.getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = parent.getChildAt(i);
- if (shouldDrawDivider(parent, child)) {
- parent.getDecoratedBoundsWithMargins(child, tempRect);
- // Take into consideration any translationY added to the view.
- int bottom = tempRect.bottom + Math.round(child.getTranslationY());
- int top = bottom - thickness;
- dividerDrawable.setBounds(left, top, right, bottom);
- dividerDrawable.draw(canvas);
- }
- }
- canvas.restore();
- }
-
- /**
- * Draws a divider for the horizontal orientation of the recycler view. The divider itself will be
- * vertical.
- */
- private void drawForHorizontalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
- canvas.save();
- int top;
- int bottom;
- if (parent.getClipToPadding()) {
- top = parent.getPaddingTop();
- bottom = parent.getHeight() - parent.getPaddingBottom();
- canvas.clipRect(
- parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
- } else {
- top = 0;
- bottom = parent.getHeight();
- }
- top += insetStart;
- bottom -= insetEnd;
-
- int childCount = parent.getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = parent.getChildAt(i);
- if (shouldDrawDivider(parent, child)) {
- parent.getDecoratedBoundsWithMargins(child, tempRect);
- // Take into consideration any translationX added to the view.
- int right = tempRect.right + Math.round(child.getTranslationX());
- int left = right - thickness;
- dividerDrawable.setBounds(left, top, right, bottom);
- dividerDrawable.draw(canvas);
- }
- }
- canvas.restore();
- }
-
- @Override
- public void getItemOffsets(
- @NonNull Rect outRect,
- @NonNull View view,
- @NonNull RecyclerView parent,
- @NonNull RecyclerView.State state) {
- outRect.set(0, 0, 0, 0);
- // Only add offset if there's a divider displayed.
- if (shouldDrawDivider(parent, view)) {
- if (orientation == VERTICAL) {
- outRect.bottom = thickness;
- } else {
- outRect.right = thickness;
- }
- }
- }
-
- private boolean shouldDrawDivider(@NonNull RecyclerView parent, @NonNull View child) {
- int position = parent.getChildAdapterPosition(child);
- RecyclerView.Adapter> adapter = parent.getAdapter();
- boolean isLastItem = adapter != null && position == adapter.getItemCount() - 1;
-
- return position != RecyclerView.NO_POSITION
- && (!isLastItem || lastItemDecorated)
- && shouldDrawDivider(position, adapter);
- }
-
- /**
- * Whether a divider should be drawn below the current item that is being drawn.
- *
- *
Note: if lasItemDecorated is false, the divider below the last item will never be drawn even
- * if this method returns true.
- *
- * @param position the position of the current item being drawn.
- * @param adapter the {@link RecyclerView.Adapter} associated with the item being drawn.
- */
- protected boolean shouldDrawDivider(int position, @Nullable RecyclerView.Adapter> adapter) {
- return true;
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
index b2912595c..1ec38068e 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
@@ -48,6 +48,8 @@ import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
+import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
+import org.oxycblt.auxio.image.extractor.SquareCropTransformation
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@@ -77,6 +79,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
FrameLayout(context, attrs, defStyleAttr) {
@Inject lateinit var imageLoader: ImageLoader
@Inject lateinit var uiSettings: UISettings
+ @Inject lateinit var imageSettings: ImageSettings
private val image: ImageView
@@ -384,13 +387,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
ImageRequest.Builder(context)
.data(songs)
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
- .transformations(
- RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f))
.target(image)
- .build()
+
+ val cornersTransformation =
+ RoundedRectTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f)
+ if (imageSettings.forceSquareCovers) {
+ request.transformations(SquareCropTransformation.INSTANCE, cornersTransformation)
+ } else {
+ request.transformations(cornersTransformation)
+ }
+
// Dispose of any previous image request and load a new image.
CoilUtils.dispose(image)
- imageLoader.enqueue(request)
+ imageLoader.enqueue(request.build())
contentDescription = desc
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
index a4c13c4c9..1a9a01b24 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
@@ -34,6 +34,8 @@ import org.oxycblt.auxio.util.logD
interface ImageSettings : Settings {
/** The strategy to use when loading album covers. */
val coverMode: CoverMode
+ /** Whether to force all album covers to have a 1:1 aspect ratio. */
+ val forceSquareCovers: Boolean
interface Listener {
/** Called when [coverMode] changes. */
@@ -49,6 +51,9 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.MEDIA_STORE
+ override val forceSquareCovers: Boolean
+ get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
+
override fun migrate() {
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
index 6ca126256..537d8e874 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
@@ -43,7 +43,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import javax.inject.Inject
-import kotlin.math.min
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.withContext
@@ -155,7 +154,7 @@ constructor(
// Get the embedded picture from MediaMetadataRetriever, which will return a full
// ByteArray of the cover without any compression artifacts.
// If its null [i.e there is no embedded cover], than just ignore it and move on
- return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
+ embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
}
private suspend fun extractExoplayerCover(album: Album): InputStream? {
@@ -212,7 +211,7 @@ constructor(
}
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
- private fun createMosaic(streams: List, size: Size): FetchResult {
+ private suspend fun createMosaic(streams: List, size: Size): FetchResult {
// Use whatever size coil gives us to create the mosaic.
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize =
@@ -234,7 +233,9 @@ constructor(
// Crop the bitmap down to a square so it leaves no empty space
// TODO: Work around this
- val bitmap = cropBitmap(BitmapFactory.decodeStream(stream), mosaicFrameSize)
+ val bitmap =
+ SquareCropTransformation.INSTANCE.transform(
+ BitmapFactory.decodeStream(stream), mosaicFrameSize)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width
@@ -259,21 +260,4 @@ constructor(
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
-
- private fun cropBitmap(input: Bitmap, size: Size): Bitmap {
- // Find the smaller dimension and then take a center portion of the image that
- // has that size.
- val dstSize = min(input.width, input.height)
- val x = (input.width - dstSize) / 2
- val y = (input.height - dstSize) / 2
- val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
-
- val desiredWidth = size.width.pxOrElse { dstSize }
- val desiredHeight = size.height.pxOrElse { dstSize }
- if (dstSize != desiredWidth || dstSize != desiredHeight) {
- // Image is not the desired size, upscale it.
- return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
- }
- return dst
- }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt
similarity index 96%
rename from app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt
rename to app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt
index c25770ec4..c8d3ee145 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2023 Auxio Project
- * RoundedCornersTransformation.kt is part of Auxio.
+ * RoundedRectTransformation.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.image
+package org.oxycblt.auxio.image.extractor
import android.graphics.Bitmap
import android.graphics.Bitmap.createBitmap
@@ -43,7 +43,7 @@ import kotlin.math.roundToInt
*
* @author Coil Team, Alexander Capehart (OxygenCobalt)
*/
-class RoundedCornersTransformation(
+class RoundedRectTransformation(
@Px private val topLeft: Float = 0f,
@Px private val topRight: Float = 0f,
@Px private val bottomLeft: Float = 0f,
@@ -122,7 +122,7 @@ class RoundedCornersTransformation(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- return other is RoundedCornersTransformation &&
+ return other is RoundedRectTransformation &&
topLeft == other.topLeft &&
topRight == other.topRight &&
bottomLeft == other.bottomLeft &&
diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt
new file mode 100644
index 000000000..57f03dbef
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareCropTransformation.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * SquareCropTransformation.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.image.extractor
+
+import android.graphics.Bitmap
+import coil.size.Size
+import coil.size.pxOrElse
+import coil.transform.Transformation
+import kotlin.math.min
+
+/**
+ * A [Transformation] that performs a center crop-style transformation on an image. Allowing this
+ * behavior to be intrinsic without any view configuration.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+class SquareCropTransformation : Transformation {
+ override val cacheKey: String
+ get() = "SquareCropTransformation"
+
+ override suspend fun transform(input: Bitmap, size: Size): Bitmap {
+ // Find the smaller dimension and then take a center portion of the image that
+ // has that size.
+ val dstSize = min(input.width, input.height)
+ val x = (input.width - dstSize) / 2
+ val y = (input.height - dstSize) / 2
+ val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
+
+ val desiredWidth = size.width.pxOrElse { dstSize }
+ val desiredHeight = size.height.pxOrElse { dstSize }
+ if (dstSize != desiredWidth || dstSize != desiredHeight) {
+ // Image is not the desired size, upscale it.
+ return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
+ }
+ return dst
+ }
+
+ companion object {
+ /** A re-usable instance. */
+ val INSTANCE = SquareCropTransformation()
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index 513786f87..6e45c5ae9 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -23,8 +23,6 @@ import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import java.util.LinkedList
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -339,40 +337,56 @@ constructor(
}
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) =
- worker.scope.launch {
- try {
- val start = System.currentTimeMillis()
- indexImpl(worker, withCache)
- logD(
- "Music indexing completed successfully in " +
- "${System.currentTimeMillis() - start}ms")
- } catch (e: CancellationException) {
- // Got cancelled, propagate upwards to top-level co-routine.
- logD("Loading routine was cancelled")
- throw e
- } catch (e: Exception) {
- // Music loading process failed due to something we have not handled.
- logE("Music indexing failed")
- logE(e.stackTraceToString())
- emitIndexingCompletion(e)
- }
+ worker.scope.launch { indexWrapper(worker, withCache) }
+
+ private suspend fun indexWrapper(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
+ try {
+ indexImpl(worker, withCache)
+ } catch (e: CancellationException) {
+ // Got cancelled, propagate upwards to top-level co-routine.
+ logD("Loading routine was cancelled")
+ throw e
+ } catch (e: Exception) {
+ // Music loading process failed due to something we have not handled.
+ // TODO: Still want to display this error eventually
+ logE("Music indexing failed")
+ logE(e.stackTraceToString())
+ emitIndexingCompletion(e)
}
+ }
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
+ val start = System.currentTimeMillis()
+ // Make sure we have permissions before going forward. Theoretically this would be better
+ // done at the UI level, but that intertwines logic and display too much.
if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) {
logE("Permissions were not granted")
- // No permissions, signal that we can't do anything.
throw NoAudioPermissionException()
}
- // Start initializing the extractors. Use an indeterminate state, as there is no ETA on
- // how long a media database query will take.
- emitIndexingProgress(IndexingProgress.Indeterminate)
-
- // Do the initial query of the cache and media databases in parallel.
+ // Begin with querying MediaStore and the music cache. The former is needed for Auxio
+ // to figure out what songs are (probably) on the device, and the latter will be needed
+ // for discovery (described later). These have no shared state, so they are done in
+ // parallel.
logD("Starting MediaStore query")
- 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 =
if (withCache) {
logD("Reading cache")
@@ -383,59 +397,121 @@ constructor(
logD("Awaiting MediaStore query")
val query = mediaStoreQueryJob.await().getOrThrow()
- // Now start processing the queried song information in parallel. Songs that can't be
- // received from the cache are consisted incomplete and pushed to a separate channel
- // that will eventually be processed into completed raw songs.
- logD("Starting song discovery")
- val completeSongs = Channel(Channel.UNLIMITED)
- val incompleteSongs = Channel(Channel.UNLIMITED)
- val processedSongs = Channel(Channel.UNLIMITED)
- logD("Started MediaStore discovery")
+ // We now have all the information required to start the "discovery" process. This
+ // is the point at which Auxio starts scanning each file given from MediaStore and
+ // transforming it into a music library. MediaStore normally
+ logD("Starting discovery")
+ val incompleteSongs = Channel(Channel.UNLIMITED) // Not fully populated w/metadata
+ val completeSongs = Channel(Channel.UNLIMITED) // Populated with quality metadata
+ val processedSongs = Channel(Channel.UNLIMITED) // Transformed into SongImpl
+
+ // MediaStoreExtractor discovers all music on the device, and forwards them to either
+ // DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata
+ // does not exist. In the latter situation, it also applies it's own (inferior) metadata.
+ logD("Starting MediaStore discovery")
val mediaStoreJob =
- worker.scope.tryAsync {
- mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
+ worker.scope.async {
+ try {
+ mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
+ } catch (e: Exception) {
+ // To prevent a deadlock, we want to close the channel with an exception
+ // to cascade to and cancel all other routines before finally bubbling up
+ // to the main extractor loop.
+ incompleteSongs.close(e)
+ return@async
+ }
incompleteSongs.close()
}
- logD("Started ExoPlayer discovery")
- val metadataJob =
- worker.scope.tryAsync {
- tagExtractor.consume(incompleteSongs, completeSongs)
+
+ // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date
+ // metadata for them, and then forwards it to DeviceLibrary.
+ logD("Starting tag extraction")
+ val tagJob =
+ worker.scope.async {
+ try {
+ tagExtractor.consume(incompleteSongs, completeSongs)
+ } catch (e: Exception) {
+ completeSongs.close(e)
+ return@async
+ }
completeSongs.close()
}
+
+ // DeviceLibrary constructs music parent instances as song information is provided,
+ // and then forwards them to the primary loading loop.
logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
- worker.scope.tryAsync(Dispatchers.Default) {
- deviceLibraryFactory.create(completeSongs, processedSongs).also {
- processedSongs.close()
- }
+ worker.scope.async(Dispatchers.Default) {
+ val deviceLibrary =
+ try {
+ deviceLibraryFactory.create(completeSongs, processedSongs)
+ } catch (e: Exception) {
+ processedSongs.close(e)
+ return@async Result.failure(e)
+ }
+ processedSongs.close()
+ Result.success(deviceLibrary)
}
- // Await completed raw songs as they are processed.
+ // We could keep track of a total here, but we also need to collate this RawSong information
+ // for when we write the cache later on in the finalization step.
val rawSongs = LinkedList()
for (rawSong in processedSongs) {
rawSongs.add(rawSong)
+ // Since discovery takes up the bulk of the music loading process, we switch to
+ // indicating a defined amount of loaded songs in comparison to the projected amount
+ // of songs that were queried.
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
}
- logD("Awaiting discovery completion")
- // These should be no-ops, but we need the error state to see if we should keep going.
- mediaStoreJob.await().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()) {
logE("Music library was empty")
throw NoMusicException()
}
- // Successfully loaded the library, now save the cache and read playlist information
- // in parallel.
+ // Now that the library is effectively loaded, we can start the finalization step, which
+ // involves writing new cache information and creating more music data that is derived
+ // from the library (e.g playlists)
logD("Discovered ${rawSongs.size} songs, starting finalization")
+
+ // We have no idea how long the cache will take, and the playlist construction
+ // will be too fast to indicate, so switch back to an indeterminate state.
emitIndexingProgress(IndexingProgress.Indeterminate)
+
+ // The UserLibrary job is split into a query and construction step, a la MediaStore.
+ // This way, we can start working on playlists even as DeviceLibrary might still be
+ // working on parent information.
logD("Starting UserLibrary query")
- val userLibraryQueryJob = worker.scope.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) {
logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs)
}
+
+ // Create UserLibrary once we finally get the required components for it.
logD("Awaiting UserLibrary query")
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
logD("Awaiting DeviceLibrary creation")
@@ -443,14 +519,20 @@ constructor(
logD("Starting UserLibrary creation")
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)
- // Comparing the library instances is obscenely expensive, do it within the library
-
val deviceLibraryChanged: Boolean
val userLibraryChanged: Boolean
+ // We want to make sure that all reads and writes are synchronized due to the sheer
+ // amount of consumers of MusicRepository.
+ // TODO: Would Atomics not be a better fit here?
synchronized(this) {
+ // It's possible that this reload might have changed nothing, so make sure that
+ // hasn't happened before dispatching a change to all consumers.
deviceLibraryChanged = this.deviceLibrary != deviceLibrary
userLibraryChanged = this.userLibrary != userLibrary
if (!deviceLibraryChanged && !userLibraryChanged) {
@@ -462,27 +544,13 @@ constructor(
this.userLibrary = userLibrary
}
+ // Consumers expect their updates to be on the main thread (notably PlaybackService),
+ // so switch to it.
withContext(Dispatchers.Main) {
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged)
}
}
- /**
- * An extension of [async] that forces the outcome to a [Result] to allow exceptions to bubble
- * upwards instead of crashing the entire app.
- */
- private inline fun CoroutineScope.tryAsync(
- context: CoroutineContext = EmptyCoroutineContext,
- crossinline block: suspend () -> R
- ) =
- async(context) {
- try {
- Result.success(block())
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
yield()
synchronized(this) {
diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
index 9cb4d70b5..354f6520f 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
@@ -151,6 +151,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
rawArtists =
rawAlbumArtists
.ifEmpty { rawIndividualArtists }
+ .distinctBy { it.key }
.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].
*/
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
@@ -169,6 +173,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
rawSong.genreNames
.parseId3GenreNames(musicSettings)
.map { RawGenre(it) }
+ .distinctBy { it.key }
.ifEmpty { listOf(RawGenre()) }
/**
@@ -207,6 +212,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
checkNotNull(_album) { "Malformed song: No album" }
check(_artists.isNotEmpty()) { "Malformed song: No artists" }
+ check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" }
for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata.
@@ -217,6 +223,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
}
check(_genres.isNotEmpty()) { "Malformed song: No genres" }
+ check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch" }
for (i in _genres.indices) {
// Non-destructively reorder the linked genres so that they align with
// the genre ordering within the song metadata.
@@ -334,6 +341,7 @@ class AlbumImpl(
fun finalize(): Album {
check(songs.isNotEmpty()) { "Malformed album: Empty" }
check(_artists.isNotEmpty()) { "Malformed album: No artists" }
+ check(_artists.size == rawArtists.size) { "Malformed album: Artist grouping mismatch" }
for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata.
diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt
index 42e29eed2..11a357f45 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt
@@ -364,7 +364,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
arrayOf(
MediaStore.Audio.AudioColumns.TRACK,
// Below API 29, we are restricted to the absolute path (Called DATA by
- // MedaStore) when working with audio files.
+ // MediaStore) when working with audio files.
MediaStore.Audio.AudioColumns.DATA)
// The selector should be configured to convert the given directories instances to their
diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
index 5d9eebcbc..fae02585e 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
@@ -149,18 +149,22 @@ private class TagWorkerImpl(
// Artist
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
- (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])
+ (textFrames["TXXX:artistssort"]
+ ?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"]
+ ?: textFrames["TSOP"])
?.let { rawSong.artistSortNames = it }
// Album artist
textFrames["TXXX:musicbrainz album artist id"]?.let {
rawSong.albumArtistMusicBrainzIds = it
}
- (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
- rawSong.albumArtistNames = it
- }
+ (textFrames["TXXX:albumartists"]
+ ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"]
+ ?: textFrames["TPE2"])
+ ?.let { rawSong.albumArtistNames = it }
(textFrames["TXXX:albumartistssort"]
- ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"]
+ ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"]
+ ?: textFrames["TXXX:albumartistsort"]
// This is a non-standard iTunes extension
?: textFrames["TSO2"])
?.let { rawSong.albumArtistSortNames = it }
@@ -261,17 +265,19 @@ private class TagWorkerImpl(
// Artist
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
- (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let {
- rawSong.artistSortNames = it
- }
+ (comments["artistssort"]
+ ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"])
+ ?.let { rawSong.artistSortNames = it }
// Album artist
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
- (comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let {
- rawSong.albumArtistNames = it
- }
+ (comments["albumartists"]
+ ?: comments["album_artists"] ?: comments["album artists"]
+ ?: comments["albumartist"])
+ ?.let { rawSong.albumArtistNames = it }
(comments["albumartistssort"]
- ?: comments["albumartists_sort"] ?: comments["albumartistsort"])
+ ?: comments["albumartists_sort"] ?: comments["albumartists sort"]
+ ?: comments["albumartistsort"])
?.let { rawSong.albumArtistSortNames = it }
// Genre
diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt
index bb1dda066..5e0e7ca55 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt
@@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.logE
* @author Alexander Capehart
*
* TODO: Communicate errors
+ * TODO: How to handle empty playlists that appear because all of their songs have disappeared?
*/
interface UserLibrary {
/** The current user-defined playlists. */
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index 26629030b..ffcc84b41 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -25,6 +25,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.media.audiofx.AudioEffect
+import android.os.Build
import android.os.IBinder
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
@@ -150,8 +151,8 @@ class PlaybackService :
playbackManager.registerInternalPlayer(this)
musicRepository.addUpdateListener(this)
mediaSessionComponent.registerListener(this)
- registerReceiver(
- systemReceiver,
+
+ val intentFilter =
IntentFilter().apply {
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG)
@@ -162,7 +163,20 @@ class PlaybackService :
addAction(ACTION_SKIP_NEXT)
addAction(ACTION_EXIT)
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
- })
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ registerReceiver(
+ systemReceiver,
+ intentFilter,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ RECEIVER_NOT_EXPORTED
+ } else {
+ 0
+ })
+ } else {
+ registerReceiver(systemReceiver, intentFilter)
+ }
logD("Service created")
}
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt
index 947362fe7..8db338dd5 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt
@@ -94,11 +94,11 @@ interface Settings {
final override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
- key: String
+ key: String?
) {
// FIXME: Settings initialization firing the listener.
logD("Dispatching settings change $key")
- onSettingChanged(key, unlikelyToBeNull(listener))
+ onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener))
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
index b0b59d02b..9e1e83af5 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
@@ -47,7 +47,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
}
override fun onSetupPreference(preference: Preference) {
- if (preference.key == getString(R.string.set_key_cover_mode)) {
+ if (preference.key == getString(R.string.set_key_cover_mode) ||
+ preference.key == getString(R.string.set_key_square_covers)) {
logD("Configuring cover mode setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
index ff54bcfac..c959d0b3e 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
@@ -26,11 +26,11 @@ import androidx.preference.PreferenceGroupAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R
-import com.google.android.material.divider.BackportMaterialDividerItemDecoration
+import com.google.android.material.divider.MaterialDividerItemDecoration
/**
- * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
- * separate preference categories.
+ * A [MaterialDividerItemDecoration] that sets up the divider configuration to correctly separate
+ * preference categories.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@@ -41,7 +41,7 @@ constructor(
attributeSet: AttributeSet? = null,
defStyleAttr: Int = R.attr.materialDividerStyle,
orientation: Int = LinearLayoutManager.VERTICAL
-) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
+) : MaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
@SuppressLint("RestrictedApi")
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) =
try {
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
index 1cb3f599c..451dcabd7 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
@@ -27,7 +27,8 @@ import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.ImageSettings
-import org.oxycblt.auxio.image.RoundedCornersTransformation
+import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
+import org.oxycblt.auxio.image.extractor.SquareCropTransformation
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue
@@ -98,10 +99,19 @@ constructor(
return if (cornerRadius > 0) {
// If rounded, reduce the bitmap size further to obtain more pronounced
// rounded corners.
- builder
- .size(getSafeRemoteViewsImageSize(context, 10f))
- .transformations(RoundedCornersTransformation(cornerRadius.toFloat()))
+ builder.size(getSafeRemoteViewsImageSize(context, 10f))
+ val cornersTransformation =
+ RoundedRectTransformation(cornerRadius.toFloat())
+ if (imageSettings.forceSquareCovers) {
+ builder.transformations(
+ SquareCropTransformation.INSTANCE, cornersTransformation)
+ } else {
+ builder.transformations(cornersTransformation)
+ }
} else {
+ if (imageSettings.forceSquareCovers) {
+ builder.transformations(SquareCropTransformation.INSTANCE)
+ }
builder.size(getSafeRemoteViewsImageSize(context))
}
}
diff --git a/app/src/main/res/drawable/ic_shuffle_off_24.xml b/app/src/main/res/drawable/ic_shuffle_off_24.xml
index ec36a047c..d73a3c489 100644
--- a/app/src/main/res/drawable/ic_shuffle_off_24.xml
+++ b/app/src/main/res/drawable/ic_shuffle_off_24.xml
@@ -2,10 +2,10 @@
-
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+
diff --git a/app/src/main/res/drawable/ic_shuffle_on_24.xml b/app/src/main/res/drawable/ic_shuffle_on_24.xml
index 8a64e052f..1e4330f55 100644
--- a/app/src/main/res/drawable/ic_shuffle_on_24.xml
+++ b/app/src/main/res/drawable/ic_shuffle_on_24.xml
@@ -1,10 +1,11 @@
+
-
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorPrimary">
+
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 561b05892..ed22aa75f 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -293,4 +293,6 @@
Няма дыска
З\'яўляецца на
Рэдагаванне %s
+ Выкарыстоўваць квадратныя вокладкі альбомаў
+ Абрэзаць усе вокладкі альбомаў да суадносін бакоў 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 956f74699..6abd46909 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -55,9 +55,9 @@
Nastavení
Vzhled a chování
Motiv
- Automatické
- Světlé
- Tmavé
+ Automaticky
+ Světlý
+ Tmavý
Barevné schéma
Černý motiv
Použít kompletně černý tmavý motiv
@@ -304,4 +304,6 @@
Žádný disk
Úprava seznamu %s
Sdílet
+ Vynutit čtvercové obaly alb
+ Oříznout všechny covery alb na poměr stran 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 03ca85350..2f944921a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -295,4 +295,6 @@
Keine Disc
Erscheint in
%s bearbeiten
+ Quadratische Album-Cover erzwingen
+ Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 3a7e94380..9fd873733 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -299,4 +299,6 @@
Aparece en
Compartir
Sin disco
+ Carátula del álbum Force Square
+ Recorta todas las portadas de los álbumes a una relación de aspecto 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 55adf537f..ce20da39c 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -297,4 +297,6 @@
Codage audio avancé (AAC)
Aucun disque
%1$s, %2$s
+ Forcer les pochettes d\'album carrées
+ Recadrer toutes les pochettes d\'album au format 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 7382cab1d..4809c780e 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -290,4 +290,6 @@
Sudjelovanja:
Dijeli
Nema diska
+ Prisili kvadratične omote albuma
+ Odreži sve omote albuma na omjer 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index f51c29ee1..592ef9dc7 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -180,7 +180,7 @@
ਕੌਮਾ (,)
ਸੈਮੀਕੋਲਨ (;)
ਸਲੈਸ਼ (/)
- Ampersand (&)
+ ਐਂਪਰਸੈਂਡ (&)
ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ
ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ
ਪਲੇਅਬੈਕ
@@ -219,7 +219,7 @@
Matroska ਆਡੀਓ
ਗੂੜ੍ਹਾ ਜ੍ਹਾਮਣੀ
Ogg ਆਡੀਓ
- % d: ਗੀਤ ਲੋਡ ਕੀਤੇ
+ %d: ਗੀਤ ਲੋਡ ਕੀਤੇ
- %d ਐਲਬਮ
- %d ਐਲਬਮਾਂ
@@ -287,4 +287,6 @@
ਡਾਇਨੈਮਿਕ
%s ਸੋਧ ਰਿਹਾ
ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)
+ ਵਰਗੀਕ੍ਰਿਤ ਐਲਬਮ ਕਵਰ ਫੋਰਸ ਕਰੋ
+ ਸਾਰੇ ਐਲਬਮ ਕਵਰਾਂ ਨੂੰ 1:1 ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਤੱਕ ਕਾਂਟ-ਛਾਂਟ ਕਰੋ
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8016b72f2..d433b61c4 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -40,7 +40,7 @@
Nie znaleziono utworów
Utwór %d
- Odtwórz bądź zapauzuj
+ Odtwórz albo zapauzuj
Szukaj w bibliotece…
@@ -102,7 +102,7 @@
Minialbum koncertowy
Minialbum z remiksami
Koncertowy singiel
- Remiks
+ Remix
Kompilacje
Kompilacja
Ścieżki dźwiękowe
@@ -139,7 +139,7 @@
Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne
Usuń utwór z kolejki
Preferuj album
- Automatycznie odśwież
+ Automatyczne odświeżanie
FLAC
Et (&)
Nie udało się zaimportować utworów
@@ -243,7 +243,7 @@
Wyłączone
Niska jakość
Wysoka jakość
- Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów - tak, jakby posiadały wiele wartości. Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami traktowanymi jako oddzielające.
+ Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów (tak, jakby posiadały wiele wartości). Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami oddzielającymi.
Dostosuj motyw i kolory aplikacji
Ukryj wykonawców uczestniczących
Zarządzaj importowaniem muzyki i obrazów
@@ -254,7 +254,7 @@
Foldery
Stan odtwarzania
Obrazy
- Zarządzanie dźwiękiem i odtwarzaniem muzyki
+ Zarządzaj dźwiękiem i odtwarzaniem muzyki
Odtwórz wybrane
Wybrane losowo
Wybrano %d
@@ -288,16 +288,18 @@
Brak utworów
Dodano do playlisty
Playlista %d
- Usuwać
+ Usuń
Usunąć %s\? Tego nie da się cofnąć.
- Przemianować
- Przemianować playlistę
+ Zmień nazwę
+ Zmień nazwę playlisty
Usunąć playlistę\?
- Edytować
- Pojawia się
- Udział
+ Edytuj
+ Pojawia się na
+ Udostępnij
Zmieniono nazwę playlisty
- Playlista usunięta
- Brak dysku
+ Usunięto playlistę
+ Brak nr. płyty
Edytowanie %s
+ Przytnij okładki do formatu 1:1
+ Wymuś kwadratowe okładki
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 6091d3ea4..43daae2e2 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -302,4 +302,6 @@
Редактирование %s
Нет диска
Поделиться
+ Использовать квадратные обложки альбомов
+ Обрезать все обложки альбомов до соотношения сторон 1:1
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
new file mode 100644
index 000000000..7acaab1b1
--- /dev/null
+++ b/app/src/main/res/values-sv/strings.xml
@@ -0,0 +1,156 @@
+
+
+ Försök igen
+ Musik laddar
+ Laddar musik
+ Alla låtar
+ Album
+ Albumet
+ Remix-album
+ EP
+ EP
+ Live-EP
+ Remix-EP
+ Singlar
+ Remix-singel
+ Sammanställning
+ Remix-sammanställning
+ Ljudspår
+ Ljudspår
+ Blandband
+ DJ-mixar
+ Live
+ Remixar
+ Framträder på
+ Konstnär
+ Konstnär
+ Genrer
+ Spellista
+ Spellistor
+ Ny spellista
+ Byt namn på spellista
+ Ta bort spellista\?
+ Sök
+ Filtrera
+ Namn
+ Datum
+ Längd
+ Antal låtar
+ Spår
+ Datum tillagt
+ Stigande
+ Fallande
+ Nu spelar
+ Utjämnare
+ Spela
+ Spela utvalda
+ Blanda
+ Kö
+ Spela nästa
+ Lägg till spellista
+ Gå till konstnär
+ Gå till album
+ Visa egenskaper
+ Dela
+ Egenskaper för låt
+ Överordnad mapp
+ Format
+ Storlek
+ Samplingsfrekvens
+ Blanda
+ Blanda alla
+ Okej
+ Avbryt
+ Spara
+ Tillstånd återstallde
+ Om
+ Källkod
+ Wiki
+ Licenser
+ Visa och kontrollera musikuppspelning
+ Laddar ditt musikbibliotek…
+ Övervakning ditt musikbibliotek för ändringar…
+ Tillagd till kö
+ Spellista skapade
+ Tillagd till spellista
+ Sök ditt musikbibliotek…
+ Inställningar
+ Utseende
+ Ändra tema och färger på appen
+ Automatisk
+ Ljust
+ Svart tema
+ Rundläge
+ Bevilja
+ En enkel, rationell musikspelare för Android.
+ Övervakar musikbiblioteket
+ Låtar
+ Live-album
+ Ta bort
+ Live-sammanställning
+ Singel
+ Live-singel
+ Sammanställningar
+ Blandband
+ DJ-mix
+ Genre
+ Byt namn
+ Redigera
+ Alla
+ Disk
+ Sortera
+ Blanda utvalda
+ Lägg till kö
+ Filnamn
+ Lägg till
+ Tillstånd tog bort
+ Bithastighet
+ Återställ
+ Tillstånd sparat
+ Version
+ Statistik över beroende
+ Bytt namn av spellista
+ Spellista tog bort
+ Utvecklad av Alexander Capeheart
+ Tema
+ Mörkt
+ Färgschema
+ Använda rent svart för det mörka temat
+ Aktivera rundade hörn på ytterligare element i användargränssnittet (kräver att albumomslag är rundade)
+ Anpassa
+ Ändra synlighet och ordningsföljd av bibliotekflikar
+ Anpassad åtgärd för uppspelningsfält
+ Anpassad aviseringsåtgärd
+ Hoppa till nästa
+ Upprepningsmodus
+ Beteende
+ När spelar från artikeluppgifter
+ Spela från genre
+ Komma ihåg blandningsstatus
+ Behåll blandning på när spelar en ny låt
+ Kontent
+ Kontrollera hur musik och bilar laddas
+ Musik
+ Automatisk omladdning
+ Inkludera bara musik
+ Ignorera ljudfiler som inte är musik, t.ex. podkaster
+ Värdeavskiljare
+ Plus (+)
+ Intelligent sortering
+ Sorterar namn som börjar med siffror eller ord som \"the\" korrekt (fungerar bäst med engelskspråkig music)
+ Dölj medarbetare
+ Skärm
+ Bibliotekflikar
+ När spelar från biblioteket
+ Spela från visad artikel
+ Spela från alla låtar
+ Spela från konstnär
+ Spela från album
+ Semikolon (;)
+ Ladda om musikbiblioteket när det ändras (kräver permanent meddelande)
+ Komma (,)
+ Snedstreck (/)
+ Konfigurera tecken som separerar flera värden i taggar
+ Advarsel: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\).
+ Anpassa UI-kontroller och beteende
+
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index a9a5c1e6a..8f7435242 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -299,4 +299,6 @@
Редагування %s
Немає диску
З\'являється на
+ Обрізання обкладинки альбомів до співвідношення сторін 1:1
+ Примусові квадратні обкладинки
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 52243b67a..4a5290943 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -293,4 +293,6 @@
分享
无唱片
正在编辑 %s
+ 强制使用方形专辑封面
+ 将所有专辑封面裁剪至 1:1 宽高比
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 7dffd1917..5295acb05 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -20,7 +20,6 @@
16dp
24dp
-
48dp
56dp
64dp
diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml
index faab5d5cd..f3c956766 100644
--- a/app/src/main/res/values/settings.xml
+++ b/app/src/main/res/values/settings.xml
@@ -15,6 +15,7 @@
auxio_observing
auxio_music_dirs
auxio_cover_mode
+ auxio_square_covers
auxio_include_dirs
auxio_exclude_non_music
auxio_separators
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 676133e13..95e6c348e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -239,6 +239,8 @@
Off
Fast
High quality
+ Force square album covers
+ Crop all album covers to a 1:1 aspect ratio
Audio
Configure sound and playback behavior
diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml
index 97611ce8d..5ee16b401 100644
--- a/app/src/main/res/values/styles_ui.xml
+++ b/app/src/main/res/values/styles_ui.xml
@@ -216,7 +216,7 @@
- @dimen/spacing_small
- @dimen/spacing_small
- @dimen/spacing_small
-
+
- @color/m3_text_button_foreground_color_selector
diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml
index a46bb1025..86a3a6b03 100644
--- a/app/src/main/res/xml/preferences_music.xml
+++ b/app/src/main/res/xml/preferences_music.xml
@@ -43,5 +43,11 @@
app:key="@string/set_key_cover_mode"
app:title="@string/set_cover_mode" />
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index dbb61d537..5c4648215 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
buildscript {
ext {
- kotlin_version = '1.8.21'
- navigation_version = "2.5.3"
+ kotlin_version = '1.8.22'
+ navigation_version = "2.6.0"
hilt_version = '2.46.1'
}
diff --git a/fastlane/metadata/android/en-US/changelogs/33.txt b/fastlane/metadata/android/en-US/changelogs/33.txt
new file mode 100644
index 000000000..5915176d0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/33.txt
@@ -0,0 +1,4 @@
+Auxio 3.1.0 introduces playlisting functionality, with more features coming soon.
+This release fixes an issue where some users would experience an infinite loading
+screen, along other quality-of-life improvements.
+For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.3.
\ No newline at end of file
diff --git a/fastlane/metadata/android/sv/short_description.txt b/fastlane/metadata/android/sv/short_description.txt
new file mode 100644
index 000000000..107ba2121
--- /dev/null
+++ b/fastlane/metadata/android/sv/short_description.txt
@@ -0,0 +1 @@
+En enkel, rationell musikspelare