all: reformat code
Reformat all project code
This commit is contained in:
parent
7212700553
commit
cce7b766d7
38 changed files with 194 additions and 203 deletions
|
@ -36,6 +36,7 @@ audio focus was lost
|
||||||
- Fixed issue where the artist name would not be shown in the OS audio switcher menu
|
- Fixed issue where the artist name would not be shown in the OS audio switcher menu
|
||||||
- Fixed issue where the search view would not update if the library changed
|
- Fixed issue where the search view would not update if the library changed
|
||||||
- Fixed visual bug with transitions in the black theme
|
- Fixed visual bug with transitions in the black theme
|
||||||
|
- Fixed toolbar flickering when fast-scrolling in the home UI
|
||||||
|
|
||||||
#### What's Changed
|
#### What's Changed
|
||||||
- Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed.
|
- Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed.
|
||||||
|
|
|
@ -24,7 +24,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
|
// ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -108,6 +107,7 @@ dependencies {
|
||||||
implementation "io.coil-kt:coil:2.1.0"
|
implementation "io.coil-kt:coil:2.1.0"
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
|
// Locked below 1.7.0-alpha03 to avoid the same ripple bug
|
||||||
implementation "com.google.android.material:material:1.7.0-alpha02"
|
implementation "com.google.android.material:material:1.7.0-alpha02"
|
||||||
|
|
||||||
// LeakCanary
|
// LeakCanary
|
||||||
|
|
|
@ -121,6 +121,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
*/
|
*/
|
||||||
private fun startIntentAction(intent: Intent?): Boolean {
|
private fun startIntentAction(intent: Intent?): Boolean {
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
|
// Nothing to do.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,9 @@ import org.oxycblt.auxio.util.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class MainFragment :
|
class MainFragment :
|
||||||
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener, NavController.OnDestinationChangedListener {
|
ViewBindingFragment<FragmentMainBinding>(),
|
||||||
|
ViewTreeObserver.OnPreDrawListener,
|
||||||
|
NavController.OnDestinationChangedListener {
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
|
|
|
@ -155,7 +155,7 @@ class DetailViewModel(application: Application) :
|
||||||
} else {
|
} else {
|
||||||
_currentSong.value = null
|
_currentSong.value = null
|
||||||
}
|
}
|
||||||
logD("Updated song to ${newSong}")
|
logD("Updated song to $newSong")
|
||||||
}
|
}
|
||||||
|
|
||||||
val album = currentAlbum.value
|
val album = currentAlbum.value
|
||||||
|
|
|
@ -105,18 +105,18 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
||||||
* @param genre The new [Song] to bind.
|
* @param genre The new [Song] to bind.
|
||||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||||
*/
|
*/
|
||||||
fun bind(item: Genre, listener: DetailAdapter.Listener) {
|
fun bind(genre: Genre, listener: DetailAdapter.Listener) {
|
||||||
binding.detailCover.bind(item)
|
binding.detailCover.bind(genre)
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
||||||
binding.detailName.text = item.resolveName(binding.context)
|
binding.detailName.text = genre.resolveName(binding.context)
|
||||||
// Nothing about a genre is applicable to the sub-head text.
|
// Nothing about a genre is applicable to the sub-head text.
|
||||||
binding.detailSubhead.isVisible = false
|
binding.detailSubhead.isVisible = false
|
||||||
// The song count of the genre maps to the info text.
|
// The song count of the genre maps to the info text.
|
||||||
binding.detailInfo.text =
|
binding.detailInfo.text =
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
binding.context.getPlural(R.plurals.fmt_artist_count, genre.artists.size),
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
binding.context.getPlural(R.plurals.fmt_song_count, genre.songs.size))
|
||||||
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
|
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
|
||||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -426,7 +426,7 @@ class HomeFragment :
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid)
|
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid)
|
||||||
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
|
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
|
||||||
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid.also { logD(it) })
|
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid)
|
||||||
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
|
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,10 +175,8 @@ object Covers {
|
||||||
* @return An [InputStream] of image data if the cover loading was successful, null otherwise.
|
* @return An [InputStream] of image data if the cover loading was successful, null otherwise.
|
||||||
*/
|
*/
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? {
|
private suspend fun fetchMediaStoreCovers(context: Context, album: Album): InputStream? {
|
||||||
val uri = data.coverUri
|
|
||||||
|
|
||||||
// Eliminate any chance that this blocking call might mess up the loading process
|
// Eliminate any chance that this blocking call might mess up the loading process
|
||||||
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,7 +178,7 @@ sealed class Music : Item {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
||||||
* unlikely-to-change metadata of the music.
|
* unlikely-to-change metadata of the music.
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||||
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
||||||
|
@ -194,27 +194,28 @@ sealed class Music : Item {
|
||||||
}
|
}
|
||||||
// Convert the digest to a UUID. This does cleave off some of the hash, but this
|
// Convert the digest to a UUID. This does cleave off some of the hash, but this
|
||||||
// is considered okay.
|
// is considered okay.
|
||||||
val uuid = UUID(
|
val uuid =
|
||||||
digest[0]
|
UUID(
|
||||||
.toLong()
|
digest[0]
|
||||||
.shl(56)
|
.toLong()
|
||||||
.or(digest[1].toLong().and(0xFF).shl(48))
|
.shl(56)
|
||||||
.or(digest[2].toLong().and(0xFF).shl(40))
|
.or(digest[1].toLong().and(0xFF).shl(48))
|
||||||
.or(digest[3].toLong().and(0xFF).shl(32))
|
.or(digest[2].toLong().and(0xFF).shl(40))
|
||||||
.or(digest[4].toLong().and(0xFF).shl(24))
|
.or(digest[3].toLong().and(0xFF).shl(32))
|
||||||
.or(digest[5].toLong().and(0xFF).shl(16))
|
.or(digest[4].toLong().and(0xFF).shl(24))
|
||||||
.or(digest[6].toLong().and(0xFF).shl(8))
|
.or(digest[5].toLong().and(0xFF).shl(16))
|
||||||
.or(digest[7].toLong().and(0xFF)),
|
.or(digest[6].toLong().and(0xFF).shl(8))
|
||||||
digest[8]
|
.or(digest[7].toLong().and(0xFF)),
|
||||||
.toLong()
|
digest[8]
|
||||||
.shl(56)
|
.toLong()
|
||||||
.or(digest[9].toLong().and(0xFF).shl(48))
|
.shl(56)
|
||||||
.or(digest[10].toLong().and(0xFF).shl(40))
|
.or(digest[9].toLong().and(0xFF).shl(48))
|
||||||
.or(digest[11].toLong().and(0xFF).shl(32))
|
.or(digest[10].toLong().and(0xFF).shl(40))
|
||||||
.or(digest[12].toLong().and(0xFF).shl(24))
|
.or(digest[11].toLong().and(0xFF).shl(32))
|
||||||
.or(digest[13].toLong().and(0xFF).shl(16))
|
.or(digest[12].toLong().and(0xFF).shl(24))
|
||||||
.or(digest[14].toLong().and(0xFF).shl(8))
|
.or(digest[13].toLong().and(0xFF).shl(16))
|
||||||
.or(digest[15].toLong().and(0xFF)))
|
.or(digest[14].toLong().and(0xFF).shl(8))
|
||||||
|
.or(digest[15].toLong().and(0xFF)))
|
||||||
return UID(Format.AUXIO, mode, uuid)
|
return UID(Format.AUXIO, mode, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +225,7 @@ sealed class Music : Item {
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||||
* @param mbid The analogous MusicBrainz ID for this item that was extracted from a
|
* @param mbid The analogous MusicBrainz ID for this item that was extracted from a
|
||||||
* file.
|
* file.
|
||||||
* @return A new MusicBrainz-style [UID]
|
* @return A new MusicBrainz-style [UID].
|
||||||
*/
|
*/
|
||||||
fun musicBrainz(mode: MusicMode, mbid: UUID): UID = UID(Format.MUSICBRAINZ, mode, mbid)
|
fun musicBrainz(mode: MusicMode, mbid: UUID): UID = UID(Format.MUSICBRAINZ, mode, mbid)
|
||||||
|
|
||||||
|
@ -396,8 +397,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
||||||
* @param context [Context] required for [resolveName].
|
* @param context [Context] required for [resolveName]. formatter.
|
||||||
* formatter.
|
|
||||||
*/
|
*/
|
||||||
fun resolveArtistContents(context: Context) =
|
fun resolveArtistContents(context: Context) =
|
||||||
// TODO Internationalize the list
|
// TODO Internationalize the list
|
||||||
|
@ -1408,7 +1408,7 @@ private fun MessageDigest.update(string: String?) {
|
||||||
private fun MessageDigest.update(date: Date?) {
|
private fun MessageDigest.update(date: Date?) {
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
update(date.toString().toByteArray())
|
update(date.toString().toByteArray())
|
||||||
}else {
|
} else {
|
||||||
update(0)
|
update(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1428,7 +1428,7 @@ private fun MessageDigest.update(strings: List<String?>) {
|
||||||
private fun MessageDigest.update(n: Int?) {
|
private fun MessageDigest.update(n: Int?) {
|
||||||
if (n != null) {
|
if (n != null) {
|
||||||
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||||
}else {
|
} else {
|
||||||
update(0)
|
update(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import androidx.core.database.sqlite.transaction
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
@ -357,23 +356,15 @@ private class CacheDatabase(context: Context) :
|
||||||
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
||||||
put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue())
|
put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue())
|
||||||
|
|
||||||
put(
|
put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toSQLMultiValue())
|
||||||
Columns.ARTIST_MUSIC_BRAINZ_IDS,
|
|
||||||
rawSong.artistMusicBrainzIds.toSQLMultiValue())
|
|
||||||
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
|
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
|
||||||
put(
|
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toSQLMultiValue())
|
||||||
Columns.ARTIST_SORT_NAMES,
|
|
||||||
rawSong.artistSortNames.toSQLMultiValue())
|
|
||||||
|
|
||||||
put(
|
put(
|
||||||
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
|
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
|
||||||
rawSong.albumArtistMusicBrainzIds.toSQLMultiValue())
|
rawSong.albumArtistMusicBrainzIds.toSQLMultiValue())
|
||||||
put(
|
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toSQLMultiValue())
|
||||||
Columns.ALBUM_ARTIST_NAMES,
|
put(Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toSQLMultiValue())
|
||||||
rawSong.albumArtistNames.toSQLMultiValue())
|
|
||||||
put(
|
|
||||||
Columns.ALBUM_ARTIST_SORT_NAMES,
|
|
||||||
rawSong.albumArtistSortNames.toSQLMultiValue())
|
|
||||||
|
|
||||||
put(Columns.GENRE_NAMES, rawSong.genreNames.toSQLMultiValue())
|
put(Columns.GENRE_NAMES, rawSong.genreNames.toSQLMultiValue())
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,8 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a [ViewModel] that manages the current music picker state.
|
* a [ViewModel] that manages the current music picker state. Make it so that the dialogs just
|
||||||
* Make it so that the dialogs just contain the music themselves and then exit if the library
|
* contain the music themselves and then exit if the library changes.
|
||||||
* changes.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class PickerViewModel : ViewModel(), MusicStore.Callback {
|
class PickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
|
|
@ -514,8 +514,8 @@ class Indexer private constructor() {
|
||||||
* system to load audio.
|
* system to load audio.
|
||||||
*/
|
*/
|
||||||
val PERMISSION_READ_AUDIO =
|
val PERMISSION_READ_AUDIO =
|
||||||
// TODO: Move elsewhere.
|
// TODO: Move elsewhere.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
// READ_EXTERNAL_STORAGE was superseded by READ_MEDIA_AUDIO in Android 13
|
// READ_EXTERNAL_STORAGE was superseded by READ_MEDIA_AUDIO in Android 13
|
||||||
Manifest.permission.READ_MEDIA_AUDIO
|
Manifest.permission.READ_MEDIA_AUDIO
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,7 +20,7 @@ package org.oxycblt.auxio.playback
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a configuration option for what kind of "secondary" action to show in a particular
|
* Represents a configuration option for what kind of "secondary" action to show in a particular UI
|
||||||
* context.
|
* context.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -47,7 +47,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* available controls.
|
* available controls.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class PlaybackPanelFragment : ViewBindingFragment<FragmentPlaybackPanelBinding>(), Toolbar.OnMenuItemClickListener, StyledSeekBar.Listener {
|
class PlaybackPanelFragment :
|
||||||
|
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
||||||
|
Toolbar.OnMenuItemClickListener,
|
||||||
|
StyledSeekBar.Listener {
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
// AudioEffect expects you to use startActivityForResult with the panel intent. There is no
|
// AudioEffect expects you to use startActivityForResult with the panel intent. There is no
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert milliseconds into deci-seconds (1/10th of a second).
|
* Convert milliseconds into deci-seconds (1/10th of a second).
|
||||||
|
@ -58,7 +57,7 @@ fun Long.secsToMs() = times(1000)
|
||||||
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
|
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
// * Format a deci-second value (1/10th of a second) into a string duration.
|
* // * Format a deci-second value (1/10th of a second) into a string duration.
|
||||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||||
* will be returned if the second value is 0.
|
* will be returned if the second value is 0.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -64,7 +64,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
val repeatMode: StateFlow<RepeatMode>
|
val repeatMode: StateFlow<RepeatMode>
|
||||||
get() = _repeatMode
|
get() = _repeatMode
|
||||||
private val _isShuffled = MutableStateFlow(false)
|
private val _isShuffled = MutableStateFlow(false)
|
||||||
/** Whether the queue is shuffled or not. */
|
/** Whether the queue is shuffled or not. */
|
||||||
val isShuffled: StateFlow<Boolean>
|
val isShuffled: StateFlow<Boolean>
|
||||||
get() = _isShuffled
|
get() = _isShuffled
|
||||||
|
|
||||||
|
@ -152,8 +152,8 @@ class PlaybackViewModel(application: Application) :
|
||||||
/**
|
/**
|
||||||
* Play a [Song] from one of it's [Artist]s.
|
* Play a [Song] from one of it's [Artist]s.
|
||||||
* @param song The [Song] to play.
|
* @param song The [Song] to play.
|
||||||
* @param artist The [Artist] to play from. Must be linked to the [Song]. If null, the user
|
* @param artist The [Artist] to play from. Must be linked to the [Song]. If null, the user will
|
||||||
* will be prompted on what artist to play. Defaults to null.
|
* be prompted on what artist to play. Defaults to null.
|
||||||
*/
|
*/
|
||||||
fun playFromArtist(song: Song, artist: Artist? = null) {
|
fun playFromArtist(song: Song, artist: Artist? = null) {
|
||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
|
@ -234,8 +234,8 @@ class PlaybackViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used
|
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to
|
||||||
* to enqueue a playback action at startup to then occur when the music library is fully loaded.
|
* enqueue a playback action at startup to then occur when the music library is fully loaded.
|
||||||
* @param action The [InternalPlayer.Action] to perform eventually.
|
* @param action The [InternalPlayer.Action] to perform eventually.
|
||||||
*/
|
*/
|
||||||
fun startAction(action: InternalPlayer.Action) {
|
fun startAction(action: InternalPlayer.Action) {
|
||||||
|
|
|
@ -53,7 +53,4 @@ enum class ReplayGainMode {
|
||||||
* @param without The pre-amp (in dB) to use when ReplayGain tags are not present.
|
* @param without The pre-amp (in dB) to use when ReplayGain tags are not present.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class ReplayGainPreAmp(
|
data class ReplayGainPreAmp(val with: Float, val without: Float)
|
||||||
val with: Float,
|
|
||||||
val without: Float
|
|
||||||
)
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the volume adjustment based on the given [Metadata].
|
* Updates the volume adjustment based on the given [Metadata].
|
||||||
* @param metadata The [Metadata] of the currently playing track, or null if the track
|
* @param metadata The [Metadata] of the currently playing track, or null if the track has no
|
||||||
* has no [Metadata].
|
* [Metadata].
|
||||||
*/
|
*/
|
||||||
fun applyReplayGain(metadata: Metadata?) {
|
fun applyReplayGain(metadata: Metadata?) {
|
||||||
// TODO: Allow this to automatically obtain it's own [Metadata].
|
// TODO: Allow this to automatically obtain it's own [Metadata].
|
||||||
|
@ -155,11 +155,12 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
||||||
// Grok a float from a ReplayGain tag by removing everything that is not 0-9, ,
|
// Grok a float from a ReplayGain tag by removing everything that is not 0-9, ,
|
||||||
// or -.
|
// or -.
|
||||||
// Derived from vanilla music: https://github.com/vanilla-music/vanilla
|
// Derived from vanilla music: https://github.com/vanilla-music/vanilla
|
||||||
val gainValue = try {
|
val gainValue =
|
||||||
value.replace(Regex("[^\\d.-]"), "").toFloat()
|
try {
|
||||||
} catch (e: Exception) {
|
value.replace(Regex("[^\\d.-]"), "").toFloat()
|
||||||
0f
|
} catch (e: Exception) {
|
||||||
}
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
tags.add(GainTag(unlikelyToBeNull(key), gainValue))
|
tags.add(GainTag(unlikelyToBeNull(key), gainValue))
|
||||||
}
|
}
|
||||||
|
@ -250,7 +251,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Always read a little-endian [Short] from the [ByteBuffer] at the given index.
|
* Always read a little-endian [Short] from the [ByteBuffer] at the given index.
|
||||||
* @param at The index to read the [Short] from.
|
* @param at The index to read the [Short] from.
|
||||||
*/
|
*/
|
||||||
private fun ByteBuffer.getLeShort(at: Int) =
|
private fun ByteBuffer.getLeShort(at: Int) =
|
||||||
|
@ -275,8 +276,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
||||||
/**
|
/**
|
||||||
* A raw ReplayGain adjustment.
|
* A raw ReplayGain adjustment.
|
||||||
* @param key The tag's key.
|
* @param key The tag's key.
|
||||||
* @param value The tag's adjustment, in dB.
|
* @param value The tag's adjustment, in dB. TODO: Try to phasse this out.
|
||||||
* TODO: Try to phasse this out.
|
|
||||||
*/
|
*/
|
||||||
private data class GainTag(val key: String, val value: Float)
|
private data class GainTag(val key: String, val value: Float)
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,8 @@ interface InternalPlayer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a [State] corresponding to the current player state.
|
* Get a [State] corresponding to the current player state.
|
||||||
* @param durationMs The duration of the currently playing track, in milliseconds.
|
* @param durationMs The duration of the currently playing track, in milliseconds. Required
|
||||||
* Required since the internal player cannot obtain an accurate duration itself.
|
* since the internal player cannot obtain an accurate duration itself.
|
||||||
*/
|
*/
|
||||||
fun getState(durationMs: Long): State
|
fun getState(durationMs: Long): State
|
||||||
|
|
||||||
|
@ -67,16 +67,14 @@ interface InternalPlayer {
|
||||||
*/
|
*/
|
||||||
fun setPlaying(isPlaying: Boolean)
|
fun setPlaying(isPlaying: Boolean)
|
||||||
|
|
||||||
/**
|
/** Possible long-running background tasks handled by the background playback task. */
|
||||||
* Possible long-running background tasks handled by the background playback task.
|
|
||||||
*/
|
|
||||||
sealed class Action {
|
sealed class Action {
|
||||||
/** Restore the previously saved playback state. */
|
/** Restore the previously saved playback state. */
|
||||||
object RestoreState : Action()
|
object RestoreState : Action()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start shuffled playback of the entire music library.
|
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All"
|
||||||
* Analogous to the "Shuffle All" shortcut.
|
* shortcut.
|
||||||
*/
|
*/
|
||||||
object ShuffleAll : Action()
|
object ShuffleAll : Action()
|
||||||
|
|
||||||
|
@ -93,15 +91,15 @@ interface InternalPlayer {
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
/** Whether the player is actively playing audio in this moment. */
|
/** Whether the player is actively playing audio in this moment. */
|
||||||
private val isAdvancing: Boolean,
|
private val isAdvancing: Boolean,
|
||||||
/** The position when this instance was created, in milliseconds. */
|
/** The position when this instance was created, in milliseconds. */
|
||||||
private val initPositionMs: Long,
|
private val initPositionMs: Long,
|
||||||
/** The time this instance was created, as a unix epoch timestamp. */
|
/** The time this instance was created, as a unix epoch timestamp. */
|
||||||
private val creationTime: Long
|
private val creationTime: Long
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Calculate the "real" playback position this instance contains, in milliseconds.
|
* Calculate the "real" playback position this instance contains, in milliseconds.
|
||||||
* @return If paused, the original position will be returned. Otherwise, it will be
|
* @return If paused, the original position will be returned. Otherwise, it will be the
|
||||||
* the original position plus the time elapsed since this state was created.
|
* original position plus the time elapsed since this state was created.
|
||||||
*/
|
*/
|
||||||
fun calculateElapsedPositionMs() =
|
fun calculateElapsedPositionMs() =
|
||||||
if (isAdvancing) {
|
if (isAdvancing) {
|
||||||
|
@ -154,8 +152,8 @@ interface InternalPlayer {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Create a new instance.
|
* Create a new instance.
|
||||||
* @param isPlaying Whether the player is actively playing audio or set to play audio
|
* @param isPlaying Whether the player is actively playing audio or set to play audio in
|
||||||
* in the future.
|
* the future.
|
||||||
* @param isAdvancing Whether the player is actively playing audio in this moment.
|
* @param isAdvancing Whether the player is actively playing audio in this moment.
|
||||||
* @param positionMs The current position of the player.
|
* @param positionMs The current position of the player.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -216,8 +216,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A lower-level form of [SavedState] that contains additional information to create
|
* A lower-level form of [SavedState] that contains additional information to create a more
|
||||||
* a more reliable restoration process.
|
* reliable restoration process.
|
||||||
*/
|
*/
|
||||||
private data class RawState(
|
private data class RawState(
|
||||||
/** @see SavedState.index */
|
/** @see SavedState.index */
|
||||||
|
@ -229,9 +229,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
/** @see SavedState.isShuffled */
|
/** @see SavedState.isShuffled */
|
||||||
val isShuffled: Boolean,
|
val isShuffled: Boolean,
|
||||||
/**
|
/**
|
||||||
* The [Music.UID] of the [Song] that was originally in the queue at [index].
|
* The [Music.UID] of the [Song] that was originally in the queue at [index]. This can be
|
||||||
* This can be used to restore the currently playing item in the queue if
|
* used to restore the currently playing item in the queue if the index mapping changed.
|
||||||
* the index mapping changed.
|
|
||||||
*/
|
*/
|
||||||
val songUid: Music.UID,
|
val songUid: Music.UID,
|
||||||
/** @see SavedState.parent */
|
/** @see SavedState.parent */
|
||||||
|
|
|
@ -57,6 +57,7 @@ class PlaybackStateManager private constructor() {
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
private var internalPlayer: InternalPlayer? = null
|
private var internalPlayer: InternalPlayer? = null
|
||||||
private var pendingAction: InternalPlayer.Action? = null
|
private var pendingAction: InternalPlayer.Action? = null
|
||||||
|
private var isInitialized = false
|
||||||
|
|
||||||
/** The currently playing [Song]. Null if nothing is playing. */
|
/** The currently playing [Song]. Null if nothing is playing. */
|
||||||
val song
|
val song
|
||||||
|
@ -84,9 +85,6 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Whether the queue is shuffled. */
|
/** Whether the queue is shuffled. */
|
||||||
var isShuffled = false
|
var isShuffled = false
|
||||||
private set
|
private set
|
||||||
/** Whether this instance has played something. */
|
|
||||||
var isInitialized = false
|
|
||||||
private set
|
|
||||||
/**
|
/**
|
||||||
* The current audio session ID of the internal player. Null if no [InternalPlayer] is
|
* The current audio session ID of the internal player. Null if no [InternalPlayer] is
|
||||||
* available.
|
* available.
|
||||||
|
@ -94,11 +92,9 @@ class PlaybackStateManager private constructor() {
|
||||||
val currentAudioSessionId: Int?
|
val currentAudioSessionId: Int?
|
||||||
get() = internalPlayer?.audioSessionId
|
get() = internalPlayer?.audioSessionId
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [Callback] to this instance. This can be used to receive changes in the playback
|
* Add a [Callback] to this instance. This can be used to receive changes in the playback state.
|
||||||
* state. Will immediately invoke [Callback] methods to initialize the instance with the
|
* Will immediately invoke [Callback] methods to initialize the instance with the current state.
|
||||||
* current state.
|
|
||||||
* @param callback The [Callback] to add.
|
* @param callback The [Callback] to add.
|
||||||
* @see Callback
|
* @see Callback
|
||||||
*/
|
*/
|
||||||
|
@ -129,7 +125,8 @@ class PlaybackStateManager private constructor() {
|
||||||
* Register an [InternalPlayer] for this instance. This instance will handle translating the
|
* Register an [InternalPlayer] for this instance. This instance will handle translating the
|
||||||
* current playback state into audio playback. There can be only one [InternalPlayer] at a time.
|
* current playback state into audio playback. There can be only one [InternalPlayer] at a time.
|
||||||
* Will invoke [InternalPlayer] methods to initialize the instance with the current state.
|
* Will invoke [InternalPlayer] methods to initialize the instance with the current state.
|
||||||
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already registered.
|
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already
|
||||||
|
* registered.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun registerInternalPlayer(internalPlayer: InternalPlayer) {
|
fun registerInternalPlayer(internalPlayer: InternalPlayer) {
|
||||||
|
@ -201,8 +198,8 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to the next [Song] in the queue. Will go to the first [Song] in the queue if there
|
* Go to the next [Song] in the queue. Will go to the first [Song] in the queue if there is no
|
||||||
* is no [Song] ahead to skip to.
|
* [Song] ahead to skip to.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun next() {
|
fun next() {
|
||||||
|
@ -217,8 +214,8 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to the previous [Song] in the queue. Will rewind if there are no previous [Song]s
|
* Go to the previous [Song] in the queue. Will rewind if there are no previous [Song]s to skip
|
||||||
* to skip to, or if configured to do so.
|
* to, or if configured to do so.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun prev() {
|
fun prev() {
|
||||||
|
@ -367,7 +364,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronize the state of this instance with the current [InternalPlayer].
|
* Synchronize the state of this instance with the current [InternalPlayer].
|
||||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -400,7 +397,7 @@ class PlaybackStateManager private constructor() {
|
||||||
/**
|
/**
|
||||||
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
|
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
|
||||||
* [InternalPlayer].
|
* [InternalPlayer].
|
||||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -433,9 +430,7 @@ class PlaybackStateManager private constructor() {
|
||||||
internalPlayer?.seekTo(positionMs)
|
internalPlayer?.seekTo(positionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Rewind to the beginning of the currently playing [Song]. */
|
||||||
* Rewind to the beginning of the currently playing [Song].
|
|
||||||
*/
|
|
||||||
fun rewind() = seekTo(0)
|
fun rewind() = seekTo(0)
|
||||||
|
|
||||||
// --- PERSISTENCE FUNCTIONS ---
|
// --- PERSISTENCE FUNCTIONS ---
|
||||||
|
@ -501,14 +496,16 @@ class PlaybackStateManager private constructor() {
|
||||||
logD("Saving state to DB")
|
logD("Saving state to DB")
|
||||||
|
|
||||||
// Create the saved state from the current playback state.
|
// Create the saved state from the current playback state.
|
||||||
val state = synchronized(this) {
|
val state =
|
||||||
PlaybackStateDatabase.SavedState(
|
synchronized(this) {
|
||||||
index = index,
|
PlaybackStateDatabase.SavedState(
|
||||||
parent = parent,
|
index = index,
|
||||||
queue = _queue,
|
parent = parent,
|
||||||
positionMs = playerState.calculateElapsedPositionMs(),
|
queue = _queue,
|
||||||
isShuffled = isShuffled,
|
positionMs = playerState.calculateElapsedPositionMs(),
|
||||||
repeatMode = repeatMode) }
|
isShuffled = isShuffled,
|
||||||
|
repeatMode = repeatMode)
|
||||||
|
}
|
||||||
return try {
|
return try {
|
||||||
withContext(Dispatchers.IO) { database.write(state) }
|
withContext(Dispatchers.IO) { database.write(state) }
|
||||||
true
|
true
|
||||||
|
@ -636,8 +633,8 @@ class PlaybackStateManager private constructor() {
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
interface Callback {
|
||||||
/**
|
/**
|
||||||
* Called when the position of the currently playing item has changed, changing the
|
* Called when the position of the currently playing item has changed, changing the current
|
||||||
* current [Song], but no other queue attribute has changed.
|
* [Song], but no other queue attribute has changed.
|
||||||
* @param index The new position in the queue.
|
* @param index The new position in the queue.
|
||||||
*/
|
*/
|
||||||
fun onIndexMoved(index: Int) {}
|
fun onIndexMoved(index: Int) {}
|
||||||
|
@ -649,8 +646,8 @@ class PlaybackStateManager private constructor() {
|
||||||
fun onQueueChanged(queue: List<Song>) {}
|
fun onQueueChanged(queue: List<Song>) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the queue has changed in a non-trivial manner (such as re-shuffling),
|
* Called when the queue has changed in a non-trivial manner (such as re-shuffling), but the
|
||||||
* but the currently playing [Song] has not.
|
* currently playing [Song] has not.
|
||||||
* @param index The new position in the queue.
|
* @param index The new position in the queue.
|
||||||
*/
|
*/
|
||||||
fun onQueueReworked(index: Int, queue: List<Song>) {}
|
fun onQueueReworked(index: Int, queue: List<Song>) {}
|
||||||
|
@ -659,8 +656,7 @@ class PlaybackStateManager private constructor() {
|
||||||
* Called when a new playback configuration was created.
|
* Called when a new playback configuration was created.
|
||||||
* @param index The new position in the queue.
|
* @param index The new position in the queue.
|
||||||
* @param queue The new queue.
|
* @param queue The new queue.
|
||||||
* @param parent The new [MusicParent] being played from, or null if playing from all
|
* @param parent The new [MusicParent] being played from, or null if playing from all songs.
|
||||||
* songs.
|
|
||||||
*/
|
*/
|
||||||
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
||||||
|
|
||||||
|
@ -677,8 +673,8 @@ class PlaybackStateManager private constructor() {
|
||||||
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the queue's shuffle state changes. Handling the queue change itself
|
* Called when the queue's shuffle state changes. Handling the queue change itself should
|
||||||
* should occur in [onQueueReworked],
|
* occur in [onQueueReworked],
|
||||||
* @param isShuffled Whether the queue is shuffled.
|
* @param isShuffled Whether the queue is shuffled.
|
||||||
*/
|
*/
|
||||||
fun onShuffledChanged(isShuffled: Boolean) {}
|
fun onShuffledChanged(isShuffled: Boolean) {}
|
||||||
|
|
|
@ -31,8 +31,8 @@ enum class RepeatMode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeat the whole queue. Songs are played immediately, and playback continues when the
|
* Repeat the whole queue. Songs are played immediately, and playback continues when the queue
|
||||||
* queue repeats.
|
* repeats.
|
||||||
*/
|
*/
|
||||||
ALL,
|
ALL,
|
||||||
|
|
||||||
|
|
|
@ -74,8 +74,8 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release this instance, closing the [MediaSessionCompat] and preventing any
|
* Release this instance, closing the [MediaSessionCompat] and preventing any further updates to
|
||||||
* further updates to the [NotificationComponent].
|
* the [NotificationComponent].
|
||||||
*/
|
*/
|
||||||
fun release() {
|
fun release() {
|
||||||
provider.release()
|
provider.release()
|
||||||
|
@ -246,10 +246,10 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
/**
|
/**
|
||||||
* Upload a new [MediaMetadataCompat] based on the current playback state to the
|
* Upload a new [MediaMetadataCompat] based on the current playback state to the
|
||||||
* [MediaSessionCompat] and [NotificationComponent].
|
* [MediaSessionCompat] and [NotificationComponent].
|
||||||
* @param song The current [Song] to create the [MediaMetadataCompat] from, or null if no
|
* @param song The current [Song] to create the [MediaMetadataCompat] from, or null if no [Song]
|
||||||
* [Song] is currently playing.
|
* is currently playing.
|
||||||
* @param parent The current [MusicParent] to create the [MediaMetadataCompat] from, or null
|
* @param parent The current [MusicParent] to create the [MediaMetadataCompat] from, or null if
|
||||||
* if playback is currently occuring from all songs.
|
* playback is currently occuring from all songs.
|
||||||
*/
|
*/
|
||||||
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
|
@ -342,8 +342,9 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
logD("Updating media session playback state")
|
logD("Updating media session playback state")
|
||||||
|
|
||||||
val state =
|
val state =
|
||||||
// InternalPlayer.State handles position/state information.
|
// InternalPlayer.State handles position/state information.
|
||||||
playbackManager.playerState.intoPlaybackState(PlaybackStateCompat.Builder())
|
playbackManager.playerState
|
||||||
|
.intoPlaybackState(PlaybackStateCompat.Builder())
|
||||||
.setActions(ACTIONS)
|
.setActions(ACTIONS)
|
||||||
// Active queue ID corresponds to the indices we populated prior, use them here.
|
// Active queue ID corresponds to the indices we populated prior, use them here.
|
||||||
.setActiveQueueItemId(playbackManager.index.toLong())
|
.setActiveQueueItemId(playbackManager.index.toLong())
|
||||||
|
@ -396,9 +397,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** An interface for handling changes in the notification configuration. */
|
||||||
* An interface for handling changes in the notification configuration.
|
|
||||||
*/
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
/**
|
/**
|
||||||
* Called when the [NotificationComponent] changes, requiring it to be re-posed.
|
* Called when the [NotificationComponent] changes, requiring it to be re-posed.
|
||||||
|
|
|
@ -34,9 +34,8 @@ import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The playback notification component. Due to race conditions regarding notification
|
* The playback notification component. Due to race conditions regarding notification updates, this
|
||||||
* updates, this component is not self-sufficient. [MediaSessionComponent] should be used
|
* component is not self-sufficient. [MediaSessionComponent] should be used instead of manage it.
|
||||||
* instead of manage it.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
@ -115,11 +114,12 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
context: Context,
|
context: Context,
|
||||||
isPlaying: Boolean
|
isPlaying: Boolean
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val drawableRes = if (isPlaying) {
|
val drawableRes =
|
||||||
R.drawable.ic_pause_24
|
if (isPlaying) {
|
||||||
} else {
|
R.drawable.ic_pause_24
|
||||||
R.drawable.ic_play_24
|
} else {
|
||||||
}
|
R.drawable.ic_play_24
|
||||||
|
}
|
||||||
return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes)
|
return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,21 +134,19 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
context: Context,
|
context: Context,
|
||||||
isShuffled: Boolean
|
isShuffled: Boolean
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val drawableRes = if (isShuffled) {
|
val drawableRes =
|
||||||
R.drawable.ic_shuffle_on_24
|
if (isShuffled) {
|
||||||
} else {
|
R.drawable.ic_shuffle_on_24
|
||||||
R.drawable.ic_shuffle_off_24
|
} else {
|
||||||
}
|
R.drawable.ic_shuffle_off_24
|
||||||
|
}
|
||||||
return buildAction(context, PlaybackService.ACTION_INVERT_SHUFFLE, drawableRes)
|
return buildAction(context, PlaybackService.ACTION_INVERT_SHUFFLE, drawableRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAction(
|
private fun buildAction(context: Context, actionName: String, @DrawableRes iconRes: Int) =
|
||||||
context: Context,
|
|
||||||
actionName: String,
|
|
||||||
@DrawableRes iconRes: Int
|
|
||||||
) =
|
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
iconRes, actionName, context.newBroadcastPendingIntent(actionName)).build()
|
iconRes, actionName, context.newBroadcastPendingIntent(actionName))
|
||||||
|
.build()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Notification channel used by solely the playback notification. */
|
/** Notification channel used by solely the playback notification. */
|
||||||
|
|
|
@ -141,7 +141,8 @@ class PlaybackService :
|
||||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
.build(),
|
.build(),
|
||||||
true)
|
true)
|
||||||
.build().also { it.addListener(this) }
|
.build()
|
||||||
|
.also { it.addListener(this) }
|
||||||
// Initialize the core service components
|
// Initialize the core service components
|
||||||
settings = Settings(this, this)
|
settings = Settings(this, this)
|
||||||
foregroundManager = ForegroundManager(this)
|
foregroundManager = ForegroundManager(this)
|
||||||
|
@ -163,8 +164,7 @@ class PlaybackService :
|
||||||
addAction(ACTION_SKIP_NEXT)
|
addAction(ACTION_SKIP_NEXT)
|
||||||
addAction(ACTION_EXIT)
|
addAction(ACTION_EXIT)
|
||||||
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
logD("Service created")
|
logD("Service created")
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,8 @@ class PlaybackService :
|
||||||
|
|
||||||
override fun getState(durationMs: Long) =
|
override fun getState(durationMs: Long) =
|
||||||
InternalPlayer.State.new(
|
InternalPlayer.State.new(
|
||||||
player.playWhenReady, player.isPlaying,
|
player.playWhenReady,
|
||||||
|
player.isPlaying,
|
||||||
// The position value can be below zero or past the expected duration, make
|
// The position value can be below zero or past the expected duration, make
|
||||||
// sure we handle that.
|
// sure we handle that.
|
||||||
player.currentPosition.coerceAtLeast(0).coerceAtMost(durationMs))
|
player.currentPosition.coerceAtLeast(0).coerceAtMost(durationMs))
|
||||||
|
@ -273,9 +274,9 @@ class PlaybackService :
|
||||||
// Any change to the analogous isPlaying, isAdvancing, or positionMs values require
|
// Any change to the analogous isPlaying, isAdvancing, or positionMs values require
|
||||||
// us to synchronize with a new state.
|
// us to synchronize with a new state.
|
||||||
if (events.containsAny(
|
if (events.containsAny(
|
||||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||||
Player.EVENT_IS_PLAYING_CHANGED,
|
Player.EVENT_IS_PLAYING_CHANGED,
|
||||||
Player.EVENT_POSITION_DISCONTINUITY)) {
|
Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||||
playbackManager.synchronizeState(this)
|
playbackManager.synchronizeState(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,7 +364,8 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performAction(action: InternalPlayer.Action): Boolean {
|
override fun performAction(action: InternalPlayer.Action): Boolean {
|
||||||
val library = musicStore.library
|
val library =
|
||||||
|
musicStore.library
|
||||||
// No library, cannot do anything.
|
// No library, cannot do anything.
|
||||||
?: return false
|
?: return false
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,7 @@ import android.widget.FrameLayout
|
||||||
/**
|
/**
|
||||||
* A [FrameLayout] that programmatically overrides the child layout to a left-to-right (LTR) layout
|
* A [FrameLayout] that programmatically overrides the child layout to a left-to-right (LTR) layout
|
||||||
* direction. This is useful for "Timeline" elements that Material Design recommends be LTR in all
|
* direction. This is useful for "Timeline" elements that Material Design recommends be LTR in all
|
||||||
* cases. This layout can only contain one child, to prevent conflicts with other layout
|
* cases. This layout can only contain one child, to prevent conflicts with other layout components.
|
||||||
* components.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
open class ForcedLTRFrameLayout
|
open class ForcedLTRFrameLayout
|
||||||
|
|
|
@ -110,8 +110,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
/** A listener for SeekBar interactions. */
|
/** A listener for SeekBar interactions. */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Called when the internal [Slider] was scrubbed to a new position, requesting that
|
* Called when the internal [Slider] was scrubbed to a new position, requesting that a seek
|
||||||
* a seek be performed.
|
* be performed.
|
||||||
* @param positionDs The position to seek to, in deci-seconds (1/10th of a second).
|
* @param positionDs The position to seek to, in deci-seconds (1/10th of a second).
|
||||||
*/
|
*/
|
||||||
fun onSeekConfirmed(positionDs: Long)
|
fun onSeekConfirmed(positionDs: Long)
|
||||||
|
|
|
@ -73,7 +73,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable.
|
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable.
|
||||||
*/
|
*/
|
||||||
fun expandWithRecycler(recycler: RecyclerView?) {
|
fun expandWithRecycler(recycler: RecyclerView?) {
|
||||||
// TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
|
// TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView]
|
||||||
|
// argument?
|
||||||
setExpanded(true)
|
setExpanded(true)
|
||||||
recycler?.let { addOnOffsetChangedListener(ExpansionHackListener(it)) }
|
recycler?.let { addOnOffsetChangedListener(ExpansionHackListener(it)) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
@ -5,7 +22,6 @@ import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is
|
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is
|
||||||
* loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
|
* loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
|
||||||
|
@ -22,22 +38,23 @@ inline fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R)
|
||||||
* @param schema A block that adds a comma-separated list of SQL column declarations.
|
* @param schema A block that adds a comma-separated list of SQL column declarations.
|
||||||
*/
|
*/
|
||||||
inline fun SQLiteDatabase.createTable(name: String, schema: StringBuilder.() -> StringBuilder) {
|
inline fun SQLiteDatabase.createTable(name: String, schema: StringBuilder.() -> StringBuilder) {
|
||||||
val command = StringBuilder()
|
val command = StringBuilder().append("CREATE TABLE IF NOT EXISTS $name(").schema().append(")")
|
||||||
.append("CREATE TABLE IF NOT EXISTS $name(")
|
|
||||||
.schema()
|
|
||||||
.append(")")
|
|
||||||
execSQL(command.toString())
|
execSQL(command.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely write a list of items to an [SQLiteDatabase]. This will clear the prior list and write
|
* Safely write a list of items to an [SQLiteDatabase]. This will clear the prior list and write as
|
||||||
* as much of the new list as possible.
|
* much of the new list as possible.
|
||||||
* @param list The list of items to write.
|
* @param list The list of items to write.
|
||||||
* @param tableName The name of the table to write the items to.
|
* @param tableName The name of the table to write the items to.
|
||||||
* @param transform Code to transform an item into a corresponding [ContentValues] to the given
|
* @param transform Code to transform an item into a corresponding [ContentValues] to the given
|
||||||
* table.
|
* table.
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> SQLiteDatabase.writeList(list: List<T>, tableName: String, transform: (Int, T) -> ContentValues) {
|
inline fun <reified T> SQLiteDatabase.writeList(
|
||||||
|
list: List<T>,
|
||||||
|
tableName: String,
|
||||||
|
transform: (Int, T) -> ContentValues
|
||||||
|
) {
|
||||||
// Clear any prior items in the table.
|
// Clear any prior items in the table.
|
||||||
transaction { delete(tableName, null, null) }
|
transaction { delete(tableName, null, null) }
|
||||||
|
|
||||||
|
@ -50,14 +67,15 @@ inline fun <reified T> SQLiteDatabase.writeList(list: List<T>, tableName: String
|
||||||
transaction {
|
transaction {
|
||||||
while (i < list.size) {
|
while (i < list.size) {
|
||||||
val values = transform(i, list[i])
|
val values = transform(i, list[i])
|
||||||
// Increment forward now so that if this insert fails, the transactionPosition
|
// Increment forward now so that if this insert fails, the transaction position
|
||||||
// will still start at the next i.
|
// will still start at the next i.
|
||||||
i++
|
i++
|
||||||
insert(tableName, null, values)
|
insert(tableName, null, values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transactionPosition = i
|
transactionPosition = i
|
||||||
logD("Wrote batch of ${T::class.simpleName} instances. " +
|
logD(
|
||||||
"Position is now at $transactionPosition")
|
"Wrote batch of ${T::class.simpleName} instances. " +
|
||||||
|
"Position is now at $transactionPosition")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -30,7 +27,6 @@ import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.database.sqlite.transaction
|
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
|
|
@ -65,7 +65,7 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
||||||
* Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material
|
* Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material
|
||||||
* Files: https://github.com/zhanghai/MaterialFiles
|
* Files: https://github.com/zhanghai/MaterialFiles
|
||||||
* @param clazz The [KClass] to reflect into.
|
* @param clazz The [KClass] to reflect into.
|
||||||
* @param field The name of the method to obtain.
|
* @param method The name of the method to obtain.
|
||||||
*/
|
*/
|
||||||
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
||||||
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
|
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
|
||||||
|
|
|
@ -217,9 +217,9 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
// widgets.
|
// widgets.
|
||||||
val background =
|
val background =
|
||||||
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
R.drawable.ui_widget_bar_round
|
R.drawable.ui_widget_bg_round
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ui_widget_bar_system
|
R.drawable.ui_widget_bg_system
|
||||||
}
|
}
|
||||||
setBackgroundResource(android.R.id.background, background)
|
setBackgroundResource(android.R.id.background, background)
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -80,7 +80,7 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the app widget layouts corresponding to the given [AppWidgetProvider] [ComponentName] with
|
* Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with
|
||||||
* an adaptive layout, in a version-compatible manner.
|
* an adaptive layout, in a version-compatible manner.
|
||||||
* @param context [Context] required to backport adaptive layout behavior.
|
* @param context [Context] required to backport adaptive layout behavior.
|
||||||
* @param component [ComponentName] of the app widget layout to update.
|
* @param component [ComponentName] of the app widget layout to update.
|
||||||
|
|
|
@ -129,12 +129,10 @@
|
||||||
<string name="fmt_lib_song_count">Canciones cargadas: %d</string>
|
<string name="fmt_lib_song_count">Canciones cargadas: %d</string>
|
||||||
<plurals name="fmt_song_count">
|
<plurals name="fmt_song_count">
|
||||||
<item quantity="one">%d canción</item>
|
<item quantity="one">%d canción</item>
|
||||||
<item quantity="many">%d canciones</item>
|
|
||||||
<item quantity="other">%d canciones</item>
|
<item quantity="other">%d canciones</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="fmt_album_count">
|
<plurals name="fmt_album_count">
|
||||||
<item quantity="one">%d álbum</item>
|
<item quantity="one">%d álbum</item>
|
||||||
<item quantity="many">%d álbumes</item>
|
|
||||||
<item quantity="other">%d álbumes</item>
|
<item quantity="other">%d álbumes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="lbl_size">Tamaño</string>
|
<string name="lbl_size">Tamaño</string>
|
||||||
|
|
|
@ -136,12 +136,10 @@
|
||||||
<string name="fmt_lib_total_duration">Durata totale: %s</string>
|
<string name="fmt_lib_total_duration">Durata totale: %s</string>
|
||||||
<plurals name="fmt_song_count">
|
<plurals name="fmt_song_count">
|
||||||
<item quantity="one">%d canzone</item>
|
<item quantity="one">%d canzone</item>
|
||||||
<item quantity="many">%d canzoni</item>
|
|
||||||
<item quantity="other">%d canzoni</item>
|
<item quantity="other">%d canzoni</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="fmt_album_count">
|
<plurals name="fmt_album_count">
|
||||||
<item quantity="one">%d disco</item>
|
<item quantity="one">%d disco</item>
|
||||||
<item quantity="many">%d dischi</item>
|
|
||||||
<item quantity="other">%d dischi</item>
|
<item quantity="other">%d dischi</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="set_dirs_mode">Modo</string>
|
<string name="set_dirs_mode">Modo</string>
|
||||||
|
@ -252,7 +250,6 @@
|
||||||
<string name="set_separators_plus">Più (+)</string>
|
<string name="set_separators_plus">Più (+)</string>
|
||||||
<plurals name="fmt_artist_count">
|
<plurals name="fmt_artist_count">
|
||||||
<item quantity="one">%d artista</item>
|
<item quantity="one">%d artista</item>
|
||||||
<item quantity="many">%d artisti</item>
|
|
||||||
<item quantity="other">%d artisti</item>
|
<item quantity="other">%d artisti</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="set_rescan">Riscansiona musica</string>
|
<string name="set_rescan">Riscansiona musica</string>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources />
|
|
@ -63,12 +63,10 @@
|
||||||
<string name="fmt_lib_song_count">Músicas carregadas: %d</string>
|
<string name="fmt_lib_song_count">Músicas carregadas: %d</string>
|
||||||
<plurals name="fmt_song_count">
|
<plurals name="fmt_song_count">
|
||||||
<item quantity="one">%d música</item>
|
<item quantity="one">%d música</item>
|
||||||
<item quantity="many">%d músicas</item>
|
|
||||||
<item quantity="other">%d músicas</item>
|
<item quantity="other">%d músicas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="fmt_album_count">
|
<plurals name="fmt_album_count">
|
||||||
<item quantity="one">%d álbum</item>
|
<item quantity="one">%d álbum</item>
|
||||||
<item quantity="many">%d álbuns</item>
|
|
||||||
<item quantity="other">%d álbuns</item>
|
<item quantity="other">%d álbuns</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="lbl_sort_asc">Crescente</string>
|
<string name="lbl_sort_asc">Crescente</string>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<style name="Widget.Auxio.AppBarLayout" parent="Widget.Material3.AppBarLayout">
|
<style name="Widget.Auxio.AppBarLayout" parent="Widget.Material3.AppBarLayout">
|
||||||
<item name="android:layout_width">match_parent</item>
|
<item name="android:layout_width">match_parent</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<!-- Resolve lifted state flickering when scrolling fast. -->
|
||||||
<item name="android:stateListAnimator">@null</item>
|
<item name="android:stateListAnimator">@null</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue