app: cleanup

This commit is contained in:
Alexander Capehart 2025-03-17 08:12:39 -06:00
parent 343856ac69
commit aac6d8ef4d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
38 changed files with 76 additions and 228 deletions

View file

@ -18,8 +18,8 @@ android {
defaultConfig { defaultConfig {
applicationId namespace applicationId namespace
versionName "4.0.2" versionName "4.0.3"
versionCode 61 versionCode 62
minSdk min_sdk minSdk min_sdk
targetSdk target_sdk targetSdk target_sdk

View file

@ -1309,7 +1309,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
+ " should not be set externally."); + " should not be set externally.");
} }
if (!hideable && state == STATE_HIDDEN) { if (!hideable && state == STATE_HIDDEN) {
Log.w(TAG, "Cannot set state: " + state);
return; return;
} }
final int finalState; final int finalState;

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio package org.oxycblt.auxio
import android.animation.ValueAnimator
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
@ -514,8 +513,6 @@ class MainFragment :
} }
} }
private var scrimAnimator: ValueAnimator? = null
private fun updateSpeedDial(open: Boolean) { private fun updateSpeedDial(open: Boolean) {
requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" } requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" }
.invalidateEnabled(open) .invalidateEnabled(open)

View file

@ -98,7 +98,7 @@ sealed interface ArtistShowChoices {
val uid: Music.UID val uid: Music.UID
/** The current [Artist] choices. */ /** The current [Artist] choices. */
val choices: List<Artist> val choices: List<Artist>
/** Sanitize this instance with a [DeviceLibrary]. */ /** Sanitize this instance with a [Library]. */
fun sanitize(newLibrary: Library): ArtistShowChoices? fun sanitize(newLibrary: Library): ArtistShowChoices?
/** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */ /** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */

View file

@ -37,12 +37,10 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding import org.oxycblt.auxio.databinding.FragmentHomeBinding
@ -68,7 +66,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.lazyReflectedMethod
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.musikr.IndexingProgress import org.oxycblt.musikr.IndexingProgress
@ -94,7 +91,6 @@ class HomeFragment :
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
private var getContentLauncher: ActivityResultLauncher<String>? = null private var getContentLauncher: ActivityResultLauncher<String>? = null
private var pendingImportTarget: Playlist? = null private var pendingImportTarget: Playlist? = null
private var lastUpdateTime = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -512,11 +508,5 @@ class HomeFragment :
private companion object { private companion object {
val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView")
val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop") val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop")
val FAB_HIDE_FROM_USER_FIELD: Method by
lazyReflectedMethod(
FloatingActionButton::class,
"hide",
FloatingActionButton.OnVisibilityChangedListener::class,
Boolean::class)
} }
} }

View file

@ -30,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.image.covers.SettingCovers import org.oxycblt.auxio.image.covers.SettingCovers
import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.CoverResult
class CoverProvider() : ContentProvider() { class CoverProvider : ContentProvider() {
override fun onCreate(): Boolean = true override fun onCreate(): Boolean = true
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {

View file

@ -37,6 +37,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isEmpty
import androidx.core.view.updateMarginsRelative import androidx.core.view.updateMarginsRelative
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import coil3.ImageLoader import coil3.ImageLoader
@ -172,7 +173,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
super.onFinishInflate() super.onFinishInflate()
// The image isn't added if other children have populated the body. This is by design. // The image isn't added if other children have populated the body. This is by design.
if (childCount == 0) { if (isEmpty()) {
addView(image) addView(image)
} }

View file

@ -19,9 +19,9 @@
package org.oxycblt.auxio.image.coil package org.oxycblt.auxio.image.coil
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asImage import coil3.asImage
@ -90,8 +90,7 @@ private constructor(
val mosaicFrameSize = val mosaicFrameSize =
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
val mosaicBitmap = val mosaicBitmap = createBitmap(mosaicSize.width, mosaicSize.height)
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(mosaicBitmap) val canvas = Canvas(mosaicBitmap)
var x = 0 var x = 0

View file

@ -18,32 +18,20 @@
package org.oxycblt.auxio.image.coil package org.oxycblt.auxio.image.coil
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import androidx.core.graphics.drawable.toDrawable
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DataSource import coil3.decode.DataSource
import coil3.decode.ImageSource import coil3.decode.ImageSource
import coil3.fetch.FetchResult import coil3.fetch.FetchResult
import coil3.fetch.Fetcher import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.size.Dimension
import coil3.size.Size
import coil3.size.pxOrElse
import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import okio.FileSystem import okio.FileSystem
import okio.buffer import okio.buffer
import okio.source import okio.source
import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.Cover
class CoverFetcher private constructor(private val context: Context, private val cover: Cover) : class CoverFetcher private constructor(private val cover: Cover) : Fetcher {
Fetcher {
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val stream = cover.open() ?: return null val stream = cover.open() ?: return null
return SourceFetchResult( return SourceFetchResult(
@ -52,59 +40,8 @@ class CoverFetcher private constructor(private val context: Context, private val
dataSource = DataSource.DISK) dataSource = DataSource.DISK)
} }
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
// Use whatever size coil gives us to create the mosaic.
val mosaicSize = android.util.Size(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize =
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
val mosaicBitmap =
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(mosaicBitmap)
var x = 0
var y = 0
// For each stream, create a bitmap scaled to 1/4th of the mosaics combined size
// and place it on a corner of the canvas.
for (stream in streams) {
if (y == mosaicSize.height) {
break
}
// Crop the bitmap down to a square so it leaves no empty space
// TODO: Work around this
val bitmap =
SquareCropTransformation.INSTANCE.transform(
BitmapFactory.decodeStream(stream), mosaicFrameSize)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width
if (x == mosaicSize.width) {
x = 0
y += bitmap.height
}
}
// It's way easier to map this into a drawable then try to serialize it into an
// BufferedSource. Just make sure we mark it as "sampled" so Coil doesn't try to
// load low-res mosaics into high-res ImageViews.
return ImageFetchResult(
image = mosaicBitmap.toDrawable(context.resources).asImage(),
isSampled = true,
dataSource = DataSource.DISK)
}
private fun Dimension.mosaicSize(): Int {
// Since we want the mosaic to be perfectly divisible into two, we need to round any
// odd image sizes upwards to prevent the mosaic creation from failing.
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
class Factory @Inject constructor() : Fetcher.Factory<Cover> { class Factory @Inject constructor() : Fetcher.Factory<Cover> {
override fun create(data: Cover, options: Options, imageLoader: ImageLoader) = override fun create(data: Cover, options: Options, imageLoader: ImageLoader) =
CoverFetcher(options.context, data) CoverFetcher(data)
} }
} }

View file

@ -38,8 +38,8 @@ import coil3.transform.Transformation
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* A vendoring of [coil.transform.RoundedCornersTransformation] that can handle non-1:1 aspect ratio * A vendoring of coil's RoundedCornersTransformation that can handle non-1:1 aspect ratio images
* images without cropping them. * without cropping them.
* *
* @author Coil Team, Alexander Capehart (OxygenCobalt) * @author Coil Team, Alexander Capehart (OxygenCobalt)
*/ */

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.image.coil package org.oxycblt.auxio.image.coil
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.scale
import coil3.size.Size import coil3.size.Size
import coil3.size.pxOrElse import coil3.size.pxOrElse
import coil3.transform.Transformation import coil3.transform.Transformation
@ -46,7 +47,7 @@ class SquareCropTransformation : Transformation() {
val desiredHeight = size.height.pxOrElse { dstSize } val desiredHeight = size.height.pxOrElse { dstSize }
if (dstSize != desiredWidth || dstSize != desiredHeight) { if (dstSize != desiredWidth || dstSize != desiredHeight) {
// Image is not the desired size, upscale it. // Image is not the desired size, upscale it.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true) return dst.scale(desiredWidth, desiredHeight)
} }
return dst return dst
} }

View file

@ -34,6 +34,7 @@ import android.view.ViewGroup
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.core.view.isEmpty
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
@ -91,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private var thumbAnimator: Animator? = null private var thumbAnimator: Animator? = null
private val thumbView = private val thumbView =
context.inflater.inflate(R.layout.view_scroll_thumb, null).apply { context.inflater.inflate(R.layout.view_scroll_thumb, this).apply {
thumbSlider.jumpOut(this) thumbSlider.jumpOut(this)
} }
private val thumbPadding = Rect(0, 0, 0, 0) private val thumbPadding = Rect(0, 0, 0, 0)
@ -339,7 +340,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// [proportion of scroll position to scroll range] * [total thumb range] // [proportion of scroll position to scroll range] * [total thumb range]
// This is somewhat adapted from the androidx RecyclerView FastScroller implementation. // This is somewhat adapted from the androidx RecyclerView FastScroller implementation.
val offsetY = computeVerticalScrollOffset() val offsetY = computeVerticalScrollOffset()
if (computeVerticalScrollRange() < height || childCount == 0) { if (computeVerticalScrollRange() < height || isEmpty()) {
fastScrollingPossible = false fastScrollingPossible = false
hideThumb() hideThumb()
hidePopup() hidePopup()

View file

@ -188,8 +188,8 @@ interface MusicRepository {
/** /**
* Flags indicating which kinds of music information changed. * Flags indicating which kinds of music information changed.
* *
* @param deviceLibrary Whether the current [DeviceLibrary] has changed. * @param deviceLibrary Whether the current songs/albums/artists/genres has changed.
* @param library Whether the current [Playlist]s have changed. * @param userLibrary Whether the current playlists have changed.
*/ */
data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean) data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean)
@ -244,7 +244,7 @@ constructor(
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>() private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null @Volatile private var indexingWorker: IndexingWorker? = null
@Volatile override var library: MutableLibrary? = null @Volatile override var library: MutableLibrary? = null
@Volatile private var previousCompletedState: IndexingState.Completed? = null @Volatile private var previousCompletedState: IndexingState.Completed? = null
@ -283,7 +283,7 @@ constructor(
} }
@Synchronized @Synchronized
override fun registerWorker(worker: MusicRepository.IndexingWorker) { override fun registerWorker(worker: IndexingWorker) {
if (indexingWorker != null) { if (indexingWorker != null) {
L.w("Worker is already registered") L.w("Worker is already registered")
return return
@ -293,7 +293,7 @@ constructor(
} }
@Synchronized @Synchronized
override fun unregisterWorker(worker: MusicRepository.IndexingWorker) { override fun unregisterWorker(worker: IndexingWorker) {
if (indexingWorker !== worker) { if (indexingWorker !== worker) {
L.w("Given worker did not match current worker") L.w("Given worker did not match current worker")
return return

View file

@ -27,15 +27,10 @@ import org.oxycblt.auxio.R
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class MusicType { enum class MusicType {
/** @see Song */
SONGS, SONGS,
/** @see Album */
ALBUMS, ALBUMS,
/** @see Artist */
ARTISTS, ARTISTS,
/** @see Genre */
GENRES, GENRES,
/** @see Playlist */
PLAYLISTS; PLAYLISTS;
/** /**

View file

@ -25,6 +25,7 @@ import android.view.LayoutInflater
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@ -80,7 +81,7 @@ class MusicSourcesDialog :
val locations = val locations =
savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull { savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull {
MusicLocation.existing(requireContext(), Uri.parse(it)) MusicLocation.existing(requireContext(), it.toUri())
} ?: musicSettings.musicLocations } ?: musicSettings.musicLocations
locationAdapter.addAll(locations) locationAdapter.addAll(locations)

View file

@ -51,7 +51,7 @@ class NewLocationFooterAdapter(private val listener: Listener) :
} }
/** /**
* A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewPlaylistFooterAdapter]. * A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewLocationFooterAdapter].
* Use [from] to create an instance. * Use [from] to create an instance.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)

View file

@ -48,13 +48,6 @@ fun Long.dsToMs() = times(100)
*/ */
fun Long.dsToSecs() = floorDiv(10) fun Long.dsToSecs() = floorDiv(10)
/**
* Convert seconds into milliseconds.
*
* @return A converted millisecond value.
*/
fun Long.secsToMs() = times(1000)
/** /**
* Convert a millisecond value into a string duration. * Convert a millisecond value into a string duration.
* *

View file

@ -18,7 +18,9 @@
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.service
import androidx.annotation.OptIn
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.exoplayer.source.ShuffleOrder
/** /**
@ -28,6 +30,7 @@ import androidx.media3.exoplayer.source.ShuffleOrder
* *
* @author media3 team, Alexander Capehart (OxygenCobalt) * @author media3 team, Alexander Capehart (OxygenCobalt)
*/ */
@OptIn(UnstableApi::class)
class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder { class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder {
private val indexInShuffled: IntArray = IntArray(shuffled.size) private val indexInShuffled: IntArray = IntArray(shuffled.size)

View file

@ -22,11 +22,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.annotation.OptIn
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.RenderersFactory
@ -62,6 +64,7 @@ import org.oxycblt.musikr.MusicParent
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import timber.log.Timber as L import timber.log.Timber as L
@OptIn(UnstableApi::class)
class ExoPlaybackStateHolder( class ExoPlaybackStateHolder(
private val context: Context, private val context: Context,
private val player: ExoPlayer, private val player: ExoPlayer,

View file

@ -19,6 +19,8 @@
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.service
import android.content.Context import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.ContentDataSource import androidx.media3.datasource.ContentDataSource
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
@ -41,6 +43,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@OptIn(UnstableApi::class)
class SystemModule { class SystemModule {
@Provides @Provides
fun mediaSourceFactory( fun mediaSourceFactory(

View file

@ -20,9 +20,9 @@ package org.oxycblt.auxio.settings
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.net.toUri
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -102,7 +102,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
} }
private fun Context.sendEmail(recipient: String) { private fun Context.sendEmail(recipient: String) {
val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:$recipient") } val intent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:$recipient".toUri() }
startIntent(intent) startIntent(intent)
} }

View file

@ -46,25 +46,25 @@ class AnimConfig(
companion object { companion object {
val STANDARD = MR.attr.motionEasingStandardInterpolator val STANDARD = MR.attr.motionEasingStandardInterpolator
val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator // val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator
val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator
val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator
val SHORT1 = MR.attr.motionDurationShort1 to 50 val SHORT1 = MR.attr.motionDurationShort1 to 50
val SHORT2 = MR.attr.motionDurationShort2 to 100 // val SHORT2 = MR.attr.motionDurationShort2 to 100
val SHORT3 = MR.attr.motionDurationShort3 to 150 val SHORT3 = MR.attr.motionDurationShort3 to 150
val SHORT4 = MR.attr.motionDurationShort4 to 200 // val SHORT4 = MR.attr.motionDurationShort4 to 200
val MEDIUM1 = MR.attr.motionDurationMedium1 to 250 val MEDIUM1 = MR.attr.motionDurationMedium1 to 250
val MEDIUM2 = MR.attr.motionDurationMedium2 to 300 val MEDIUM2 = MR.attr.motionDurationMedium2 to 300
val MEDIUM3 = MR.attr.motionDurationMedium3 to 350 val MEDIUM3 = MR.attr.motionDurationMedium3 to 350
val MEDIUM4 = MR.attr.motionDurationMedium4 to 400 // val MEDIUM4 = MR.attr.motionDurationMedium4 to 400
val LONG1 = MR.attr.motionDurationLong1 to 450 // val LONG1 = MR.attr.motionDurationLong1 to 450
val LONG2 = MR.attr.motionDurationLong2 to 500 // val LONG2 = MR.attr.motionDurationLong2 to 500
val LONG3 = MR.attr.motionDurationLong3 to 550 // val LONG3 = MR.attr.motionDurationLong3 to 550
val LONG4 = MR.attr.motionDurationLong4 to 600 // val LONG4 = MR.attr.motionDurationLong4 to 600
val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700 // val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700
val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800 // val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800
val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900 // val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900
val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000 // val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000
fun of(context: Context, @AttrRes interpolator: Int, duration: Pair<Int, Int>) = fun of(context: Context, @AttrRes interpolator: Int, duration: Pair<Int, Int>) =
AnimConfig(context, interpolator, duration.first, duration.second) AnimConfig(context, interpolator, duration.first, duration.second)
@ -122,7 +122,7 @@ private constructor(
} }
} }
fun jumpToFadeIn(view: View) { private fun jumpToFadeIn(view: View) {
view.apply { view.apply {
alpha = 1f alpha = 1f
scaleX = 1.0f scaleX = 1.0f

View file

@ -24,7 +24,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
@ -36,7 +35,6 @@ import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.children import androidx.core.view.children
import androidx.navigation.NavController import androidx.navigation.NavController
@ -106,10 +104,6 @@ private fun isUnderImpl(
val View.isRtl: Boolean val View.isRtl: Boolean
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
/** Whether this [Drawable] is using an RTL layout direction. */
val Drawable.isRtl: Boolean
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
/** Get a [Context] from a [ViewBinding]'s root [View]. */ /** Get a [Context] from a [ViewBinding]'s root [View]. */
val ViewBinding.context: Context val ViewBinding.context: Context
get() = root.context get() = root.context
@ -357,7 +351,7 @@ fun Context.startIntent(intent: Intent) {
// No app installed to open the link // No app installed to open the link
showToast(R.string.err_no_app) showToast(R.string.err_no_app)
} }
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { } else {
// On older versions of android, opening links from an ACTION_VIEW intent might // On older versions of android, opening links from an ACTION_VIEW intent might
// not work in all cases, especially when no default app was set. If that is the // not work in all cases, especially when no default app was set. If that is the
// case, we will try to manually handle these cases before we try to launch the // case, we will try to manually handle these cases before we try to launch the

View file

@ -22,18 +22,11 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import java.util.concurrent.TimeoutException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.oxycblt.auxio.BuildConfig
import timber.log.Timber as L
/** /**
* A wrapper around [StateFlow] exposing a one-time consumable event. * A wrapper around [StateFlow] exposing a one-time consumable event.
@ -153,71 +146,3 @@ private fun Fragment.launch(
) { ) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
} }
const val DEFAULT_TIMEOUT = 60000L
/**
* Wraps [SendChannel.send] with a specified timeout.
*
* @param element The element to send.
* @param timeout The timeout in milliseconds. Defaults to 10 seconds.
* @throws TimeoutException If the timeout is reached, provides context on what element
* specifically.
*/
suspend fun <E> SendChannel<E>.sendWithTimeout(element: E, timeout: Long = DEFAULT_TIMEOUT) {
try {
withTimeout(timeout) { send(element) }
} catch (e: TimeoutCancellationException) {
L.e("Failed to send element to channel $e in ${timeout}ms.")
if (BuildConfig.DEBUG) {
throw TimeoutException("Timed out sending element to channel: $e")
} else {
L.e(e.stackTraceToString())
send(element)
}
}
}
/**
* Wraps a [ReceiveChannel] consumption with a specified timeout. Note that the timeout will only
* start on the first element received, as to prevent initialization of dependent coroutines being
* interpreted as a timeout.
*
* @param action The action to perform on each element received.
* @param timeout The timeout in milliseconds. Defaults to 10 seconds.
* @throws TimeoutException If the timeout is reached, provides context on what element
* specifically.
*/
suspend fun <E> ReceiveChannel<E>.forEachWithTimeout(
timeout: Long = DEFAULT_TIMEOUT,
action: suspend (E) -> Unit
) {
var exhausted = false
var subsequent = false
val handler: suspend () -> Unit = {
val value = receiveCatching()
if (value.isClosed && value.exceptionOrNull() == null) {
exhausted = true
} else {
action(value.getOrThrow())
}
}
while (!exhausted) {
try {
if (subsequent) {
withTimeout(timeout) { handler() }
} else {
handler()
subsequent = true
}
} catch (e: TimeoutCancellationException) {
L.e("Failed to send element to channel $e in ${timeout}ms.")
if (BuildConfig.DEBUG) {
throw TimeoutException("Timed out sending element to channel: $e")
} else {
L.e(e.stackTraceToString())
handler()
}
}
}
}

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.widgets
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.scale
import coil3.size.Size import coil3.size.Size
import coil3.transform.Transformation import coil3.transform.Transformation
import kotlin.math.sqrt import kotlin.math.sqrt
@ -49,7 +50,7 @@ class WidgetBitmapTransformation(reduce: Float) : Transformation() {
val scale = sqrt(maxBitmapArea / inputArea.toDouble()) val scale = sqrt(maxBitmapArea / inputArea.toDouble())
val newWidth = (input.width * scale).toInt() val newWidth = (input.width * scale).toInt()
val newHeight = (input.height * scale).toInt() val newHeight = (input.height * scale).toInt()
return Bitmap.createScaledBitmap(input, newWidth, newHeight, true) return input.scale(newWidth, newHeight)
} }
return input return input
} }

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.widgets package org.oxycblt.auxio.widgets
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
@ -66,11 +65,6 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) {
setInt(viewId, "setLayoutDirection", layoutDirection) setInt(viewId, "setLayoutDirection", layoutDirection)
} }
fun AppWidgetManager.setWidgetPreviewCompat(component: ComponentName, remoteViews: RemoteViews) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
setWidgetPreview(component, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, remoteViews)
}
}
/** /**
* Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an
* adaptive layout, in a version-compatible manner. * adaptive layout, in a version-compatible manner.

View file

@ -119,7 +119,8 @@
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_cover" /> app:layout_constraintTop_toBottomOf="@+id/detail_cover"
tools:ignore="RtlSymmetry" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton <org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_shuffle_button" android:id="@+id/detail_shuffle_button"

View file

@ -101,7 +101,8 @@
app:icon="@drawable/ic_play_24" app:icon="@drawable/ic_play_24"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_info" /> app:layout_constraintTop_toBottomOf="@+id/detail_info"
tools:ignore="RtlSymmetry"/>
<org.oxycblt.auxio.ui.RippleFixMaterialButton <org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_shuffle_button" android:id="@+id/detail_shuffle_button"

View file

@ -129,7 +129,8 @@
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button" app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" /> app:layout_constraintTop_toTopOf="@+id/detail_play_button"
tools:ignore="RtlSymmetry" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider

View file

@ -132,7 +132,8 @@
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button" app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" /> app:layout_constraintTop_toTopOf="@+id/detail_play_button"
tools:ignore="RtlSymmetry" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider

View file

@ -34,6 +34,7 @@
android:layout_height="@dimen/size_icon_huge" android:layout_height="@dimen/size_icon_huge"
android:layout_marginBottom="@dimen/spacing_small" android:layout_marginBottom="@dimen/spacing_small"
android:src="@drawable/ic_song_48" android:src="@drawable/ic_song_48"
tools:ignore="ContentDescription"
app:tint="?attr/colorOnSurface" /> app:tint="?attr/colorOnSurface" />
<TextView <TextView

View file

@ -3,12 +3,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/spacing_tiny" android:padding="@dimen/spacing_tiny"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:tools="http://schemas.android.com/tools">
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/size_touchable_small" android:layout_height="@dimen/size_touchable_small"
android:scaleType="centerInside" android:scaleType="centerInside"
tools:ignore="ContentDescription"
android:src="@drawable/ui_scroll_thumb" /> android:src="@drawable/ui_scroll_thumb" />
</FrameLayout> </FrameLayout>

View file

@ -22,7 +22,7 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="@drawable/ui_widget_bg_round" android:background="@drawable/ui_widget_bg_round"
android:clipToOutline="true" android:clipToOutline="true"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription,UnusedAttribute" />
<android.widget.LinearLayout <android.widget.LinearLayout
android:id="@+id/widget_panel" android:id="@+id/widget_panel"

View file

@ -20,8 +20,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="@drawable/ui_widget_bg_round" android:background="@drawable/ui_widget_bg_round"
android:clipToOutline="true" tools:ignore="ContentDescription,UnusedAttribute" />
tools:ignore="ContentDescription" />
<android.widget.LinearLayout <android.widget.LinearLayout
android:id="@+id/widget_panel" android:id="@+id/widget_panel"

View file

@ -302,7 +302,7 @@
<string name="clr_brown">Kafe</string> <string name="clr_brown">Kafe</string>
<string name="fmt_db_neg">-%.1f dB</string> <string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kbps</string>
<string name="fmt_indexing">Duke ngarkuar bibliotekën tuaj muzikore... (%1$d/%2$d)</string> <string name="fmt_indexing">Duke ngarkuar bibliotekën tuaj muzikore (%1$d/%2$d)</string>
<string name="fmt_lib_song_count">Këngët e ngarkuara: %d</string> <string name="fmt_lib_song_count">Këngët e ngarkuara: %d</string>
<string name="fmt_lib_album_count">Albumet e ngarkuara: %d</string> <string name="fmt_lib_album_count">Albumet e ngarkuara: %d</string>
<string name="fmt_lib_artist_count">Artistët e ngarkuar: %d</string> <string name="fmt_lib_artist_count">Artistët e ngarkuar: %d</string>

View file

@ -1,6 +1,9 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Auxio.Black" parent="Theme.Auxio.Base"> <!--
Can't meaningfully dim the default black theme colors, have to tweak the palette manually
-->
<style name="Theme.Auxio.Black" parent="Theme.Auxio.Base" tools:ignore="PrivateResource">
<item name="colorSurface">@android:color/black</item> <item name="colorSurface">@android:color/black</item>
<item name="colorSurfaceDim">@color/m3_ref_palette_dynamic_neutral_variant4</item> <item name="colorSurfaceDim">@color/m3_ref_palette_dynamic_neutral_variant4</item>
<item name="colorSurfaceBright">@color/m3_ref_palette_dynamic_neutral_variant12</item> <item name="colorSurfaceBright">@color/m3_ref_palette_dynamic_neutral_variant12</item>

View file

@ -59,8 +59,7 @@ internal interface CacheReadDao {
@Dao @Dao
internal interface CacheWriteDao { internal interface CacheWriteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(data: CachedSongData)
suspend fun updateSong(CachedSongData: CachedSongData)
@Transaction @Transaction
suspend fun deleteExcludingUris(uris: Set<String>) { suspend fun deleteExcludingUris(uris: Set<String>) {

View file

@ -37,6 +37,10 @@ sealed interface Name : Comparable<Name> {
/** A tokenized version of the name that will be compared. */ /** A tokenized version of the name that will be compared. */
abstract val tokens: List<Token> abstract val tokens: List<Token>
abstract override fun hashCode(): Int
abstract override fun equals(other: Any?): Boolean
final override fun compareTo(other: Name) = final override fun compareTo(other: Name) =
when (other) { when (other) {
is Known -> { is Known -> {