Merge branch 'hotfixes' into dev

This commit is contained in:
Alexander Capehart 2024-08-23 13:46:30 -06:00
commit f251813200
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 251 additions and 70 deletions

View file

@ -15,6 +15,24 @@
- Excessive CPU no longer spent showing music loading process - Excessive CPU no longer spent showing music loading process
- Fixed playback sheet flickering on warm start - Fixed playback sheet flickering on warm start
## 3.5.3
#### What's New
- Basic Tasker integration for safely starting Auxio's service
#### What's Improved
- Added support for informal singular-spaced tags like `album artist` in
file metadata
#### What's Fixed
- Fix "Foreground not allowed" music loading crash from starting too early
- Fixed widget not loading on some devices due to the cover being too large
## 3.5.2
#### What's Fixed
- Fixed music loading failure from improper sort systems (For real this time)
## 3.5.1 ## 3.5.1
#### What's Fixed #### What's Fixed

View file

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

View file

@ -16,13 +16,13 @@ android {
// it here so that binary stripping will work. // it here so that binary stripping will work.
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the // TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
// NDK use is unified // NDK use is unified
ndkVersion = "25.2.9519653" ndkVersion "26.3.11579264"
namespace "org.oxycblt.auxio" namespace "org.oxycblt.auxio"
defaultConfig { defaultConfig {
applicationId namespace applicationId namespace
versionName "3.5.1" versionName "3.5.3"
versionCode 47 versionCode 49
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
@ -155,6 +155,9 @@ dependencies {
// Speed dial // Speed dial
implementation "com.leinardi.android:speed-dial:3.3.0" implementation "com.leinardi.android:speed-dial:3.3.0"
// Tasker integration
implementation 'com.joaomgcd:taskerpluginlibrary:0.4.10'
// Testing // Testing
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"

View file

@ -135,5 +135,15 @@
android:resource="@xml/widget_info" /> android:resource="@xml/widget_info" />
</receiver> </receiver>
<!-- Tasker 'start service' integration -->
<activity
android:name=".tasker.ActivityConfigStartAction"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/lbl_start_playback">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

View file

@ -55,13 +55,9 @@ class AuxioService : MediaLibraryService(), ForegroundListener {
} }
private fun start(intent: Intent?) { private fun start(intent: Intent?) {
val nativeStart = intent?.getBooleanExtra(INTENT_KEY_NATIVE_START, false) ?: false val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1
if (!nativeStart) {
// Some foreign code started us, no guarantees about foreground stability. Figure
// out what to do.
mediaSessionFragment.handleNonNativeStart()
}
indexingFragment.start() indexingFragment.start()
mediaSessionFragment.start(startId)
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
@ -87,6 +83,7 @@ class AuxioService : MediaLibraryService(), ForegroundListener {
if (change == ForegroundListener.Change.MEDIA_SESSION) { if (change == ForegroundListener.Change.MEDIA_SESSION) {
mediaSessionFragment.createNotification { mediaSessionFragment.createNotification {
startForeground(it.notificationId, it.notification) startForeground(it.notificationId, it.notification)
isForeground = true
} }
} }
// Nothing changed, but don't show anything music related since we can always // Nothing changed, but don't show anything music related since we can always
@ -95,16 +92,21 @@ class AuxioService : MediaLibraryService(), ForegroundListener {
indexingFragment.createNotification { indexingFragment.createNotification {
if (it != null) { if (it != null) {
startForeground(it.code, it.build()) startForeground(it.code, it.build())
isForeground = true
} else { } else {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
isForeground = false
} }
} }
} }
} }
companion object { companion object {
var isForeground = false
private set
// This is only meant for Auxio to internally ensure that it's state management will work. // This is only meant for Auxio to internally ensure that it's state management will work.
const val INTENT_KEY_NATIVE_START = BuildConfig.APPLICATION_ID + ".service.NATIVE_START" const val INTENT_KEY_START_ID = BuildConfig.APPLICATION_ID + ".service.START_ID"
} }
} }

View file

@ -59,6 +59,10 @@ object IntegerTable {
const val INDEXER_NOTIFICATION_CODE = 0xA0A1 const val INDEXER_NOTIFICATION_CODE = 0xA0A1
/** MainActivity Intent request code */ /** MainActivity Intent request code */
const val REQUEST_CODE = 0xA0C0 const val REQUEST_CODE = 0xA0C0
/** Activity AuxioService Start ID */
const val START_ID_ACTIVITY = 0xA050
/** Tasker AuxioService Start ID */
const val START_ID_TASKER = 0xA051
/** RepeatMode.NONE */ /** RepeatMode.NONE */
const val REPEAT_MODE_NONE = 0xA100 const val REPEAT_MODE_NONE = 0xA100
/** RepeatMode.ALL */ /** RepeatMode.ALL */

View file

@ -71,11 +71,11 @@ class MainActivity : AppCompatActivity() {
startService( startService(
Intent(this, AuxioService::class.java) Intent(this, AuxioService::class.java)
.putExtra(AuxioService.INTENT_KEY_NATIVE_START, true)) .putExtra(AuxioService.INTENT_KEY_START_ID, IntegerTable.START_ID_ACTIVITY))
if (!startIntentAction(intent)) { if (!startIntentAction(intent)) {
// No intent action to do, just restore the previously saved state. // No intent action to do, just restore the previously saved state.
playbackModel.playDeferred(DeferredPlayback.RestoreState) playbackModel.playDeferred(DeferredPlayback.RestoreState(false))
} }
} }

View file

@ -107,7 +107,10 @@ class RoundedRectTransformation(
} }
private fun calculateOutputSize(input: Bitmap, size: Size): Pair<Int, Int> { private fun calculateOutputSize(input: Bitmap, size: Size): Pair<Int, Int> {
// MODIFICATION: Remove short-circuiting for original size and input size if (size == Size.ORIGINAL) {
// This path only runs w/the widget code, which already normalizes widget sizes
return input.width to input.height
}
val multiplier = val multiplier =
DecodeUtils.computeSizeMultiplier( DecodeUtils.computeSizeMultiplier(
srcWidth = input.width, srcWidth = input.width,

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.metadata.splitEscaped import org.oxycblt.auxio.music.metadata.splitEscaped
@Database(entities = [CachedSong::class], version = 46, exportSchema = false) @Database(entities = [CachedSong::class], version = 49, exportSchema = false)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun cachedSongsDao(): CachedSongsDao abstract fun cachedSongsDao(): CachedSongsDao
} }

View file

@ -70,12 +70,12 @@ sealed interface Name : Comparable<Name> {
final override fun compareTo(other: Name) = final override fun compareTo(other: Name) =
when (other) { when (other) {
is Known -> { is Known -> {
// Progressively compare the sort tokens between each known name. val result =
sortTokens.zip(other.sortTokens).fold(0) { acc, (token, otherToken) -> sortTokens.zip(other.sortTokens).fold(0) { acc, (token, otherToken) ->
acc.takeIf { it != 0 } ?: token.compareTo(otherToken) acc.takeIf { it != 0 } ?: token.compareTo(otherToken)
} }
if (result != 0) result else sortTokens.size.compareTo(other.sortTokens.size)
} }
// Unknown names always come before known names.
is Unknown -> 1 is Unknown -> 1
} }

View file

@ -100,6 +100,7 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx
private fun populateWithId3v2(rawSong: RawSong, textFrames: Map<String, List<String>>) { private fun populateWithId3v2(rawSong: RawSong, textFrames: Map<String, List<String>>) {
// Song // Song
logD(textFrames)
(textFrames["TXXX:musicbrainz release track id"] (textFrames["TXXX:musicbrainz release track id"]
?: textFrames["TXXX:musicbrainz_releasetrackid"]) ?: textFrames["TXXX:musicbrainz_releasetrackid"])
?.let { rawSong.musicBrainzId = it.first() } ?.let { rawSong.musicBrainzId = it.first() }
@ -147,10 +148,13 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx
(textFrames["TXXX:musicbrainz artist id"] ?: textFrames["TXXX:musicbrainz_artistid"])?.let { (textFrames["TXXX:musicbrainz artist id"] ?: textFrames["TXXX:musicbrainz_artistid"])?.let {
rawSong.artistMusicBrainzIds = it rawSong.artistMusicBrainzIds = it
} }
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } (textFrames["TXXX:artists"] ?: textFrames["TPE1"] ?: textFrames["TXXX:artist"])?.let {
rawSong.artistNames = it
}
(textFrames["TXXX:artistssort"] (textFrames["TXXX:artistssort"]
?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"]
?: textFrames["TSOP"]) ?: textFrames["TSOP"] ?: textFrames["artistsort"]
?: textFrames["TXXX:artist sort"])
?.let { rawSong.artistSortNames = it } ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
@ -159,13 +163,14 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx
?.let { rawSong.albumArtistMusicBrainzIds = it } ?.let { rawSong.albumArtistMusicBrainzIds = it }
(textFrames["TXXX:albumartists"] (textFrames["TXXX:albumartists"]
?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"] ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"]
?: textFrames["TPE2"]) ?: textFrames["TPE2"] ?: textFrames["TXXX:albumartist"]
?: textFrames["TXXX:album artist"])
?.let { rawSong.albumArtistNames = it } ?.let { rawSong.albumArtistNames = it }
(textFrames["TXXX:albumartistssort"] (textFrames["TXXX:albumartistssort"]
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"] ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"]
?: textFrames["TXXX:albumartistsort"] ?: textFrames["TXXX:albumartistsort"]
// This is a non-standard iTunes extension // This is a non-standard iTunes extension
?: textFrames["TSO2"]) ?: textFrames["TSO2"] ?: textFrames["TXXX:album artist sort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
@ -273,7 +278,8 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx
} }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
(comments["artistssort"] (comments["artistssort"]
?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"]) ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"]
?: comments["artist sort"])
?.let { rawSong.artistSortNames = it } ?.let { rawSong.artistSortNames = it }
// Album artist // Album artist
@ -281,12 +287,12 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx
rawSong.albumArtistMusicBrainzIds = it rawSong.albumArtistMusicBrainzIds = it
} }
(comments["albumartists"] (comments["albumartists"]
?: comments["album_artists"] ?: comments["album artists"] ?: comments["album_artists"] ?: comments["album artists"] ?: comments["albumartist"]
?: comments["albumartist"]) ?: comments["album artist"])
?.let { rawSong.albumArtistNames = it } ?.let { rawSong.albumArtistNames = it }
(comments["albumartistssort"] (comments["albumartistssort"]
?: comments["albumartists_sort"] ?: comments["albumartists sort"] ?: comments["albumartists_sort"] ?: comments["albumartists sort"]
?: comments["albumartistsort"]) ?: comments["albumartistsort"] ?: comments["album artist sort"])
?.let { rawSong.albumArtistSortNames = it } ?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre

View file

@ -164,10 +164,18 @@ class ExoPlaybackStateHolder(
is DeferredPlayback.RestoreState -> { is DeferredPlayback.RestoreState -> {
logD("Restoring playback state") logD("Restoring playback state")
restoreScope.launch { restoreScope.launch {
persistenceRepository.readState()?.let { val state = persistenceRepository.readState()
// Apply the saved state on the main thread to prevent code expecting withContext(Dispatchers.Main) {
// state updates on the main thread from crashing. if (state != null) {
withContext(Dispatchers.Main) { playbackManager.applySavedState(it, false) } // Apply the saved state on the main thread to prevent code expecting
// state updates on the main thread from crashing.
playbackManager.applySavedState(state, false)
if (action.play) {
playbackManager.playing(true)
}
} else if (action.fallback != null) {
playbackManager.playDeferred(action.fallback)
}
} }
} }
} }

View file

@ -109,11 +109,23 @@ constructor(
} }
} }
fun handleNonNativeStart() { fun start(startedBy: Int) {
// At minimum we want to ensure an active playback state. // At minimum we want to ensure an active playback state.
// TODO: Possibly also force to go foreground? // TODO: Possibly also force to go foreground?
logD("Handling non-native start.") logD("Handling non-native start.")
playbackManager.playDeferred(DeferredPlayback.RestoreState) val action =
when (startedBy) {
IntegerTable.START_ID_ACTIVITY -> null
IntegerTable.START_ID_TASKER ->
DeferredPlayback.RestoreState(
play = true, fallback = DeferredPlayback.ShuffleAll)
// External services using Auxio better know what they are doing.
else -> DeferredPlayback.RestoreState(play = false)
}
if (action != null) {
logD("Initing service fragment using action $action")
playbackManager.playDeferred(action)
}
} }
fun hasNotification(): Boolean = exoHolder.sessionOngoing fun hasNotification(): Boolean = exoHolder.sessionOngoing

View file

@ -281,7 +281,8 @@ data class QueueChange(val type: Type, val instructions: UpdateInstructions) {
/** Possible long-running background tasks handled by the background playback task. */ /** Possible long-running background tasks handled by the background playback task. */
sealed interface DeferredPlayback { sealed interface DeferredPlayback {
/** Restore the previously saved playback state. */ /** Restore the previously saved playback state. */
data object RestoreState : DeferredPlayback data class RestoreState(val play: Boolean, val fallback: DeferredPlayback? = null) :
DeferredPlayback
/** /**
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All" shortcut. * Start shuffled playback of the entire music library. Analogous to the "Shuffle All" shortcut.

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 Auxio Project
* Start.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.tasker
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess
import org.oxycblt.auxio.AuxioService
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
class StartActionHelper(config: TaskerPluginConfig<Unit>) :
TaskerPluginConfigHelperNoOutputOrInput<StartActionRunner>(config) {
override val runnerClass: Class<StartActionRunner>
get() = StartActionRunner::class.java
override fun addToStringBlurb(input: TaskerInput<Unit>, blurbBuilder: StringBuilder) {
blurbBuilder.append(context.getString(R.string.lng_tasker_start))
}
}
class ActivityConfigStartAction : Activity(), TaskerPluginConfigNoInput {
override val context
get() = applicationContext
private val taskerHelper by lazy { StartActionHelper(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
taskerHelper.finishForTasker()
}
}
class StartActionRunner : TaskerPluginRunnerActionNoOutputOrInput() {
override fun run(context: Context, input: TaskerInput<Unit>): TaskerPluginResult<Unit> {
ContextCompat.startForegroundService(
context,
Intent(context, AuxioService::class.java)
.putExtra(AuxioService.INTENT_KEY_START_ID, IntegerTable.START_ID_TASKER))
while (!AuxioService.isForeground) {
Thread.sleep(100)
}
return TaskerPluginResultSucess()
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 Auxio Project
* WidgetBitmapTransformation.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.widgets
import android.content.res.Resources
import android.graphics.Bitmap
import coil.size.Size
import coil.transform.Transformation
import kotlin.math.sqrt
class WidgetBitmapTransformation(private val reduce: Float) : Transformation {
private val metrics = Resources.getSystem().displayMetrics
private val sw = metrics.widthPixels
private val sh = metrics.heightPixels
// Cap memory usage at 1.5 times the size of the display
// 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
// Of course since OEMs randomly patch this check, we give a lot of slack.
private val maxBitmapArea = (1.5 * sw * sh / reduce).toInt()
override val cacheKey: String
get() = "WidgetBitmapTransformation:${maxBitmapArea}"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
if (size !== Size.ORIGINAL) {
// The widget loading stack basically discards the size parameter since there's no
// sane value from the get-go, all this transform does is actually dynamically apply
// the size cap so this transform must always be zero.
throw IllegalArgumentException("WidgetBitmapTransformation requires original size.")
}
val inputArea = input.width * input.height
if (inputArea != maxBitmapArea) {
val scale = sqrt(maxBitmapArea / inputArea.toDouble())
val newWidth = (input.width * scale).toInt()
val newHeight = (input.height * scale).toInt()
return Bitmap.createScaledBitmap(input, newWidth, newHeight, true)
}
return input
}
}

View file

@ -22,6 +22,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -96,24 +97,19 @@ constructor(
0 0
} }
return if (cornerRadius > 0) { val transformations = buildList {
// If rounded, reduce the bitmap size further to obtain more pronounced
// rounded corners.
builder.size(getSafeRemoteViewsImageSize(context, 10f))
val cornersTransformation =
RoundedRectTransformation(cornerRadius.toFloat())
if (imageSettings.forceSquareCovers) { if (imageSettings.forceSquareCovers) {
builder.transformations( add(SquareCropTransformation.INSTANCE)
SquareCropTransformation.INSTANCE, cornersTransformation) }
if (cornerRadius > 0) {
add(WidgetBitmapTransformation(15f))
add(RoundedRectTransformation(cornerRadius.toFloat()))
} else { } else {
builder.transformations(cornersTransformation) add(WidgetBitmapTransformation(3f))
} }
} else {
if (imageSettings.forceSquareCovers) {
builder.transformations(SquareCropTransformation.INSTANCE)
}
builder.size(getSafeRemoteViewsImageSize(context))
} }
return builder.size(Size.ORIGINAL).transformations(transformations)
} }
override fun onCompleted(bitmap: Bitmap?) { override fun onCompleted(bitmap: Bitmap?) {

View file

@ -27,7 +27,6 @@ import android.widget.RemoteViews
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import kotlin.math.sqrt
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent
@ -46,24 +45,6 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews {
return views return views
} }
/**
* Get an image size guaranteed to not exceed the [RemoteViews] bitmap memory limit, assuming that
* there is only one image.
*
* @param context [Context] required to perform calculation.
* @param reduce Optional multiplier to reduce the image size. Recommended value is 3 to avoid
* device-specific variations in memory limit.
* @return The dimension of a bitmap that can be safely used in [RemoteViews].
*/
fun getSafeRemoteViewsImageSize(context: Context, reduce: Float = 3f): Int {
val metrics = context.resources.displayMetrics
val sw = metrics.widthPixels
val sh = metrics.heightPixels
// Maximum size is 1/3 total screen area * 4 bytes per pixel. Reverse
// that to obtain the image size.
return sqrt((6f / 4f / reduce) * sw * sh).toInt()
}
/** /**
* Set the background resource of a [RemoteViews] View. * Set the background resource of a [RemoteViews] View.
* *

View file

@ -153,6 +153,7 @@
<string name="lbl_shuffle_shortcut_short">Shuffle</string> <string name="lbl_shuffle_shortcut_short">Shuffle</string>
<!-- Limit to 25 characters --> <!-- Limit to 25 characters -->
<string name="lbl_shuffle_shortcut_long">Shuffle all</string> <string name="lbl_shuffle_shortcut_long">Shuffle all</string>
<string name="lbl_start_playback">Start playback</string>
<string name="lbl_ok">OK</string> <string name="lbl_ok">OK</string>
<string name="lbl_cancel">Cancel</string> <string name="lbl_cancel">Cancel</string>
@ -205,6 +206,10 @@
<string name="lng_supporters_promo">Donate to the project to get your name added here!</string> <string name="lng_supporters_promo">Donate to the project to get your name added here!</string>
<!-- As in music library --> <!-- As in music library -->
<string name="lng_search_library">Search your library…</string> <string name="lng_search_library">Search your library…</string>
<string name="lng_tasker_start">
Starts Auxio using the previously saved state. If no saved state is available, all songs will be shuffled. Playback will start immediately.
\n\nWARNING: Be careful controlling this service, if you close it and then try to use it again, you will probably crash the app.
</string>
<!-- Settings namespace | Settings-related labels --> <!-- Settings namespace | Settings-related labels -->
<eat-comment /> <eat-comment />

View file

@ -0,0 +1,3 @@
Auxio 3.5.0 adds support for android auto alongside various playback and music quality of life improvements.
This release fixes a critical bug with the music loader.
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.5.2

View file

@ -0,0 +1,3 @@
Auxio 3.5.0 adds support for android auto alongside various playback and music quality of life improvements.
This release adds basic Tasker integration while fixing a few issues that affected certain devices.
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.5.3

2
media

@ -1 +1 @@
Subproject commit 9fc2401b8fdc2b23905402462e775c6db4e1527f Subproject commit 34b33175c00183dc95cdcb8c735033b6785041e1