Reinstate package name

Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
This commit is contained in:
Muntashir Al-Islam 2023-03-15 00:26:47 +06:00
parent 9971c25649
commit 2845945763
478 changed files with 2648 additions and 2613 deletions

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic
import android.app.Application
import cat.ereza.customactivityoncrash.config.CaocConfig
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.activities.ErrorActivity
import code.name.monkey.retromusic.appshortcuts.DynamicShortcutManager
import code.name.monkey.retromusic.helper.WallpaperAccentManager
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
class App : Application() {
private val wallpaperAccentManager = WallpaperAccentManager(this)
override fun onCreate() {
super.onCreate()
instance = this
startKoin {
androidContext(this@App)
modules(appModules)
}
// default theme
if (!ThemeStore.isConfigured(this, 3)) {
ThemeStore.editTheme(this)
.accentColorRes(R.color.md_deep_purple_A200)
.coloredNavigationBar(true)
.commit()
}
wallpaperAccentManager.init()
if (VersionUtils.hasNougatMR())
DynamicShortcutManager(this).initDynamicShortcuts()
// setting Error activity
CaocConfig.Builder.create().errorActivity(ErrorActivity::class.java).apply()
}
override fun onTerminate() {
super.onTerminate()
wallpaperAccentManager.release()
}
companion object {
private var instance: App? = null
fun getContext(): App {
return instance!!
}
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic
import android.provider.BaseColumns
import android.provider.MediaStore
object Constants {
const val TRANSLATE = "https://crowdin.com/project/retromusicplayer"
const val GITHUB_PROJECT = "https://github.com/MuntashirAkon/Metro"
const val TELEGRAM_CHANGE_LOG = "https://t.me/AppManagerChannel"
const val USER_PROFILE = "profile.jpg"
const val USER_BANNER = "banner.jpg"
const val FAQ_LINK = "https://github.com/MuntashirAkon/Metro/blob/master/FAQ.md"
const val IS_MUSIC =
MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"
const val DATA = "_data"
@Suppress("Deprecation")
val baseProjection = arrayOf(
BaseColumns._ID, // 0
MediaStore.Audio.AudioColumns.TITLE, // 1
MediaStore.Audio.AudioColumns.TRACK, // 2
MediaStore.Audio.AudioColumns.YEAR, // 3
MediaStore.Audio.AudioColumns.DURATION, // 4
DATA, // 5
MediaStore.Audio.AudioColumns.DATE_MODIFIED, // 6
MediaStore.Audio.AudioColumns.ALBUM_ID, // 7
MediaStore.Audio.AudioColumns.ALBUM, // 8
MediaStore.Audio.AudioColumns.ARTIST_ID, // 9
MediaStore.Audio.AudioColumns.ARTIST, // 10
MediaStore.Audio.AudioColumns.COMPOSER, // 11
ALBUM_ARTIST // 12
)
const val NUMBER_OF_TOP_TRACKS = 99
}
const val EXTRA_PLAYLIST_TYPE = "type"
const val EXTRA_GENRE = "extra_genre"
const val EXTRA_PLAYLIST = "extra_playlist"
const val EXTRA_PLAYLIST_ID = "extra_playlist_id"
const val EXTRA_ALBUM_ID = "extra_album_id"
const val EXTRA_ARTIST_ID = "extra_artist_id"
const val EXTRA_SONG = "extra_songs"
const val EXTRA_PLAYLISTS = "extra_playlists"
const val LIBRARY_CATEGORIES = "library_categories"
const val EXTRA_SONG_INFO = "extra_song_info"
const val DESATURATED_COLOR = "desaturated_color"
const val BLACK_THEME = "black_theme"
const val KEEP_SCREEN_ON = "keep_screen_on"
const val TOGGLE_HOME_BANNER = "toggle_home_banner"
const val NOW_PLAYING_SCREEN_ID = "now_playing_screen_id"
const val CAROUSEL_EFFECT = "carousel_effect"
const val COLORED_NOTIFICATION = "colored_notification"
const val CLASSIC_NOTIFICATION = "classic_notification"
const val GAP_LESS_PLAYBACK = "gapless_playback"
const val ALBUM_ART_ON_LOCK_SCREEN = "album_art_on_lock_screen"
const val BLURRED_ALBUM_ART = "blurred_album_art"
const val NEW_BLUR_AMOUNT = "new_blur_amount"
const val TOGGLE_HEADSET = "toggle_headset"
const val GENERAL_THEME = "general_theme"
const val ACCENT_COLOR = "accent_color"
const val SHOULD_COLOR_APP_SHORTCUTS = "should_color_app_shortcuts"
const val CIRCULAR_ALBUM_ART = "circular_album_art"
const val USER_NAME = "user_name"
const val TOGGLE_FULL_SCREEN = "toggle_full_screen"
const val TOGGLE_VOLUME = "toggle_volume"
const val ROUND_CORNERS = "corner_window"
const val TOGGLE_GENRE = "toggle_genre"
const val PROFILE_IMAGE_PATH = "profile_image_path"
const val BANNER_IMAGE_PATH = "banner_image_path"
const val ADAPTIVE_COLOR_APP = "adaptive_color_app"
const val TOGGLE_SEPARATE_LINE = "toggle_separate_line"
const val HOME_ARTIST_GRID_STYLE = "home_artist_grid_style"
const val HOME_ALBUM_GRID_STYLE = "home_album_grid_style"
const val TOGGLE_ADD_CONTROLS = "toggle_add_controls"
const val ALBUM_COVER_STYLE = "album_cover_style_id"
const val ALBUM_COVER_TRANSFORM = "album_cover_transform"
const val TAB_TEXT_MODE = "tab_text_mode"
const val LANGUAGE_NAME = "language_name"
const val SLEEP_TIMER_FINISH_SONG = "sleep_timer_finish_song"
const val ALBUM_GRID_STYLE = "album_grid_style_home"
const val ARTIST_GRID_STYLE = "artist_grid_style_home"
const val SAF_SDCARD_URI = "saf_sdcard_uri"
const val SONG_SORT_ORDER = "song_sort_order"
const val SONG_GRID_SIZE = "song_grid_size"
const val GENRE_SORT_ORDER = "genre_sort_order"
const val LAST_PAGE = "last_start_page"
const val BLUETOOTH_PLAYBACK = "bluetooth_playback"
const val INITIALIZED_BLACKLIST = "initialized_blacklist"
const val ARTIST_SORT_ORDER = "artist_sort_order"
const val ARTIST_ALBUM_SORT_ORDER = "artist_album_sort_order"
const val ALBUM_SORT_ORDER = "album_sort_order"
const val PLAYLIST_SORT_ORDER = "playlist_sort_order"
const val ALBUM_SONG_SORT_ORDER = "album_song_sort_order"
const val ARTIST_SONG_SORT_ORDER = "artist_song_sort_order"
const val ALBUM_GRID_SIZE = "album_grid_size"
const val ALBUM_GRID_SIZE_LAND = "album_grid_size_land"
const val SONG_GRID_SIZE_LAND = "song_grid_size_land"
const val ARTIST_GRID_SIZE = "artist_grid_size"
const val ARTIST_GRID_SIZE_LAND = "artist_grid_size_land"
const val PLAYLIST_GRID_SIZE = "playlist_grid_size"
const val PLAYLIST_GRID_SIZE_LAND = "playlist_grid_size_land"
const val COLORED_APP_SHORTCUTS = "colored_app_shortcuts"
const val AUDIO_DUCKING = "audio_ducking"
const val LAST_ADDED_CUTOFF = "last_added_interval"
const val LAST_SLEEP_TIMER_VALUE = "last_sleep_timer_value"
const val NEXT_SLEEP_TIMER_ELAPSED_REALTIME = "next_sleep_timer_elapsed_real_time"
const val IGNORE_MEDIA_STORE_ARTWORK = "ignore_media_store_artwork"
const val LAST_CHANGELOG_VERSION = "last_changelog_version"
const val START_DIRECTORY = "start_directory"
const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval"
const val LOCK_SCREEN = "lock_screen"
const val ALBUM_ARTISTS_ONLY = "album_artists_only"
const val ALBUM_ARTIST = "album_artist"
const val ALBUM_DETAIL_SONG_SORT_ORDER = "album_detail_song_sort_order"
const val ARTIST_DETAIL_SONG_SORT_ORDER = "artist_detail_song_sort_order"
const val LYRICS_OPTIONS = "lyrics_tab_position"
const val CHOOSE_EQUALIZER = "choose_equalizer"
const val EQUALIZER = "equalizer"
const val TOGGLE_SHUFFLE = "toggle_shuffle"
const val SONG_GRID_STYLE = "song_grid_style"
const val PAUSE_ON_ZERO_VOLUME = "pause_on_zero_volume"
const val FILTER_SONG = "filter_song"
const val EXPAND_NOW_PLAYING_PANEL = "expand_now_playing_panel"
const val EXTRA_ARTIST_NAME = "extra_artist_name"
const val TOGGLE_SUGGESTIONS = "toggle_suggestions"
const val AUDIO_FADE_DURATION = "audio_fade_duration"
const val CROSS_FADE_DURATION = "cross_fade_duration"
const val SHOW_LYRICS = "show_lyrics"
const val REMEMBER_LAST_TAB = "remember_last_tab"
const val LAST_USED_TAB = "last_used_tab"
const val WHITELIST_MUSIC = "whitelist_music"
const val MATERIAL_YOU = "material_you"
const val SNOWFALL = "snowfall"
const val LYRICS_TYPE = "lyrics_type"
const val PLAYBACK_SPEED = "playback_speed"
const val PLAYBACK_PITCH = "playback_pitch"
const val CUSTOM_FONT = "custom_font"
const val APPBAR_MODE = "appbar_mode"
const val WALLPAPER_ACCENT = "wallpaper_accent"
const val SCREEN_ON_LYRICS = "screen_on_lyrics"
const val CIRCLE_PLAY_BUTTON = "circle_play_button"
const val SWIPE_ANYWHERE_NOW_PLAYING = "swipe_anywhere_now_playing"
const val PAUSE_HISTORY = "pause_history"
const val MANAGE_AUDIO_FOCUS = "manage_audio_focus"
const val SWIPE_DOWN_DISMISS = "swipe_to_dismiss"

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic
import androidx.annotation.IntDef
@IntDef(
RECENT_ALBUMS,
TOP_ALBUMS,
RECENT_ARTISTS,
TOP_ARTISTS,
SUGGESTIONS,
FAVOURITES,
GENRES,
PLAYLISTS
)
@Retention(AnnotationRetention.SOURCE)
annotation class HomeSection
const val RECENT_ALBUMS = 3
const val TOP_ALBUMS = 1
const val RECENT_ARTISTS = 2
const val TOP_ARTISTS = 0
const val SUGGESTIONS = 5
const val FAVOURITES = 4
const val GENRES = 6
const val PLAYLISTS = 7
const val HISTORY_PLAYLIST = 8
const val LAST_ADDED_PLAYLIST = 9
const val TOP_PLAYED_PLAYLIST = 10

View file

@ -0,0 +1,25 @@
package code.name.monkey.retromusic
import android.content.Context
import android.content.ContextWrapper
import android.os.LocaleList
import code.name.monkey.appthemehelper.util.VersionUtils.hasNougatMR
import java.util.*
class LanguageContextWrapper(base: Context?) : ContextWrapper(base) {
companion object {
fun wrap(context: Context?, newLocale: Locale?): LanguageContextWrapper {
if (context == null) return LanguageContextWrapper(context)
val configuration = context.resources.configuration
if (hasNougatMR()) {
configuration.setLocale(newLocale)
val localeList = LocaleList(newLocale)
LocaleList.setDefault(localeList)
configuration.setLocales(localeList)
} else {
configuration.setLocale(newLocale)
}
return LanguageContextWrapper(context.createConfigurationContext(configuration))
}
}
}

View file

@ -0,0 +1,182 @@
package code.name.monkey.retromusic
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import code.name.monkey.retromusic.auto.AutoMusicProvider
import code.name.monkey.retromusic.db.BlackListStoreDao
import code.name.monkey.retromusic.db.BlackListStoreEntity
import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.db.RetroDatabase
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.fragments.albums.AlbumDetailsViewModel
import code.name.monkey.retromusic.fragments.artists.ArtistDetailsViewModel
import code.name.monkey.retromusic.fragments.genres.GenreDetailsViewModel
import code.name.monkey.retromusic.fragments.playlists.PlaylistDetailsViewModel
import code.name.monkey.retromusic.model.Genre
import code.name.monkey.retromusic.repository.*
import code.name.monkey.retromusic.util.FilePathUtil
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module
private val roomModule = module {
single {
Room.databaseBuilder(androidContext(), RetroDatabase::class.java, "playlist.db")
.allowMainThreadQueries()
.addCallback(object : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
GlobalScope.launch(IO) {
FilePathUtil.blacklistFilePaths().map {
get<BlackListStoreDao>().insertBlacklistPath(BlackListStoreEntity(it))
}
}
}
})
.fallbackToDestructiveMigration()
.build()
}
factory {
get<RetroDatabase>().lyricsDao()
}
factory {
get<RetroDatabase>().playlistDao()
}
factory {
get<RetroDatabase>().blackListStore()
}
factory {
get<RetroDatabase>().playCountDao()
}
factory {
get<RetroDatabase>().historyDao()
}
single {
RealRoomRepository(get(), get(), get(), get(), get())
} bind RoomRepository::class
}
private val autoModule = module {
single {
AutoMusicProvider(
androidContext(),
get(),
get(),
get(),
get(),
get(),
get()
)
}
}
private val mainModule = module {
single {
androidContext().contentResolver
}
}
private val dataModule = module {
single {
RealRepository(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
)
} bind Repository::class
single {
RealSongRepository(get())
} bind SongRepository::class
single {
RealGenreRepository(get(), get())
} bind GenreRepository::class
single {
RealAlbumRepository(get())
} bind AlbumRepository::class
single {
RealArtistRepository(get(), get())
} bind ArtistRepository::class
single {
RealPlaylistRepository(get())
} bind PlaylistRepository::class
single {
RealTopPlayedRepository(get(), get(), get(), get())
} bind TopPlayedRepository::class
single {
RealLastAddedRepository(
get(),
get(),
get()
)
} bind LastAddedRepository::class
single {
RealSearchRepository(
get(),
get(),
get(),
get(),
get()
)
}
}
private val viewModules = module {
viewModel {
LibraryViewModel(get())
}
viewModel { (albumId: Long) ->
AlbumDetailsViewModel(
get(),
albumId
)
}
viewModel { (artistId: Long?, artistName: String?) ->
ArtistDetailsViewModel(
get(),
artistId,
artistName
)
}
viewModel { (playlist: PlaylistWithSongs) ->
PlaylistDetailsViewModel(
get(),
playlist
)
}
viewModel { (genre: Genre) ->
GenreDetailsViewModel(
get(),
genre
)
}
}
val appModules = listOf(mainModule, dataModule, autoModule, viewModules, roomModule)

View file

@ -0,0 +1,262 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.animation.ObjectAnimator
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.animation.LinearInterpolator
import android.widget.SeekBar
import androidx.lifecycle.lifecycleScope
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
import code.name.monkey.retromusic.databinding.ActivityDriveModeBinding
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.drawAboveSystemBars
import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment
import code.name.monkey.retromusic.glide.BlurTransformation
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper.Callback
import code.name.monkey.retromusic.helper.PlayPauseButtonOnClickHandler
import code.name.monkey.retromusic.misc.SimpleOnSeekbarChangeListener
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.repository.RealRepository
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.util.MusicUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
/**
* Created by hemanths on 2020-02-02.
*/
class DriveModeActivity : AbsMusicServiceActivity(), Callback {
private lateinit var binding: ActivityDriveModeBinding
private var lastPlaybackControlsColor: Int = Color.GRAY
private var lastDisabledPlaybackControlsColor: Int = Color.GRAY
private lateinit var progressViewUpdateHelper: MusicProgressViewUpdateHelper
private val repository: RealRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDriveModeBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpMusicControllers()
progressViewUpdateHelper = MusicProgressViewUpdateHelper(this)
lastPlaybackControlsColor = accentColor()
binding.close.setOnClickListener {
onBackPressed()
}
binding.repeatButton.drawAboveSystemBars()
}
private fun setUpMusicControllers() {
setUpPlayPauseFab()
setUpPrevNext()
setUpRepeatButton()
setUpShuffleButton()
setUpProgressSlider()
setupFavouriteToggle()
}
private fun setupFavouriteToggle() {
binding.songFavourite.setOnClickListener {
toggleFavorite(MusicPlayerRemote.currentSong)
}
}
private fun toggleFavorite(song: Song) {
lifecycleScope.launch(Dispatchers.IO) {
val playlist = repository.favoritePlaylist()
val songEntity = song.toSongEntity(playlist.playListId)
val isFavorite = repository.isSongFavorite(song.id)
if (isFavorite) {
repository.removeSongFromPlaylist(songEntity)
} else {
repository.insertSongs(listOf(song.toSongEntity(playlist.playListId)))
}
sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
}
}
private fun updateFavorite() {
lifecycleScope.launch(Dispatchers.IO) {
val isFavorite: Boolean =
repository.isSongFavorite(MusicPlayerRemote.currentSong.id)
withContext(Dispatchers.Main) {
binding.songFavourite.setImageResource(if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border)
}
}
}
private fun setUpProgressSlider() {
binding.progressSlider.setOnSeekBarChangeListener(object : SimpleOnSeekbarChangeListener() {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
MusicPlayerRemote.seekTo(progress)
onUpdateProgressViews(
MusicPlayerRemote.songProgressMillis,
MusicPlayerRemote.songDurationMillis
)
}
}
})
}
override fun onPause() {
super.onPause()
progressViewUpdateHelper.stop()
}
override fun onResume() {
super.onResume()
progressViewUpdateHelper.start()
}
private fun setUpPrevNext() {
binding.nextButton.setOnClickListener { MusicPlayerRemote.playNextSong() }
binding.previousButton.setOnClickListener { MusicPlayerRemote.back() }
}
private fun setUpShuffleButton() {
binding.shuffleButton.setOnClickListener { MusicPlayerRemote.toggleShuffleMode() }
}
private fun setUpRepeatButton() {
binding.repeatButton.setOnClickListener { MusicPlayerRemote.cycleRepeatMode() }
}
private fun setUpPlayPauseFab() {
binding.playPauseButton.setOnClickListener(PlayPauseButtonOnClickHandler())
}
override fun onRepeatModeChanged() {
super.onRepeatModeChanged()
updateRepeatState()
}
override fun onShuffleModeChanged() {
super.onShuffleModeChanged()
updateShuffleState()
}
override fun onPlayStateChanged() {
super.onPlayStateChanged()
updatePlayPauseDrawableState()
}
override fun onServiceConnected() {
super.onServiceConnected()
updatePlayPauseDrawableState()
updateSong()
updateRepeatState()
updateShuffleState()
updateFavorite()
}
private fun updatePlayPauseDrawableState() {
if (MusicPlayerRemote.isPlaying) {
binding.playPauseButton.setImageResource(R.drawable.ic_pause)
} else {
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow)
}
}
fun updateShuffleState() {
when (MusicPlayerRemote.shuffleMode) {
MusicService.SHUFFLE_MODE_SHUFFLE -> binding.shuffleButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
else -> binding.shuffleButton.setColorFilter(
lastDisabledPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
}
private fun updateRepeatState() {
when (MusicPlayerRemote.repeatMode) {
MusicService.REPEAT_MODE_NONE -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat)
binding.repeatButton.setColorFilter(
lastDisabledPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
MusicService.REPEAT_MODE_ALL -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat)
binding.repeatButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
MusicService.REPEAT_MODE_THIS -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat_one)
binding.repeatButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
}
}
override fun onPlayingMetaChanged() {
super.onPlayingMetaChanged()
updateSong()
updateFavorite()
}
override fun onFavoriteStateChanged() {
super.onFavoriteStateChanged()
updateFavorite()
}
private fun updateSong() {
val song = MusicPlayerRemote.currentSong
binding.songTitle.text = song.title
binding.songText.text = song.artistName
GlideApp.with(this)
.load(RetroGlideExtension.getSongModel(song))
.songCoverOptions(song)
.transform(BlurTransformation.Builder(this).build())
.into(binding.image)
}
override fun onUpdateProgressViews(progress: Int, total: Int) {
binding.progressSlider.max = total
val animator = ObjectAnimator.ofInt(binding.progressSlider, "progress", progress)
animator.duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME
animator.interpolator = LinearInterpolator()
animator.start()
binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong())
binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong())
}
}

View file

@ -0,0 +1,80 @@
package code.name.monkey.retromusic.activities
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import cat.ereza.customactivityoncrash.CustomActivityOnCrash
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.util.FileUtils.createFile
import code.name.monkey.retromusic.util.Share.shareFile
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class ErrorActivity : AppCompatActivity() {
private val dayFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val ReportPrefix = "bug_report-"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.customactivityoncrash_default_error_activity)
val restartButton =
findViewById<Button>(R.id.customactivityoncrash_error_activity_restart_button)
val config = CustomActivityOnCrash.getConfigFromIntent(intent)
if (config == null) {
finish()
return
}
restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app)
restartButton.setOnClickListener {
CustomActivityOnCrash.restartApplication(
this@ErrorActivity,
config
)
}
val moreInfoButton =
findViewById<Button>(R.id.customactivityoncrash_error_activity_more_info_button)
moreInfoButton.setOnClickListener { //We retrieve all the error data and show it
AlertDialog.Builder(this@ErrorActivity)
.setTitle(R.string.customactivityoncrash_error_activity_error_details_title)
.setMessage(
CustomActivityOnCrash.getAllErrorDetailsFromIntent(
this@ErrorActivity,
intent
)
)
.setPositiveButton(
R.string.customactivityoncrash_error_activity_error_details_close,
null
)
.setNeutralButton(
R.string.customactivityoncrash_error_activity_error_details_share
) { _, _ ->
val bugReport = createFile(
context = this,
"Bug Report",
"$ReportPrefix${dayFormat.format(Date())}",
CustomActivityOnCrash.getAllErrorDetailsFromIntent(
this@ErrorActivity,
intent
), ".txt"
)
shareFile(this, bugReport)
}
.show()
}
val errorActivityDrawableId = config.errorDrawable
val errorImageView =
findViewById<ImageView>(R.id.customactivityoncrash_error_activity_image)
if (errorActivityDrawableId != null) {
errorImageView.setImageResource(
errorActivityDrawableId
)
}
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.activities
import android.graphics.Color
import android.os.Bundle
import android.view.MenuItem
import code.name.monkey.appthemehelper.util.ATHUtil.isWindowBackgroundDark
import code.name.monkey.appthemehelper.util.ColorUtil.lightenColor
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import code.name.monkey.retromusic.activities.base.AbsThemeActivity
import code.name.monkey.retromusic.databinding.ActivityLicenseBinding
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.drawAboveSystemBars
import code.name.monkey.retromusic.extensions.surfaceColor
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
/** Created by hemanths on 2019-09-27. */
class LicenseActivity : AbsThemeActivity() {
private lateinit var binding: ActivityLicenseBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLicenseBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar)
try {
val buf = StringBuilder()
val json = assets.open("license.html")
BufferedReader(InputStreamReader(json, StandardCharsets.UTF_8)).use { br ->
var str: String?
while (br.readLine().also { str = it } != null) {
buf.append(str)
}
}
// Inject color values for WebView body background and links
val isDark = isWindowBackgroundDark(this)
val backgroundColor = colorToCSS(
surfaceColor(Color.parseColor(if (isDark) "#424242" else "#ffffff"))
)
val contentColor = colorToCSS(Color.parseColor(if (isDark) "#ffffff" else "#000000"))
val changeLog = buf.toString()
.replace(
"{style-placeholder}", String.format(
"body { background-color: %s; color: %s; }", backgroundColor, contentColor
)
)
.replace("{link-color}", colorToCSS(accentColor()))
.replace(
"{link-color-active}",
colorToCSS(
lightenColor(accentColor())
)
)
binding.license.loadData(changeLog, "text/html", "UTF-8")
} catch (e: Throwable) {
binding.license.loadData(
"<h1>Unable to load</h1><p>" + e.localizedMessage + "</p>", "text/html", "UTF-8"
)
}
binding.license.drawAboveSystemBars()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
private fun colorToCSS(color: Int): String {
return String.format(
"rgb(%d, %d, %d)",
Color.red(color),
Color.green(color),
Color.blue(color)
) // on API 29, WebView doesn't load with hex colors
}
}

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.app.KeyguardManager
import android.os.Bundle
import android.view.WindowManager
import androidx.core.content.getSystemService
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
import code.name.monkey.retromusic.databinding.ActivityLockScreenBinding
import code.name.monkey.retromusic.extensions.hideStatusBar
import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
import code.name.monkey.retromusic.extensions.whichFragment
import code.name.monkey.retromusic.fragments.player.lockscreen.LockScreenControlsFragment
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import com.r0adkll.slidr.Slidr
import com.r0adkll.slidr.model.SlidrConfig
import com.r0adkll.slidr.model.SlidrListener
import com.r0adkll.slidr.model.SlidrPosition
class LockScreenActivity : AbsMusicServiceActivity() {
private lateinit var binding: ActivityLockScreenBinding
private var fragment: LockScreenControlsFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lockScreenInit()
binding = ActivityLockScreenBinding.inflate(layoutInflater)
setContentView(binding.root)
hideStatusBar()
setTaskDescriptionColorAuto()
val config = SlidrConfig.Builder().listener(object : SlidrListener {
override fun onSlideStateChanged(state: Int) {
}
override fun onSlideChange(percent: Float) {
}
override fun onSlideOpened() {
}
override fun onSlideClosed(): Boolean {
if (VersionUtils.hasOreo()) {
val keyguardManager =
getSystemService<KeyguardManager>()
keyguardManager?.requestDismissKeyguard(this@LockScreenActivity, null)
}
finish()
return true
}
}).position(SlidrPosition.BOTTOM).build()
Slidr.attach(this, config)
fragment = whichFragment<LockScreenControlsFragment>(R.id.playback_controls_fragment)
binding.slide.apply {
translationY = 100f
alpha = 0f
animate().translationY(0f).alpha(1f).setDuration(1500).start()
}
}
@Suppress("Deprecation")
private fun lockScreenInit() {
if (VersionUtils.hasOreoMR1()) {
setShowWhenLocked(true)
val keyguardManager = getSystemService<KeyguardManager>()
keyguardManager?.requestDismissKeyguard(this, null)
} else {
this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
)
}
}
override fun onPlayingMetaChanged() {
super.onPlayingMetaChanged()
updateSongs()
}
override fun onServiceConnected() {
super.onServiceConnected()
updateSongs()
}
private fun updateSongs() {
val song = MusicPlayerRemote.currentSong
GlideApp.with(this)
.asBitmapPalette()
.songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.dontAnimate()
.into(object : RetroMusicColoredTarget(binding.image) {
override fun onColorReady(colors: MediaNotificationProcessor) {
fragment?.setColor(colors)
}
})
}
}

View file

@ -0,0 +1,236 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import androidx.lifecycle.lifecycleScope
import androidx.navigation.contains
import androidx.navigation.ui.setupWithNavController
import code.name.monkey.retromusic.*
import code.name.monkey.retromusic.activities.base.AbsSlidingMusicPanelActivity
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.databinding.SlidingMusicPanelLayoutBinding
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.SearchQueryHelper.getSongs
import code.name.monkey.retromusic.interfaces.IScrollHelper
import code.name.monkey.retromusic.model.CategoryInfo
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.repository.PlaylistSongsLoader
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.util.PreferenceUtil
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeListener {
companion object {
const val TAG = "MainActivity"
const val EXPAND_PANEL = "expand_panel"
}
override fun createContentView(): SlidingMusicPanelLayoutBinding {
return wrapSlidingMusicPanel()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTaskDescriptionColorAuto()
hideStatusBar()
updateTabs()
setupNavigationController()
if (!hasPermissions()) {
findNavController(R.id.fragment_container).navigate(R.id.permissionFragment)
}
WhatsNewFragment.showChangeLog(this)
}
private fun setupNavigationController() {
val navController = findNavController(R.id.fragment_container)
val navInflater = navController.navInflater
val navGraph = navInflater.inflate(R.navigation.main_graph)
val categoryInfo: CategoryInfo = PreferenceUtil.libraryCategory.first { it.visible }
if (categoryInfo.visible) {
if (!navGraph.contains(PreferenceUtil.lastTab)) PreferenceUtil.lastTab =
categoryInfo.category.id
navGraph.setStartDestination(
if (PreferenceUtil.rememberLastTab) {
PreferenceUtil.lastTab.let {
if (it == 0) {
categoryInfo.category.id
} else {
it
}
}
} else categoryInfo.category.id
)
}
navController.graph = navGraph
bottomNavigationView.setupWithNavController(navController)
// Scroll Fragment to top
bottomNavigationView.setOnItemReselectedListener {
currentFragment(R.id.fragment_container).apply {
if (this is IScrollHelper) {
scrollToTop()
}
}
}
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == navGraph.startDestinationId) {
currentFragment(R.id.fragment_container)?.enterTransition = null
}
when (destination.id) {
R.id.action_home, R.id.action_song, R.id.action_album, R.id.action_artist, R.id.action_folder, R.id.action_playlist, R.id.action_genre, R.id.action_search -> {
// Save the last tab
if (PreferenceUtil.rememberLastTab) {
saveTab(destination.id)
}
// Show Bottom Navigation Bar
setBottomNavVisibility(visible = true, animate = true)
}
R.id.playing_queue_fragment -> {
setBottomNavVisibility(visible = false, hideBottomSheet = true)
}
else -> setBottomNavVisibility(
visible = false,
animate = true
) // Hide Bottom Navigation Bar
}
}
}
private fun saveTab(id: Int) {
PreferenceUtil.lastTab = id
}
override fun onSupportNavigateUp(): Boolean =
findNavController(R.id.fragment_container).navigateUp()
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val expand = intent?.extra<Boolean>(EXPAND_PANEL)?.value ?: false
if (expand && PreferenceUtil.isExpandPanel) {
fromNotification = true
slidingPanel.bringToFront()
expandPanel()
intent?.removeExtra(EXPAND_PANEL)
}
}
override fun onResume() {
super.onResume()
PreferenceUtil.registerOnSharedPreferenceChangedListener(this)
}
override fun onDestroy() {
super.onDestroy()
PreferenceUtil.unregisterOnSharedPreferenceChangedListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == GENERAL_THEME || key == MATERIAL_YOU || key == WALLPAPER_ACCENT || key == BLACK_THEME || key == ADAPTIVE_COLOR_APP || key == USER_NAME || key == TOGGLE_FULL_SCREEN || key == TOGGLE_VOLUME || key == ROUND_CORNERS || key == CAROUSEL_EFFECT || key == NOW_PLAYING_SCREEN_ID || key == TOGGLE_GENRE || key == BANNER_IMAGE_PATH || key == PROFILE_IMAGE_PATH || key == CIRCULAR_ALBUM_ART || key == KEEP_SCREEN_ON || key == TOGGLE_SEPARATE_LINE || key == TOGGLE_HOME_BANNER || key == TOGGLE_ADD_CONTROLS || key == ALBUM_COVER_STYLE || key == HOME_ARTIST_GRID_STYLE || key == ALBUM_COVER_TRANSFORM || key == DESATURATED_COLOR || key == EXTRA_SONG_INFO || key == TAB_TEXT_MODE || key == LANGUAGE_NAME || key == LIBRARY_CATEGORIES || key == CUSTOM_FONT || key == APPBAR_MODE || key == CIRCLE_PLAY_BUTTON || key == SWIPE_DOWN_DISMISS) {
postRecreate()
}
}
override fun onServiceConnected() {
super.onServiceConnected()
intent ?: return
handlePlaybackIntent(intent)
}
private fun handlePlaybackIntent(intent: Intent) {
lifecycleScope.launch(IO) {
val uri: Uri? = intent.data
val mimeType: String? = intent.type
var handled = false
if (intent.action != null &&
intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
) {
val songs: List<Song> = getSongs(intent.extras!!)
if (MusicPlayerRemote.shuffleMode == MusicService.SHUFFLE_MODE_SHUFFLE) {
MusicPlayerRemote.openAndShuffleQueue(songs, true)
} else {
MusicPlayerRemote.openQueue(songs, 0, true)
}
handled = true
}
if (uri != null && uri.toString().isNotEmpty()) {
MusicPlayerRemote.playFromUri(this@MainActivity, uri)
handled = true
} else if (MediaStore.Audio.Playlists.CONTENT_TYPE == mimeType) {
val id = parseLongFromIntent(intent, "playlistId", "playlist")
if (id >= 0L) {
val position: Int = intent.getIntExtra("position", 0)
val songs: List<Song> = PlaylistSongsLoader.getPlaylistSongList(get(), id)
MusicPlayerRemote.openQueue(songs, position, true)
handled = true
}
} else if (MediaStore.Audio.Albums.CONTENT_TYPE == mimeType) {
val id = parseLongFromIntent(intent, "albumId", "album")
if (id >= 0L) {
val position: Int = intent.getIntExtra("position", 0)
val songs = libraryViewModel.albumById(id).songs
MusicPlayerRemote.openQueue(
songs,
position,
true
)
handled = true
}
} else if (MediaStore.Audio.Artists.CONTENT_TYPE == mimeType) {
val id = parseLongFromIntent(intent, "artistId", "artist")
if (id >= 0L) {
val position: Int = intent.getIntExtra("position", 0)
val songs: List<Song> = libraryViewModel.artistById(id).songs
MusicPlayerRemote.openQueue(
songs,
position,
true
)
handled = true
}
}
if (handled) {
setIntent(Intent())
}
}
}
private fun parseLongFromIntent(
intent: Intent,
longKey: String,
stringKey: String
): Long {
var id = intent.getLongExtra(longKey, -1)
if (id < 0) {
val idString = intent.getStringExtra(stringKey)
if (idString != null) {
try {
id = idString.toLong()
} catch (e: NumberFormatException) {
println(e.message)
}
}
}
return id
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.Manifest
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.databinding.ActivityPermissionBinding
class PermissionActivity : AbsMusicServiceActivity() {
private lateinit var binding: ActivityPermissionBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPermissionBinding.inflate(layoutInflater)
setContentView(binding.root)
setStatusBarColorAuto()
setTaskDescriptionColorAuto()
setupTitle()
binding.storagePermission.setButtonClick {
requestPermissions()
}
if (VersionUtils.hasMarshmallow()) {
binding.audioPermission.show()
binding.audioPermission.setButtonClick {
if (!hasAudioPermission()) {
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.data = ("package:" + applicationContext.packageName).toUri()
startActivity(intent)
}
}
}
if (VersionUtils.hasS()) {
binding.bluetoothPermission.show()
binding.bluetoothPermission.setButtonClick {
ActivityCompat.requestPermissions(this,
arrayOf(BLUETOOTH_CONNECT),
PERMISSION_REQUEST)
}
}
binding.finish.accentBackgroundColor()
binding.finish.setOnClickListener {
if (hasPermissions()) {
startActivity(
Intent(this, MainActivity::class.java).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
)
finish()
}
}
}
private fun setupTitle() {
val appName =
getString(R.string.message_welcome,
"<b>Metro</b>")
.parseAsHtml()
binding.appNameText.text = appName
}
override fun onResume() {
super.onResume()
binding.finish.isEnabled = hasStoragePermission()
if (hasStoragePermission()) {
binding.storagePermission.checkImage.isVisible = true
binding.storagePermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
if (VersionUtils.hasMarshmallow()) {
if (hasAudioPermission()) {
binding.audioPermission.checkImage.isVisible = true
binding.audioPermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
}
if (VersionUtils.hasS()) {
if (hasBluetoothPermission()) {
binding.bluetoothPermission.checkImage.isVisible = true
binding.bluetoothPermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
}
}
private fun hasStoragePermission(): Boolean {
return ActivityCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
@RequiresApi(Build.VERSION_CODES.S)
private fun hasBluetoothPermission(): Boolean {
return ActivityCompat.checkSelfPermission(this,
BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
}
@RequiresApi(Build.VERSION_CODES.M)
private fun hasAudioPermission(): Boolean {
return Settings.System.canWrite(this)
}
override fun onBackPressed() {
super.onBackPressed()
finishAffinity()
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
import code.name.monkey.retromusic.appshortcuts.DynamicShortcutManager
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.databinding.ActivitySettingsBinding
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.color.ColorCallback
class SettingsActivity : AbsBaseActivity(), ColorCallback, OnThemeChangedListener {
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
val mSavedInstanceState = extra<Bundle>(TAG).value ?: savedInstanceState
super.onCreate(mSavedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setupToolbar()
setPermissionDeniedMessage(getString(R.string.permission_bluetooth_denied))
}
override fun onResume() {
super.onResume()
setNavigationBarColorPreOreo(surfaceColor())
}
private fun setupToolbar() {
applyToolbar(binding.toolbar)
val navController: NavController = findNavController(R.id.contentFrame)
navController.addOnDestinationChangedListener { _, _, _ ->
binding.collapsingToolbarLayout.title =
navController.currentDestination?.let { getStringFromDestination(it) }
}
}
private fun getStringFromDestination(currentDestination: NavDestination): String {
val idRes = when (currentDestination.id) {
R.id.mainSettingsFragment -> R.string.action_settings
R.id.audioSettings -> R.string.pref_header_audio
R.id.imageSettingFragment -> R.string.pref_header_images
R.id.notificationSettingsFragment -> R.string.notification
R.id.nowPlayingSettingsFragment -> R.string.now_playing
R.id.otherSettingsFragment -> R.string.others
R.id.personalizeSettingsFragment -> R.string.personalize
R.id.themeSettingsFragment -> R.string.general_settings_title
R.id.aboutActivity -> R.string.action_about
R.id.backup_fragment -> R.string.backup_restore_title
else -> R.id.action_settings
}
return getString(idRes)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.contentFrame).navigateUp() || super.onSupportNavigateUp()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
}
return super.onOptionsItemSelected(item)
}
override fun getPermissionsToRequest(): Array<String> {
return if (VersionUtils.hasS()) {
arrayOf(BLUETOOTH_CONNECT)
} else {
arrayOf()
}
}
override fun invoke(dialog: MaterialDialog, color: Int) {
ThemeStore.editTheme(this).accentColor(color).commit()
if (VersionUtils.hasNougatMR())
DynamicShortcutManager(this).updateDynamicShortcuts()
restart()
}
override fun onThemeValuesChanged() {
restart()
}
private fun restart() {
val savedInstanceState = Bundle().apply {
onSaveInstanceState(this)
}
finish()
val intent = Intent(this, this::class.java).putExtra(TAG, savedInstanceState)
startActivity(intent)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
companion object {
val TAG: String = SettingsActivity::class.java.simpleName
}
}
interface OnThemeChangedListener {
fun onThemeValuesChanged()
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.provider.MediaStore.Images.Media
import android.view.MenuItem
import androidx.core.net.toUri
import androidx.core.view.drawToBitmap
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
import code.name.monkey.retromusic.databinding.ActivityShareInstagramBinding
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.setLightStatusBar
import code.name.monkey.retromusic.extensions.setStatusBarColor
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.Share
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
/**
* Created by hemanths on 2020-02-02.
*/
class ShareInstagramStory : AbsBaseActivity() {
private lateinit var binding: ActivityShareInstagramBinding
companion object {
const val EXTRA_SONG = "extra_song"
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityShareInstagramBinding.inflate(layoutInflater)
setContentView(binding.root)
setStatusBarColor(Color.TRANSPARENT)
binding.toolbar.setBackgroundColor(Color.TRANSPARENT)
setSupportActionBar(binding.toolbar)
val song = intent.extras?.getParcelable<Song>(EXTRA_SONG)
song?.let { songFinal ->
GlideApp.with(this)
.asBitmapPalette()
.songCoverOptions(songFinal)
.load(RetroGlideExtension.getSongModel(songFinal))
.into(object : RetroMusicColoredTarget(binding.image) {
override fun onColorReady(colors: MediaNotificationProcessor) {
val isColorLight = ColorUtil.isColorLight(colors.backgroundColor)
setColors(isColorLight, colors.backgroundColor)
}
})
binding.shareTitle.text = songFinal.title
binding.shareText.text = songFinal.artistName
binding.shareButton.setOnClickListener {
val path: String = Media.insertImage(
contentResolver,
binding.mainContent.drawToBitmap(Bitmap.Config.ARGB_8888),
"Design", null
)
Share.shareStoryToSocial(
this@ShareInstagramStory,
path.toUri()
)
}
}
binding.shareButton.setTextColor(
MaterialValueHelper.getPrimaryTextColor(
this,
ColorUtil.isColorLight(accentColor())
)
)
binding.shareButton.backgroundTintList =
ColorStateList.valueOf(accentColor())
}
private fun setColors(colorLight: Boolean, color: Int) {
setLightStatusBar(colorLight)
binding.toolbar.setTitleTextColor(
MaterialValueHelper.getPrimaryTextColor(
this@ShareInstagramStory,
colorLight
)
)
binding.toolbar.navigationIcon?.setTintList(
ColorStateList.valueOf(
MaterialValueHelper.getPrimaryTextColor(
this@ShareInstagramStory,
colorLight
)
)
)
binding.mainContent.background =
GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(color, Color.BLACK)
)
}
}

View file

@ -0,0 +1,138 @@
package code.name.monkey.retromusic.activities
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.FragmentActivity
import code.name.monkey.appthemehelper.util.ATHUtil.isWindowBackgroundDark
import code.name.monkey.appthemehelper.util.ColorUtil.isColorLight
import code.name.monkey.appthemehelper.util.ColorUtil.lightenColor
import code.name.monkey.appthemehelper.util.MaterialValueHelper.getPrimaryTextColor
import code.name.monkey.retromusic.BuildConfig
import code.name.monkey.retromusic.Constants
import code.name.monkey.retromusic.databinding.FragmentWhatsNewBinding
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.openUrl
import code.name.monkey.retromusic.util.PreferenceUtil.lastVersion
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.nio.charset.StandardCharsets
import java.util.*
class WhatsNewFragment : BottomSheetDialogFragment() {
private var _binding: FragmentWhatsNewBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWhatsNewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
try {
val buf = StringBuilder()
val stream= requireContext().assets.open("retro-changelog.html")
stream.reader(StandardCharsets.UTF_8).buffered().use { br ->
var str: String?
while (br.readLine().also { str = it } != null) {
buf.append(str)
}
}
// Inject color values for WebView body background and links
val isDark = isWindowBackgroundDark(requireContext())
val accentColor = accentColor()
binding.webView.setBackgroundColor(0)
val contentColor = colorToCSS(Color.parseColor(if (isDark) "#ffffff" else "#000000"))
val textColor = colorToCSS(Color.parseColor(if (isDark) "#60FFFFFF" else "#80000000"))
val accentColorString = colorToCSS(accentColor())
val cardBackgroundColor =
colorToCSS(Color.parseColor(if (isDark) "#353535" else "#ffffff"))
val accentTextColor = colorToCSS(
getPrimaryTextColor(
requireContext(), isColorLight(accentColor)
)
)
val changeLog = buf.toString()
.replace(
"{style-placeholder}",
"body { color: $contentColor; } li {color: $textColor;} h3 {color: $accentColorString;} .tag {background-color: $accentColorString; color: $accentTextColor; } div{background-color: $cardBackgroundColor;}"
)
.replace("{link-color}", colorToCSS(accentColor()))
.replace(
"{link-color-active}",
colorToCSS(
lightenColor(accentColor())
)
)
binding.webView.loadData(changeLog, "text/html", "UTF-8")
} catch (e: Throwable) {
binding.webView.loadData(
"<h1>Unable to load</h1><p>" + e.localizedMessage + "</p>", "text/html", "UTF-8"
)
}
setChangelogRead(requireContext())
binding.tgFab.setOnClickListener {
openUrl(Constants.TELEGRAM_CHANGE_LOG)
}
binding.tgFab.accentColor()
binding.tgFab.shrink()
binding.container.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, oldScrollY: Int ->
val dy = scrollY - oldScrollY
if (dy > 0) {
binding.tgFab.shrink()
} else if (dy < 0) {
binding.tgFab.extend()
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
companion object {
const val TAG = "WhatsNewFragment"
private fun colorToCSS(color: Int): String {
return String.format(
Locale.getDefault(),
"rgba(%d, %d, %d, %d)",
Color.red(color),
Color.green(color),
Color.blue(color),
Color.alpha(color)
) // on API 29, WebView doesn't load with hex colors
}
private fun setChangelogRead(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val currentVersion = PackageInfoCompat.getLongVersionCode(pInfo)
lastVersion = currentVersion
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
fun showChangeLog(activity: FragmentActivity) {
val pInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
val currentVersion = PackageInfoCompat.getLongVersionCode(pInfo)
if (currentVersion > lastVersion && !BuildConfig.DEBUG) {
val changelogBottomSheet = WhatsNewFragment()
changelogBottomSheet.show(activity.supportFragmentManager, TAG)
}
}
}
}

View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.base
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.app.ActivityCompat
import androidx.core.content.getSystemService
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.accentColor
import com.google.android.material.snackbar.Snackbar
abstract class AbsBaseActivity : AbsThemeActivity() {
private var hadPermissions: Boolean = false
private lateinit var permissions: Array<String>
private var permissionDeniedMessage: String? = null
open fun getPermissionsToRequest(): Array<String> {
return arrayOf()
}
protected fun setPermissionDeniedMessage(message: String) {
permissionDeniedMessage = message
}
fun getPermissionDeniedMessage(): String {
return if (permissionDeniedMessage == null) getString(R.string.permissions_denied) else permissionDeniedMessage!!
}
private val snackBarContainer: View
get() = window.decorView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
volumeControlStream = AudioManager.STREAM_MUSIC
permissions = getPermissionsToRequest()
hadPermissions = hasPermissions()
permissionDeniedMessage = null
}
override fun onResume() {
super.onResume()
val hasPermissions = hasPermissions()
if (hasPermissions != hadPermissions) {
hadPermissions = hasPermissions
if (VersionUtils.hasMarshmallow()) {
onHasPermissionsChanged(hasPermissions)
}
}
}
protected open fun onHasPermissionsChanged(hasPermissions: Boolean) {
// implemented by sub classes
println(hasPermissions)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_MENU && event.action == KeyEvent.ACTION_UP) {
showOverflowMenu()
return true
}
return super.dispatchKeyEvent(event)
}
private fun showOverflowMenu() {
}
protected open fun requestPermissions() {
ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST)
}
protected fun hasPermissions(): Boolean {
for (permission in permissions) {
if (ActivityCompat.checkSelfPermission(this,
permission) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST) {
for (grantResult in grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this@AbsBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
// User has deny from permission dialog
Snackbar.make(
snackBarContainer,
permissionDeniedMessage!!,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_grant) { requestPermissions() }
.setActionTextColor(accentColor()).show()
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this@AbsBaseActivity, Manifest.permission.BLUETOOTH_CONNECT
)
) {
// User has deny from permission dialog
Snackbar.make(
snackBarContainer,
R.string.permission_bluetooth_denied,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_grant) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
PERMISSION_REQUEST)
}
.setActionTextColor(accentColor()).show()
} else {
// User has deny permission and checked never show permission dialog so you can redirect to Application settings page
Snackbar.make(
snackBarContainer,
permissionDeniedMessage!!,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_settings) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts(
"package",
this@AbsBaseActivity.packageName,
null
)
intent.data = uri
startActivity(intent)
}.setActionTextColor(accentColor()).show()
}
return
}
}
hadPermissions = true
onHasPermissionsChanged(true)
}
}
companion object {
const val PERMISSION_REQUEST = 100
}
// this lets keyboard close when clicked in background
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val v = currentFocus
if (v is EditText) {
val outRect = Rect()
v.getGlobalVisibleRect(outRect)
if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
v.clearFocus()
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
v.windowToken,
0
)
}
}
}
return super.dispatchTouchEvent(event)
}
}

View file

@ -0,0 +1,223 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.base
import android.Manifest
import android.content.*
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.toPlayCount
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener
import code.name.monkey.retromusic.repository.RealRepository
import code.name.monkey.retromusic.service.MusicService.Companion.FAVORITE_STATE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.MEDIA_STORE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.META_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.PLAY_STATE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.QUEUE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.REPEAT_MODE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_CHANGED
import code.name.monkey.retromusic.util.PreferenceUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.lang.ref.WeakReference
abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventListener {
private val mMusicServiceEventListeners = ArrayList<IMusicServiceEventListener>()
private val repository: RealRepository by inject()
private var serviceToken: MusicPlayerRemote.ServiceToken? = null
private var musicStateReceiver: MusicStateReceiver? = null
private var receiverRegistered: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
serviceToken = MusicPlayerRemote.bindToService(this, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
this@AbsMusicServiceActivity.onServiceConnected()
}
override fun onServiceDisconnected(name: ComponentName) {
this@AbsMusicServiceActivity.onServiceDisconnected()
}
})
setPermissionDeniedMessage(getString(R.string.permission_external_storage_denied))
}
override fun onDestroy() {
super.onDestroy()
MusicPlayerRemote.unbindFromService(serviceToken)
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver)
receiverRegistered = false
}
}
fun addMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
if (listenerI != null) {
mMusicServiceEventListeners.add(listenerI)
}
}
fun removeMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
if (listenerI != null) {
mMusicServiceEventListeners.remove(listenerI)
}
}
override fun onServiceConnected() {
if (!receiverRegistered) {
musicStateReceiver = MusicStateReceiver(this)
val filter = IntentFilter()
filter.addAction(PLAY_STATE_CHANGED)
filter.addAction(SHUFFLE_MODE_CHANGED)
filter.addAction(REPEAT_MODE_CHANGED)
filter.addAction(META_CHANGED)
filter.addAction(QUEUE_CHANGED)
filter.addAction(MEDIA_STORE_CHANGED)
filter.addAction(FAVORITE_STATE_CHANGED)
registerReceiver(musicStateReceiver, filter)
receiverRegistered = true
}
for (listener in mMusicServiceEventListeners) {
listener.onServiceConnected()
}
}
override fun onServiceDisconnected() {
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver)
receiverRegistered = false
}
for (listener in mMusicServiceEventListeners) {
listener.onServiceDisconnected()
}
}
override fun onPlayingMetaChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onPlayingMetaChanged()
}
lifecycleScope.launch(Dispatchers.IO) {
val entity = repository.songPresentInHistory(MusicPlayerRemote.currentSong)
if (entity != null) {
repository.updateHistorySong(MusicPlayerRemote.currentSong)
} else {
// Check whether pause history option is ON or OFF
if (!PreferenceUtil.pauseHistory) {
repository.addSongToHistory(MusicPlayerRemote.currentSong)
}
}
val songs = repository.checkSongExistInPlayCount(MusicPlayerRemote.currentSong.id)
if (songs.isNotEmpty()) {
repository.updateSongInPlayCount(songs.first().apply {
playCount += 1
})
} else {
repository.insertSongInPlayCount(MusicPlayerRemote.currentSong.toPlayCount())
}
}
}
override fun onQueueChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onQueueChanged()
}
}
override fun onPlayStateChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onPlayStateChanged()
}
}
override fun onMediaStoreChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onMediaStoreChanged()
}
}
override fun onRepeatModeChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onRepeatModeChanged()
}
}
override fun onShuffleModeChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onShuffleModeChanged()
}
}
override fun onFavoriteStateChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onFavoriteStateChanged()
}
}
override fun onHasPermissionsChanged(hasPermissions: Boolean) {
super.onHasPermissionsChanged(hasPermissions)
val intent = Intent(MEDIA_STORE_CHANGED)
intent.putExtra(
"from_permissions_changed",
true
) // just in case we need to know this at some point
sendBroadcast(intent)
println("sendBroadcast $hasPermissions")
}
override fun getPermissionsToRequest(): Array<String> {
return mutableListOf(Manifest.permission.READ_EXTERNAL_STORAGE).apply {
if (!VersionUtils.hasQ()) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
private class MusicStateReceiver(activity: AbsMusicServiceActivity) : BroadcastReceiver() {
private val reference: WeakReference<AbsMusicServiceActivity> = WeakReference(activity)
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
val activity = reference.get()
if (activity != null && action != null) {
when (action) {
FAVORITE_STATE_CHANGED -> activity.onFavoriteStateChanged()
META_CHANGED -> activity.onPlayingMetaChanged()
QUEUE_CHANGED -> activity.onQueueChanged()
PLAY_STATE_CHANGED -> activity.onPlayStateChanged()
REPEAT_MODE_CHANGED -> activity.onRepeatModeChanged()
SHUFFLE_MODE_CHANGED -> activity.onShuffleModeChanged()
MEDIA_STORE_CHANGED -> activity.onMediaStoreChanged()
}
}
}
}
companion object {
val TAG: String = AbsMusicServiceActivity::class.java.simpleName
}
}

View file

@ -0,0 +1,475 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.base
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.animation.PathInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.SlidingMusicPanelLayoutBinding
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.fragments.NowPlayingScreen
import code.name.monkey.retromusic.fragments.NowPlayingScreen.*
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
import code.name.monkey.retromusic.fragments.other.MiniPlayerFragment
import code.name.monkey.retromusic.fragments.player.adaptive.AdaptiveFragment
import code.name.monkey.retromusic.fragments.player.blur.BlurPlayerFragment
import code.name.monkey.retromusic.fragments.player.card.CardFragment
import code.name.monkey.retromusic.fragments.player.cardblur.CardBlurFragment
import code.name.monkey.retromusic.fragments.player.circle.CirclePlayerFragment
import code.name.monkey.retromusic.fragments.player.classic.ClassicPlayerFragment
import code.name.monkey.retromusic.fragments.player.color.ColorFragment
import code.name.monkey.retromusic.fragments.player.fit.FitFragment
import code.name.monkey.retromusic.fragments.player.flat.FlatPlayerFragment
import code.name.monkey.retromusic.fragments.player.full.FullPlayerFragment
import code.name.monkey.retromusic.fragments.player.gradient.GradientPlayerFragment
import code.name.monkey.retromusic.fragments.player.material.MaterialFragment
import code.name.monkey.retromusic.fragments.player.md3.MD3PlayerFragment
import code.name.monkey.retromusic.fragments.player.normal.PlayerFragment
import code.name.monkey.retromusic.fragments.player.peek.PeekPlayerFragment
import code.name.monkey.retromusic.fragments.player.plain.PlainPlayerFragment
import code.name.monkey.retromusic.fragments.player.simple.SimplePlayerFragment
import code.name.monkey.retromusic.fragments.player.tiny.TinyPlayerFragment
import code.name.monkey.retromusic.fragments.queue.PlayingQueueFragment
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.model.CategoryInfo
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.ViewUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.*
import org.koin.androidx.viewmodel.ext.android.viewModel
abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() {
companion object {
val TAG: String = AbsSlidingMusicPanelActivity::class.java.simpleName
}
var fromNotification = false
private var windowInsets: WindowInsetsCompat? = null
protected val libraryViewModel by viewModel<LibraryViewModel>()
private lateinit var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>
private var playerFragment: AbsPlayerFragment? = null
private var miniPlayerFragment: MiniPlayerFragment? = null
private var nowPlayingScreen: NowPlayingScreen? = null
private var taskColor: Int = 0
private var paletteColor: Int = Color.WHITE
private var navigationBarColor = 0
protected abstract fun createContentView(): SlidingMusicPanelLayoutBinding
private val panelState: Int
get() = bottomSheetBehavior.state
private lateinit var binding: SlidingMusicPanelLayoutBinding
private var isInOneTabMode = false
private var navigationBarColorAnimator: ValueAnimator? = null
private val argbEvaluator: ArgbEvaluator = ArgbEvaluator()
private val bottomSheetCallbackList = object : BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
setMiniPlayerAlphaProgress(slideOffset)
navigationBarColorAnimator?.cancel()
setNavigationBarColorPreOreo(
argbEvaluator.evaluate(
slideOffset,
surfaceColor(),
navigationBarColor
) as Int
)
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
when (newState) {
STATE_EXPANDED -> {
onPanelExpanded()
if (PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics) {
keepScreenOn(true)
}
}
STATE_COLLAPSED -> {
onPanelCollapsed()
if ((PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics) || !PreferenceUtil.isScreenOnEnabled) {
keepScreenOn(false)
}
}
STATE_SETTLING, STATE_DRAGGING -> {
if (fromNotification) {
binding.bottomNavigationView.bringToFront()
fromNotification = false
}
}
STATE_HIDDEN -> {
MusicPlayerRemote.clearQueue()
}
else -> {
println("Do a flip")
}
}
}
}
fun getBottomSheetBehavior() = bottomSheetBehavior
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = createContentView()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _, insets ->
windowInsets = insets
insets
}
chooseFragmentForTheme()
setupSlidingUpPanel()
setupBottomSheet()
updateColor()
if (!PreferenceUtil.materialYou) {
binding.slidingPanel.backgroundTintList = ColorStateList.valueOf(darkAccentColor())
bottomNavigationView.backgroundTintList = ColorStateList.valueOf(darkAccentColor())
}
navigationBarColor = surfaceColor()
}
private fun setupBottomSheet() {
bottomSheetBehavior = from(binding.slidingPanel)
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallbackList)
bottomSheetBehavior.isHideable = PreferenceUtil.swipeDownToDismiss
setMiniPlayerAlphaProgress(0F)
}
override fun onResume() {
super.onResume()
if (nowPlayingScreen != PreferenceUtil.nowPlayingScreen) {
postRecreate()
}
if (bottomSheetBehavior.state == STATE_EXPANDED) {
setMiniPlayerAlphaProgress(1f)
}
}
override fun onDestroy() {
super.onDestroy()
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallbackList)
}
protected fun wrapSlidingMusicPanel(): SlidingMusicPanelLayoutBinding {
return SlidingMusicPanelLayoutBinding.inflate(layoutInflater)
}
fun collapsePanel() {
bottomSheetBehavior.state = STATE_COLLAPSED
}
fun expandPanel() {
bottomSheetBehavior.state = STATE_EXPANDED
}
private fun setMiniPlayerAlphaProgress(progress: Float) {
if (progress < 0) return
val alpha = 1 - progress
miniPlayerFragment?.view?.alpha = 1 - (progress / 0.2F)
miniPlayerFragment?.view?.isGone = alpha == 0f
binding.bottomNavigationView.translationY = progress * 500
binding.bottomNavigationView.alpha = alpha
binding.playerFragmentContainer.alpha = (progress - 0.2F) / 0.2F
}
private fun animateNavigationBarColor(color: Int) {
if (VersionUtils.hasOreo()) return
navigationBarColorAnimator?.cancel()
navigationBarColorAnimator = ValueAnimator
.ofArgb(window.navigationBarColor, color).apply {
duration = ViewUtil.RETRO_MUSIC_ANIM_TIME.toLong()
interpolator = PathInterpolator(0.4f, 0f, 1f, 1f)
addUpdateListener { animation: ValueAnimator ->
setNavigationBarColorPreOreo(
animation.animatedValue as Int
)
}
start()
}
}
open fun onPanelCollapsed() {
setMiniPlayerAlphaProgress(0F)
// restore values
animateNavigationBarColor(surfaceColor())
setLightStatusBarAuto()
setLightNavigationBarAuto()
setTaskDescriptionColor(taskColor)
playerFragment?.onHide()
}
open fun onPanelExpanded() {
setMiniPlayerAlphaProgress(1F)
onPaletteColorChanged()
playerFragment?.onShow()
}
private fun setupSlidingUpPanel() {
binding.slidingPanel.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this)
if (nowPlayingScreen != Peek) {
binding.slidingPanel.updateLayoutParams<ViewGroup.LayoutParams> {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
}
when (panelState) {
STATE_EXPANDED -> onPanelExpanded()
STATE_COLLAPSED -> onPanelCollapsed()
else -> {
// playerFragment!!.onHide()
}
}
}
})
}
val bottomNavigationView get() = binding.bottomNavigationView
val slidingPanel get() = binding.slidingPanel
override fun onServiceConnected() {
super.onServiceConnected()
if (MusicPlayerRemote.playingQueue.isNotEmpty()) {
binding.slidingPanel.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this)
hideBottomSheet(false)
}
})
} // don't call hideBottomSheet(true) here as it causes a bug with the SlidingUpPanelLayout
}
override fun onQueueChanged() {
super.onQueueChanged()
// Mini player should be hidden in Playing Queue
// it may pop up if hideBottomSheet is called
if (currentFragment(R.id.fragment_container) !is PlayingQueueFragment) {
hideBottomSheet(MusicPlayerRemote.playingQueue.isEmpty())
}
}
override fun onBackPressed() {
if (!handleBackPress()) super.onBackPressed()
}
private fun handleBackPress(): Boolean {
if (bottomSheetBehavior.peekHeight != 0 && playerFragment!!.onBackPressed()) return true
if (panelState == STATE_EXPANDED) {
collapsePanel()
return true
}
return false
}
private fun onPaletteColorChanged() {
if (panelState == STATE_EXPANDED) {
navigationBarColor = surfaceColor()
setTaskDescColor(paletteColor)
val isColorLight = paletteColor.isColorLight
if (PreferenceUtil.isAdaptiveColor && (nowPlayingScreen == Normal || nowPlayingScreen == Flat || nowPlayingScreen == Material)) {
setLightNavigationBar(true)
setLightStatusBar(isColorLight)
} else if (nowPlayingScreen == Card || nowPlayingScreen == Blur || nowPlayingScreen == BlurCard) {
animateNavigationBarColor(Color.BLACK)
navigationBarColor = Color.BLACK
setLightStatusBar(false)
setLightNavigationBar(true)
} else if (nowPlayingScreen == Color || nowPlayingScreen == Tiny || nowPlayingScreen == Gradient) {
animateNavigationBarColor(paletteColor)
navigationBarColor = paletteColor
setLightNavigationBar(isColorLight)
setLightStatusBar(isColorLight)
} else if (nowPlayingScreen == Full) {
animateNavigationBarColor(paletteColor)
navigationBarColor = paletteColor
setLightNavigationBar(isColorLight)
setLightStatusBar(false)
} else if (nowPlayingScreen == Classic) {
setLightStatusBar(false)
} else if (nowPlayingScreen == Fit) {
setLightStatusBar(false)
}
}
}
private fun setTaskDescColor(color: Int) {
taskColor = color
if (panelState == STATE_COLLAPSED) {
setTaskDescriptionColor(color)
}
}
fun updateTabs() {
binding.bottomNavigationView.menu.clear()
val currentTabs: List<CategoryInfo> = PreferenceUtil.libraryCategory
for (tab in currentTabs) {
if (tab.visible) {
val menu = tab.category
binding.bottomNavigationView.menu.add(0, menu.id, 0, menu.stringRes)
.setIcon(menu.icon)
}
}
if (binding.bottomNavigationView.menu.size() == 1) {
isInOneTabMode = true
binding.bottomNavigationView.hide()
}
}
private fun updateColor() {
libraryViewModel.paletteColor.observe(this) { color ->
this.paletteColor = color
onPaletteColorChanged()
}
}
fun setBottomNavVisibility(
visible: Boolean,
animate: Boolean = false,
hideBottomSheet: Boolean = MusicPlayerRemote.playingQueue.isEmpty(),
) {
if (!ViewCompat.isLaidOut(bottomNavigationView)) {
return
}
if (isInOneTabMode) {
hideBottomSheet(
hide = hideBottomSheet,
animate = animate,
isBottomNavVisible = false
)
return
}
val mAnimate = animate && bottomSheetBehavior.state == STATE_COLLAPSED
if (mAnimate) {
if (visible) {
binding.bottomNavigationView.bringToFront()
binding.bottomNavigationView.show()
} else {
binding.bottomNavigationView.hide()
}
} else {
binding.bottomNavigationView.isVisible = false
if (visible && bottomSheetBehavior.state != STATE_EXPANDED) {
binding.bottomNavigationView.bringToFront()
}
}
hideBottomSheet(
hide = hideBottomSheet,
animate = animate,
isBottomNavVisible = visible
)
}
fun hideBottomSheet(
hide: Boolean,
animate: Boolean = false,
isBottomNavVisible: Boolean = bottomNavigationView.isVisible,
) {
val heightOfBar =
windowInsets.safeGetBottomInsets() +
if (MusicPlayerRemote.isCasting) dip(R.dimen.cast_mini_player_height) else dip(R.dimen.mini_player_height)
val heightOfBarWithTabs = heightOfBar + dip(R.dimen.bottom_nav_height)
if (hide) {
bottomSheetBehavior.peekHeight = -windowInsets.safeGetBottomInsets()
bottomSheetBehavior.state = STATE_COLLAPSED
libraryViewModel.setFabMargin(
this,
if (isBottomNavVisible) dip(R.dimen.bottom_nav_height) else 0
)
} else {
if (MusicPlayerRemote.playingQueue.isNotEmpty()) {
binding.slidingPanel.elevation = 0F
binding.bottomNavigationView.elevation = 5F
if (isBottomNavVisible) {
println("List")
if (animate) {
bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs)
} else {
bottomSheetBehavior.peekHeight = heightOfBarWithTabs
}
libraryViewModel.setFabMargin(this, dip(R.dimen.mini_player_height_expanded))
} else {
println("Details")
if (animate) {
bottomSheetBehavior.peekHeightAnimate(heightOfBar).doOnEnd {
binding.slidingPanel.bringToFront()
}
} else {
bottomSheetBehavior.peekHeight = heightOfBar
binding.slidingPanel.bringToFront()
}
libraryViewModel.setFabMargin(this, dip(R.dimen.mini_player_height))
}
}
}
}
fun setAllowDragging(allowDragging: Boolean) {
bottomSheetBehavior.isDraggable = allowDragging
hideBottomSheet(false)
}
private fun chooseFragmentForTheme() {
nowPlayingScreen = PreferenceUtil.nowPlayingScreen
val fragment: Fragment = when (nowPlayingScreen) {
Blur -> BlurPlayerFragment()
Adaptive -> AdaptiveFragment()
Normal -> PlayerFragment()
Card -> CardFragment()
BlurCard -> CardBlurFragment()
Fit -> FitFragment()
Flat -> FlatPlayerFragment()
Full -> FullPlayerFragment()
Plain -> PlainPlayerFragment()
Simple -> SimplePlayerFragment()
Material -> MaterialFragment()
Color -> ColorFragment()
Gradient -> GradientPlayerFragment()
Tiny -> TinyPlayerFragment()
Peek -> PeekPlayerFragment()
Circle -> CirclePlayerFragment()
Classic -> ClassicPlayerFragment()
MD3 -> MD3PlayerFragment()
else -> PlayerFragment()
} // must implement AbsPlayerFragment
supportFragmentManager.commit {
replace(R.id.playerFragmentContainer, fragment)
}
supportFragmentManager.executePendingTransactions()
playerFragment = whichFragment<AbsPlayerFragment>(R.id.playerFragmentContainer)
miniPlayerFragment = whichFragment<MiniPlayerFragment>(R.id.miniPlayerFragment)
miniPlayerFragment?.view?.setOnClickListener { expandPanel() }
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.base
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.View
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.core.os.ConfigurationCompat
import code.name.monkey.appthemehelper.common.ATHToolbarActivity
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.LanguageContextWrapper
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.theme.getNightMode
import code.name.monkey.retromusic.util.theme.getThemeResValue
import java.util.*
abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
updateTheme()
hideStatusBar()
super.onCreate(savedInstanceState)
setEdgeToEdgeOrImmersive()
registerSystemUiVisibility()
toggleScreenOn()
setLightNavigationBarAuto()
setLightStatusBarAuto(surfaceColor())
if (VersionUtils.hasQ()) {
window.decorView.isForceDarkAllowed = false
}
}
private fun updateTheme() {
setTheme(getThemeResValue())
if (PreferenceUtil.materialYou) {
setDefaultNightMode(getNightMode())
}
if (PreferenceUtil.isCustomFont) {
setTheme(R.style.FontThemeOverlay)
}
if (PreferenceUtil.circlePlayButton) {
setTheme(R.style.CircleFABOverlay)
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideStatusBar()
handler.removeCallbacks(this)
handler.postDelayed(this, 300)
} else {
handler.removeCallbacks(this)
}
}
private fun registerSystemUiVisibility() {
val decorView = window.decorView
decorView.setOnSystemUiVisibilityChangeListener { visibility ->
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
setImmersiveFullscreen()
}
}
}
private fun unregisterSystemUiVisibility() {
val decorView = window.decorView
decorView.setOnSystemUiVisibilityChangeListener(null)
}
override fun run() {
setImmersiveFullscreen()
}
override fun onStop() {
handler.removeCallbacks(this)
super.onStop()
}
public override fun onDestroy() {
super.onDestroy()
unregisterSystemUiVisibility()
exitFullscreen()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
handler.removeCallbacks(this)
handler.postDelayed(this, 500)
}
return super.onKeyDown(keyCode, event)
}
override fun attachBaseContext(newBase: Context?) {
val code = PreferenceUtil.languageCode
val locale = if (code == "auto") {
// Get the device default locale
ConfigurationCompat.getLocales(Resources.getSystem().configuration)[0]
} else {
Locale.forLanguageTag(code)
}
super.attachBaseContext(LanguageContextWrapper.wrap(newBase, locale))
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.bugreport
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsThemeActivity
import code.name.monkey.retromusic.activities.bugreport.model.DeviceInfo
import code.name.monkey.retromusic.databinding.ActivityBugReportBinding
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
import code.name.monkey.retromusic.extensions.showToast
open class BugReportActivity : AbsThemeActivity() {
private lateinit var binding: ActivityBugReportBinding
private var deviceInfo: DeviceInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBugReportBinding.inflate(layoutInflater)
setContentView(binding.root)
setTaskDescriptionColorAuto()
initViews()
if (title.isNullOrEmpty()) setTitle(R.string.report_an_issue)
deviceInfo = DeviceInfo(this)
binding.cardDeviceInfo.airTextDeviceInfo.text = deviceInfo.toString()
}
private fun initViews() {
val accentColor = accentColor()
setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.cardDeviceInfo.airTextDeviceInfo.setOnClickListener { copyDeviceInfoToClipBoard() }
TintHelper.setTintAuto(binding.sendFab, accentColor, true)
binding.sendFab.setOnClickListener { reportIssue() }
}
private fun reportIssue() {
copyDeviceInfoToClipBoard()
val i = Intent(Intent.ACTION_VIEW)
i.data = ISSUE_TRACKER_LINK.toUri()
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i)
}
private fun copyDeviceInfoToClipBoard() {
val clipboard = getSystemService<ClipboardManager>()
val clip = ClipData.newPlainText(getString(R.string.device_info), deviceInfo?.toMarkdown())
clipboard?.setPrimaryClip(clip)
showToast(R.string.copied_device_info_to_clipboard)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
}
return super.onOptionsItemSelected(item)
}
private fun tryToFinishActivity() {
if (!isFinishing) {
finish()
}
}
companion object {
private const val ISSUE_TRACKER_LINK =
"https://github.com/MuntashirAkon/Metro/issues/new"
}
}

View file

@ -0,0 +1,111 @@
package code.name.monkey.retromusic.activities.bugreport.model
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.IntRange
import androidx.core.content.pm.PackageInfoCompat
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.PreferenceUtil.isAdaptiveColor
import code.name.monkey.retromusic.util.PreferenceUtil.languageCode
import code.name.monkey.retromusic.util.PreferenceUtil.nowPlayingScreen
import java.util.*
class DeviceInfo(context: Context) {
@SuppressLint("NewApi")
private val abis = Build.SUPPORTED_ABIS
@SuppressLint("NewApi")
private val abis32Bits = Build.SUPPORTED_32_BIT_ABIS
@SuppressLint("NewApi")
private val abis64Bits = Build.SUPPORTED_64_BIT_ABIS
private val baseTheme: String
private val brand = Build.BRAND
private val buildID = Build.DISPLAY
private val buildVersion = Build.VERSION.INCREMENTAL
private val device = Build.DEVICE
private val hardware = Build.HARDWARE
private val isAdaptive: Boolean
private val manufacturer = Build.MANUFACTURER
private val model = Build.MODEL
private val nowPlayingTheme: String
private val product = Build.PRODUCT
private val releaseVersion = Build.VERSION.RELEASE
@IntRange(from = 0)
private val sdkVersion = Build.VERSION.SDK_INT
private var versionCode = 0L
private var versionName: String? = null
private val selectedLang: String
fun toMarkdown(): String {
return """
Device info:
---
<table>
<tr><td><b>App version</b></td><td>$versionName</td></tr>
<tr><td>App version code</td><td>$versionCode</td></tr>
<tr><td>Android build version</td><td>$buildVersion</td></tr>
<tr><td>Android release version</td><td>$releaseVersion</td></tr>
<tr><td>Android SDK version</td><td>$sdkVersion</td></tr>
<tr><td>Android build ID</td><td>$buildID</td></tr>
<tr><td>Device brand</td><td>$brand</td></tr>
<tr><td>Device manufacturer</td><td>$manufacturer</td></tr>
<tr><td>Device name</td><td>$device</td></tr>
<tr><td>Device model</td><td>$model</td></tr>
<tr><td>Device product name</td><td>$product</td></tr>
<tr><td>Device hardware name</td><td>$hardware</td></tr>
<tr><td>ABIs</td><td>${Arrays.toString(abis)}</td></tr>
<tr><td>ABIs (32bit)</td><td>${Arrays.toString(abis32Bits)}</td></tr>
<tr><td>ABIs (64bit)</td><td>${Arrays.toString(abis64Bits)}</td></tr>
<tr><td>Language</td><td>$selectedLang</td></tr>
</table>
""".trimIndent()
}
override fun toString(): String {
return """
App version: $versionName
App version code: $versionCode
Android build version: $buildVersion
Android release version: $releaseVersion
Android SDK version: $sdkVersion
Android build ID: $buildID
Device brand: $brand
Device manufacturer: $manufacturer
Device name: $device
Device model: $model
Device product name: $product
Device hardware name: $hardware
ABIs: ${Arrays.toString(abis)}
ABIs (32bit): ${Arrays.toString(abis32Bits)}
ABIs (64bit): ${Arrays.toString(abis64Bits)}
Base theme: $baseTheme
Now playing theme: $nowPlayingTheme
Adaptive: $isAdaptive
System language: ${Locale.getDefault().toLanguageTag()}
In-App Language: $selectedLang
""".trimIndent()
}
init {
val packageInfo = try {
context.packageManager.getPackageInfo(context.packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
if (packageInfo != null) {
versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
versionName = packageInfo.versionName
} else {
versionCode = -1
versionName = null
}
baseTheme = PreferenceUtil.baseTheme
nowPlayingTheme = context.getString(nowPlayingScreen.titleRes)
isAdaptive = isAdaptiveColor
selectedLang = languageCode
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.activities.saf;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.heinrichreimersoftware.materialintro.app.IntroActivity;
import com.heinrichreimersoftware.materialintro.slide.SimpleSlide;
import io.github.muntashirakon.music.R;
/** Created by hemanths on 2019-07-31. */
public class SAFGuideActivity extends IntroActivity {
public static final int REQUEST_CODE_SAF_GUIDE = 98;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setButtonCtaVisible(false);
setButtonNextVisible(false);
setButtonBackVisible(false);
setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT);
String title =
String.format(getString(R.string.saf_guide_slide1_title), getString(R.string.app_name));
addSlide(
new SimpleSlide.Builder()
.title(title)
.description(
Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1
? R.string.saf_guide_slide1_description_before_o
: R.string.saf_guide_slide1_description)
.image(R.drawable.saf_guide_1)
.background(R.color.md_deep_purple_300)
.backgroundDark(R.color.md_deep_purple_400)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
addSlide(
new SimpleSlide.Builder()
.title(R.string.saf_guide_slide2_title)
.description(R.string.saf_guide_slide2_description)
.image(R.drawable.saf_guide_2)
.background(R.color.md_deep_purple_500)
.backgroundDark(R.color.md_deep_purple_600)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
addSlide(
new SimpleSlide.Builder()
.title(R.string.saf_guide_slide3_title)
.description(R.string.saf_guide_slide3_description)
.image(R.drawable.saf_guide_3)
.background(R.color.md_deep_purple_700)
.backgroundDark(R.color.md_deep_purple_800)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021 Bartlomiej Uliasz.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.activities.saf
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE
import code.name.monkey.retromusic.util.SAFUtil
/** Created by buliasz on 2021-02-07. */
class SAFRequestActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, code.name.monkey.retromusic.activities.saf.SAFGuideActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_SAF_GUIDE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
when (requestCode) {
REQUEST_CODE_SAF_GUIDE -> {
SAFUtil.openTreePicker(this)
}
SAFUtil.REQUEST_SAF_PICK_TREE -> {
if (resultCode == RESULT_OK) {
SAFUtil.saveTreeUri(this, intent)
}
finish()
}
}
}
}

View file

@ -0,0 +1,468 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.tageditor
import android.app.Activity
import android.app.SearchManager
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.animation.OvershootInterpolator
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.hideSoftKeyboard
import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
import code.name.monkey.retromusic.model.ArtworkInfo
import code.name.monkey.retromusic.model.AudioTagInfo
import code.name.monkey.retromusic.repository.Repository
import code.name.monkey.retromusic.util.SAFUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jaudiotagger.audio.AudioFile
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
import org.koin.android.ext.android.inject
import java.io.File
import java.util.*
abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
abstract val editorImage: ImageView
val repository by inject<Repository>()
lateinit var saveFab: MaterialButton
protected var id: Long = 0
private set
private var paletteColorPrimary: Int = 0
private var songPaths: List<String>? = null
private var savedSongPaths: List<String>? = null
private val currentSongPath: String? = null
private var savedTags: Map<FieldKey, String>? = null
private var savedArtworkInfo: ArtworkInfo? = null
private var _binding: VB? = null
protected val binding: VB get() = _binding!!
private var cacheFiles = listOf<File>()
abstract val bindingInflater: (LayoutInflater) -> VB
private lateinit var launcher: ActivityResultLauncher<IntentSenderRequest>
protected abstract fun loadImageFromFile(selectedFile: Uri?)
protected val show: AlertDialog
get() =
MaterialAlertDialogBuilder(this)
.setTitle(R.string.update_image)
.setItems(items.toTypedArray()) { _, position ->
when (position) {
0 -> startImagePicker()
1 -> searchImageOnWeb()
2 -> deleteImage()
}
}
.setNegativeButton(R.string.action_cancel, null)
.show()
.colorButtons()
internal val albumArtist: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM_ARTIST)
} catch (ignored: Exception) {
null
}
}
protected val songTitle: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.TITLE)
} catch (ignored: Exception) {
null
}
}
protected val composer: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.COMPOSER)
} catch (ignored: Exception) {
null
}
}
protected val albumTitle: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM)
} catch (ignored: Exception) {
null
}
}
protected val artistName: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ARTIST)
} catch (ignored: Exception) {
null
}
}
protected val albumArtistName: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM_ARTIST)
} catch (ignored: Exception) {
null
}
}
protected val genreName: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.GENRE)
} catch (ignored: Exception) {
null
}
}
protected val songYear: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.YEAR)
} catch (ignored: Exception) {
null
}
}
protected val trackNumber: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.TRACK)
} catch (ignored: Exception) {
null
}
}
protected val discNumber: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.DISC_NO)
} catch (ignored: Exception) {
null
}
}
protected val lyrics: String?
get() {
return try {
getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.LYRICS)
} catch (ignored: Exception) {
null
}
}
protected val albumArt: Bitmap?
get() {
try {
val artworkTag = getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.firstArtwork
if (artworkTag != null) {
val artworkBinaryData = artworkTag.binaryData
return BitmapFactory.decodeByteArray(
artworkBinaryData,
0,
artworkBinaryData.size
)
}
return null
} catch (ignored: Exception) {
return null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = bindingInflater.invoke(layoutInflater)
setContentView(binding.root)
setTaskDescriptionColorAuto()
saveFab = findViewById(R.id.saveTags)
getIntentExtras()
songPaths = getSongPaths()
println(songPaths?.size)
if (songPaths!!.isEmpty()) {
finish()
}
setUpViews()
launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
writeToFiles(getSongUris(), cacheFiles)
}
}
}
private fun setUpViews() {
setUpFab()
setUpImageView()
}
private lateinit var items: List<String>
private fun setUpImageView() {
loadCurrentImage()
items = listOf(
getString(R.string.pick_from_local_storage),
getString(R.string.web_search),
getString(R.string.remove_cover)
)
editorImage.setOnClickListener { show }
}
private fun startImagePicker() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
startActivityForResult(
Intent.createChooser(
intent,
getString(R.string.pick_from_local_storage)
), REQUEST_CODE_SELECT_IMAGE
)
}
protected abstract fun loadCurrentImage()
protected abstract fun searchImageOnWeb()
protected abstract fun deleteImage()
private fun setUpFab() {
saveFab.accentColor()
saveFab.apply {
scaleX = 0f
scaleY = 0f
isEnabled = false
setOnClickListener { save() }
TintHelper.setTintAuto(this, ThemeStore.accentColor(this@AbsTagEditorActivity), true)
}
}
protected abstract fun save()
private fun getIntentExtras() {
val intentExtras = intent.extras
if (intentExtras != null) {
id = intentExtras.getLong(EXTRA_ID)
}
}
protected abstract fun getSongPaths(): List<String>
protected abstract fun getSongUris(): List<Uri>
protected fun searchWebFor(vararg keys: String) {
val stringBuilder = StringBuilder()
for (key in keys) {
stringBuilder.append(key)
stringBuilder.append(" ")
}
val intent = Intent(Intent.ACTION_WEB_SEARCH)
intent.putExtra(SearchManager.QUERY, stringBuilder.toString())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
super.onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
protected fun dataChanged() {
showFab()
}
private fun showFab() {
saveFab.animate().setDuration(500).setInterpolator(OvershootInterpolator()).scaleX(1f)
.scaleY(1f).start()
saveFab.isEnabled = true
}
private fun hideFab() {
saveFab.animate().setDuration(500).setInterpolator(OvershootInterpolator()).scaleX(0.0f)
.scaleY(0.0f).start()
saveFab.isEnabled = false
}
protected fun setImageBitmap(bitmap: Bitmap?, bgColor: Int) {
if (bitmap == null) {
editorImage.setImageResource(R.drawable.default_audio_art)
} else {
editorImage.setImageBitmap(bitmap)
}
setColors(bgColor)
}
protected open fun setColors(color: Int) {
paletteColorPrimary = color
}
protected fun writeValuesToFiles(
fieldKeyValueMap: Map<FieldKey, String>,
artworkInfo: ArtworkInfo?
) {
hideSoftKeyboard()
hideFab()
println(fieldKeyValueMap)
GlobalScope.launch {
if (VersionUtils.hasR()) {
cacheFiles = TagWriter.writeTagsToFilesR(
this@AbsTagEditorActivity, AudioTagInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
} else {
TagWriter.writeTagsToFiles(
this@AbsTagEditorActivity, AudioTagInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
}
}
}
private fun writeTags(paths: List<String>?) {
GlobalScope.launch {
if (VersionUtils.hasR()) {
cacheFiles = TagWriter.writeTagsToFilesR(
this@AbsTagEditorActivity, AudioTagInfo(
paths,
savedTags,
savedArtworkInfo
)
)
val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
} else {
TagWriter.writeTagsToFiles(
this@AbsTagEditorActivity, AudioTagInfo(
paths,
savedTags,
savedArtworkInfo
)
)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
when (requestCode) {
REQUEST_CODE_SELECT_IMAGE -> if (resultCode == Activity.RESULT_OK) {
intent?.data?.let {
loadImageFromFile(it)
}
}
code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE -> {
SAFUtil.openTreePicker(this)
}
SAFUtil.REQUEST_SAF_PICK_TREE -> {
if (resultCode == Activity.RESULT_OK) {
SAFUtil.saveTreeUri(this, intent)
writeTags(savedSongPaths)
}
}
SAFUtil.REQUEST_SAF_PICK_FILE -> {
if (resultCode == Activity.RESULT_OK) {
writeTags(Collections.singletonList(currentSongPath + SAFUtil.SEPARATOR + intent!!.dataString))
}
}
}
}
private fun getAudioFile(path: String): AudioFile {
return try {
AudioFileIO.read(File(path))
} catch (e: Exception) {
Log.e(TAG, "Could not read audio file $path", e)
AudioFile()
}
}
private fun writeToFiles(songUris: List<Uri>, cacheFiles: List<File>) {
if (cacheFiles.size == songUris.size) {
for (i in cacheFiles.indices) {
contentResolver.openOutputStream(songUris[i])?.use { output ->
cacheFiles[i].inputStream().use { input ->
input.copyTo(output)
}
}
}
}
lifecycleScope.launch {
TagWriter.scan(this@AbsTagEditorActivity, getSongPaths())
}
}
override fun onDestroy() {
super.onDestroy()
// Delete Cache Files
cacheFiles.forEach { file ->
file.delete()
}
}
companion object {
const val EXTRA_ID = "extra_id"
const val EXTRA_PALETTE = "extra_palette"
private val TAG = AbsTagEditorActivity::class.java.simpleName
private const val REQUEST_CODE_SELECT_IMAGE = 1000
}
}

View file

@ -0,0 +1,215 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.tageditor
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.transition.Slide
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.Toast
import androidx.core.widget.doAfterTextChanged
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.ActivityAlbumTagEditorBinding
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.model.ArtworkInfo
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.ImageUtil
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
import code.name.monkey.retromusic.util.RetroColorUtil.getColor
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.ImageViewTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.shape.MaterialShapeDrawable
import org.jaudiotagger.tag.FieldKey
import java.util.*
class AlbumTagEditorActivity : AbsTagEditorActivity<ActivityAlbumTagEditorBinding>() {
override val bindingInflater: (LayoutInflater) -> ActivityAlbumTagEditorBinding =
ActivityAlbumTagEditorBinding::inflate
private fun windowEnterTransition() {
val slide = Slide()
slide.excludeTarget(R.id.appBarLayout, true)
slide.excludeTarget(R.id.status_bar, true)
slide.excludeTarget(android.R.id.statusBarBackground, true)
slide.excludeTarget(android.R.id.navigationBarBackground, true)
window.enterTransition = slide
}
private var albumArtBitmap: Bitmap? = null
private var deleteAlbumArt: Boolean = false
private fun setupToolbar() {
setSupportActionBar(binding.toolbar)
binding.appBarLayout?.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.sharedElementsUseOverlay = true
binding.imageContainer.transitionName = getString(R.string.transition_album_art)
windowEnterTransition()
setUpViews()
setupToolbar()
}
private fun setUpViews() {
fillViewsWithFileTags()
binding.yearContainer.setTint(false)
binding.genreContainer.setTint(false)
binding.albumTitleContainer.setTint(false)
binding.albumArtistContainer.setTint(false)
binding.albumText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.albumArtistText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.genreTitle.appHandleColor().doAfterTextChanged { dataChanged() }
binding.yearTitle.appHandleColor().doAfterTextChanged { dataChanged() }
}
private fun fillViewsWithFileTags() {
binding.albumText.setText(albumTitle)
binding.albumArtistText.setText(albumArtistName)
binding.genreTitle.setText(genreName)
binding.yearTitle.setText(songYear)
println(albumTitle + albumArtistName)
}
override fun loadCurrentImage() {
val bitmap = albumArt
setImageBitmap(
bitmap,
getColor(
generatePalette(bitmap),
defaultFooterColor()
)
)
deleteAlbumArt = false
}
private fun toastLoadingFailed() {
showToast(R.string.could_not_download_album_cover)
}
override fun searchImageOnWeb() {
searchWebFor(binding.albumText.text.toString(), binding.albumArtistText.text.toString())
}
override fun deleteImage() {
setImageBitmap(
BitmapFactory.decodeResource(resources, R.drawable.default_audio_art),
defaultFooterColor()
)
deleteAlbumArt = true
dataChanged()
}
override fun loadImageFromFile(selectedFile: Uri?) {
GlideApp.with(this@AlbumTagEditorActivity).asBitmapPalette().load(selectedFile)
.diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true)
.into(object : ImageViewTarget<BitmapPaletteWrapper>(binding.editorImage) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
getColor(resource.palette, Color.TRANSPARENT)
albumArtBitmap = resource.bitmap?.let { ImageUtil.resizeBitmap(it, 2048) }
setImageBitmap(
albumArtBitmap,
getColor(
resource.palette,
defaultFooterColor()
)
)
deleteAlbumArt = false
dataChanged()
setResult(Activity.RESULT_OK)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
showToast(R.string.error_load_failed, Toast.LENGTH_LONG)
}
override fun setResource(resource: BitmapPaletteWrapper?) {}
})
}
override fun save() {
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
fieldKeyValueMap[FieldKey.ALBUM] = binding.albumText.text.toString()
// android seems not to recognize album_artist field so we additionally write the normal artist field
fieldKeyValueMap[FieldKey.ARTIST] = binding.albumArtistText.text.toString()
fieldKeyValueMap[FieldKey.ALBUM_ARTIST] = binding.albumArtistText.text.toString()
fieldKeyValueMap[FieldKey.GENRE] = binding.genreTitle.text.toString()
fieldKeyValueMap[FieldKey.YEAR] = binding.yearTitle.text.toString()
writeValuesToFiles(
fieldKeyValueMap,
when {
deleteAlbumArt -> ArtworkInfo(id, null)
albumArtBitmap == null -> null
else -> ArtworkInfo(id, albumArtBitmap!!)
}
)
}
override fun getSongPaths(): List<String> {
return repository.albumById(id).songs
.map(Song::data)
}
override fun getSongUris(): List<Uri> = repository.albumById(id).songs.map {
MusicUtil.getSongFileUri(it.id)
}
override fun setColors(color: Int) {
super.setColors(color)
saveFab.backgroundTintList = ColorStateList.valueOf(color)
saveFab.backgroundTintList = ColorStateList.valueOf(color)
ColorStateList.valueOf(
MaterialValueHelper.getPrimaryTextColor(
this,
color.isColorLight
)
).also {
saveFab.iconTint = it
saveFab.setTextColor(it)
}
}
override val editorImage: ImageView
get() = binding.editorImage
companion object {
val TAG: String = AlbumTagEditorActivity::class.java.simpleName
}
}

View file

@ -0,0 +1,209 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.activities.tageditor
import android.annotation.SuppressLint
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.Toast
import androidx.core.widget.doAfterTextChanged
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.ActivitySongTagEditorBinding
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.model.ArtworkInfo
import code.name.monkey.retromusic.repository.SongRepository
import code.name.monkey.retromusic.util.ImageUtil
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.RetroColorUtil
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.ImageViewTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.shape.MaterialShapeDrawable
import org.jaudiotagger.tag.FieldKey
import org.koin.android.ext.android.inject
import java.util.*
class SongTagEditorActivity : AbsTagEditorActivity<ActivitySongTagEditorBinding>() {
override val bindingInflater: (LayoutInflater) -> ActivitySongTagEditorBinding =
ActivitySongTagEditorBinding::inflate
private val songRepository by inject<SongRepository>()
private var albumArtBitmap: Bitmap? = null
private var deleteAlbumArt: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpViews()
setSupportActionBar(binding.toolbar)
binding.appBarLayout?.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(this)
}
@SuppressLint("ClickableViewAccessibility")
private fun setUpViews() {
fillViewsWithFileTags()
binding.songTextContainer.setTint(false)
binding.composerContainer.setTint(false)
binding.albumTextContainer.setTint(false)
binding.artistContainer.setTint(false)
binding.albumArtistContainer.setTint(false)
binding.yearContainer.setTint(false)
binding.genreContainer.setTint(false)
binding.trackNumberContainer.setTint(false)
binding.discNumberContainer.setTint(false)
binding.lyricsContainer.setTint(false)
binding.songText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.albumText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.albumArtistText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.artistText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.genreText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.yearText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.trackNumberText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.discNumberText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.lyricsText.appHandleColor().doAfterTextChanged { dataChanged() }
binding.songComposerText.appHandleColor().doAfterTextChanged { dataChanged() }
}
private fun fillViewsWithFileTags() {
binding.songText.setText(songTitle)
binding.albumArtistText.setText(albumArtist)
binding.albumText.setText(albumTitle)
binding.artistText.setText(artistName)
binding.genreText.setText(genreName)
binding.yearText.setText(songYear)
binding.trackNumberText.setText(trackNumber)
binding.discNumberText.setText(discNumber)
binding.lyricsText.setText(lyrics)
binding.songComposerText.setText(composer)
println(songTitle + songYear)
}
override fun loadCurrentImage() {
val bitmap = albumArt
setImageBitmap(
bitmap,
RetroColorUtil.getColor(
RetroColorUtil.generatePalette(bitmap),
defaultFooterColor()
)
)
deleteAlbumArt = false
}
override fun searchImageOnWeb() {
searchWebFor(binding.songText.text.toString(), binding.artistText.text.toString())
}
override fun deleteImage() {
setImageBitmap(
BitmapFactory.decodeResource(resources, R.drawable.default_audio_art),
defaultFooterColor()
)
deleteAlbumArt = true
dataChanged()
}
override fun setColors(color: Int) {
super.setColors(color)
saveFab.backgroundTintList = ColorStateList.valueOf(color)
ColorStateList.valueOf(
MaterialValueHelper.getPrimaryTextColor(
this,
color.isColorLight
)
).also {
saveFab.iconTint = it
saveFab.setTextColor(it)
}
}
override fun save() {
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
fieldKeyValueMap[FieldKey.TITLE] = binding.songText.text.toString()
fieldKeyValueMap[FieldKey.ALBUM] = binding.albumText.text.toString()
fieldKeyValueMap[FieldKey.ARTIST] = binding.artistText.text.toString()
fieldKeyValueMap[FieldKey.GENRE] = binding.genreText.text.toString()
fieldKeyValueMap[FieldKey.YEAR] = binding.yearText.text.toString()
fieldKeyValueMap[FieldKey.TRACK] = binding.trackNumberText.text.toString()
fieldKeyValueMap[FieldKey.DISC_NO] = binding.discNumberText.text.toString()
fieldKeyValueMap[FieldKey.LYRICS] = binding.lyricsText.text.toString()
fieldKeyValueMap[FieldKey.ALBUM_ARTIST] = binding.albumArtistText.text.toString()
fieldKeyValueMap[FieldKey.COMPOSER] = binding.songComposerText.text.toString()
writeValuesToFiles(
fieldKeyValueMap, when {
deleteAlbumArt -> ArtworkInfo(id, null)
albumArtBitmap == null -> null
else -> ArtworkInfo(id, albumArtBitmap!!)
}
)
}
override fun getSongPaths(): List<String> = listOf(songRepository.song(id).data)
override fun getSongUris(): List<Uri> = listOf(MusicUtil.getSongFileUri(id))
override fun loadImageFromFile(selectedFile: Uri?) {
GlideApp.with(this@SongTagEditorActivity).asBitmapPalette().load(selectedFile)
.diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true)
.into(object : ImageViewTarget<BitmapPaletteWrapper>(binding.editorImage) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
RetroColorUtil.getColor(resource.palette, Color.TRANSPARENT)
albumArtBitmap = resource.bitmap?.let { ImageUtil.resizeBitmap(it, 2048) }
setImageBitmap(
albumArtBitmap,
RetroColorUtil.getColor(
resource.palette,
defaultFooterColor()
)
)
deleteAlbumArt = false
dataChanged()
setResult(Activity.RESULT_OK)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
showToast(R.string.error_load_failed, Toast.LENGTH_LONG)
}
override fun setResource(resource: BitmapPaletteWrapper?) {}
})
}
companion object {
val TAG: String = SongTagEditorActivity::class.java.simpleName
}
override val editorImage: ImageView
get() = binding.editorImage
}

View file

@ -0,0 +1,201 @@
package code.name.monkey.retromusic.activities.tageditor
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener
import code.name.monkey.retromusic.model.AudioTagInfo
import code.name.monkey.retromusic.util.MusicUtil.createAlbumArtFile
import code.name.monkey.retromusic.util.MusicUtil.deleteAlbumArt
import code.name.monkey.retromusic.util.MusicUtil.insertAlbumArt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.audio.exceptions.CannotReadException
import org.jaudiotagger.audio.exceptions.CannotWriteException
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException
import org.jaudiotagger.tag.TagException
import org.jaudiotagger.tag.images.AndroidArtwork
import org.jaudiotagger.tag.images.Artwork
import java.io.File
import java.io.IOException
class TagWriter {
companion object {
suspend fun scan(context: Context, toBeScanned: List<String?>?) {
if (toBeScanned == null || toBeScanned.isEmpty()) {
Log.i("scan", "scan: Empty")
context.showToast( "Scan file from folder")
return
}
MediaScannerConnection.scanFile(
context,
toBeScanned.toTypedArray(),
null,
withContext(Dispatchers.Main) {
if (context is Activity) UpdateToastMediaScannerCompletionListener(
context, toBeScanned
) else null
}
)
}
suspend fun writeTagsToFiles(context: Context, info: AudioTagInfo) {
withContext(Dispatchers.IO) {
runCatching {
var artwork: Artwork? = null
var albumArtFile: File? = null
if (info.artworkInfo?.artwork != null) {
try {
albumArtFile = createAlbumArtFile(context).canonicalFile
info.artworkInfo.artwork.compress(
Bitmap.CompressFormat.JPEG,
100,
albumArtFile.outputStream()
)
artwork = AndroidArtwork.createArtworkFromFile(albumArtFile)
} catch (e: IOException) {
e.printStackTrace()
}
}
var wroteArtwork = false
var deletedArtwork = false
for (filePath in info.filePaths!!) {
try {
val audioFile = AudioFileIO.read(File(filePath))
val tag = audioFile.tagOrCreateAndSetDefault
if (info.fieldKeyValueMap != null) {
for ((key, value) in info.fieldKeyValueMap) {
try {
tag.setField(key, value)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
if (info.artworkInfo != null) {
if (info.artworkInfo.artwork == null) {
tag.deleteArtworkField()
deletedArtwork = true
} else if (artwork != null) {
tag.deleteArtworkField()
tag.setField(artwork)
wroteArtwork = true
}
}
audioFile.commit()
} catch (e: CannotReadException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: CannotWriteException) {
e.printStackTrace()
} catch (e: TagException) {
e.printStackTrace()
} catch (e: ReadOnlyFileException) {
e.printStackTrace()
} catch (e: InvalidAudioFrameException) {
e.printStackTrace()
}
}
if (wroteArtwork) {
insertAlbumArt(context, info.artworkInfo!!.albumId, albumArtFile!!.path)
} else if (deletedArtwork) {
deleteAlbumArt(context, info.artworkInfo!!.albumId)
}
scan(context, info.filePaths)
}.onFailure {
it.printStackTrace()
}
}
}
@RequiresApi(Build.VERSION_CODES.R)
suspend fun writeTagsToFilesR(context: Context, info: AudioTagInfo): List<File> =
withContext(Dispatchers.IO) {
val cacheFiles = mutableListOf<File>()
runCatching {
var artwork: Artwork? = null
var albumArtFile: File? = null
if (info.artworkInfo?.artwork != null) {
try {
albumArtFile = createAlbumArtFile(context).canonicalFile
info.artworkInfo.artwork.compress(
Bitmap.CompressFormat.JPEG,
100,
albumArtFile.outputStream()
)
artwork = AndroidArtwork.createArtworkFromFile(albumArtFile)
} catch (e: IOException) {
e.printStackTrace()
}
}
var wroteArtwork = false
var deletedArtwork = false
for (filePath in info.filePaths!!) {
try {
val originFile = File(filePath)
val cacheFile = File(context.cacheDir, originFile.name)
cacheFiles.add(cacheFile)
originFile.inputStream().use { input ->
cacheFile.outputStream().use { output ->
input.copyTo(output)
}
}
val audioFile = AudioFileIO.read(cacheFile)
val tag = audioFile.tagOrCreateAndSetDefault
if (info.fieldKeyValueMap != null) {
for ((key, value) in info.fieldKeyValueMap) {
try {
tag.setField(key, value)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
if (info.artworkInfo != null) {
if (info.artworkInfo.artwork == null) {
tag.deleteArtworkField()
deletedArtwork = true
} else if (artwork != null) {
tag.deleteArtworkField()
tag.setField(artwork)
wroteArtwork = true
}
}
audioFile.commit()
} catch (e: CannotReadException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: CannotWriteException) {
e.printStackTrace()
} catch (e: TagException) {
e.printStackTrace()
} catch (e: ReadOnlyFileException) {
e.printStackTrace()
} catch (e: InvalidAudioFrameException) {
e.printStackTrace()
}
}
if (wroteArtwork) {
insertAlbumArt(context, info.artworkInfo!!.albumId, albumArtFile!!.path)
} else if (deletedArtwork) {
deleteAlbumArt(context, info.artworkInfo!!.albumId)
}
}.onFailure {
it.printStackTrace()
}
cacheFiles
}
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.adapter
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.appthemehelper.ThemeStore.Companion.accentColor
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.PreferenceDialogLibraryCategoriesListitemBinding
import code.name.monkey.retromusic.extensions.showToast
import code.name.monkey.retromusic.model.CategoryInfo
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.SwipeAndDragHelper
import code.name.monkey.retromusic.util.SwipeAndDragHelper.ActionCompletionContract
class CategoryInfoAdapter : RecyclerView.Adapter<code.name.monkey.retromusic.adapter.CategoryInfoAdapter.ViewHolder>(),
ActionCompletionContract {
var categoryInfos: MutableList<CategoryInfo> =
PreferenceUtil.libraryCategory.toMutableList()
@SuppressLint("NotifyDataSetChanged")
set(value) {
field = value
notifyDataSetChanged()
}
private val touchHelper: ItemTouchHelper
fun attachToRecyclerView(recyclerView: RecyclerView?) {
touchHelper.attachToRecyclerView(recyclerView)
}
override fun getItemCount(): Int {
return categoryInfos.size
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val categoryInfo = categoryInfos[position]
holder.binding.checkbox.isChecked = categoryInfo.visible
holder.binding.title.text =
holder.binding.title.resources.getString(categoryInfo.category.stringRes)
holder.itemView.setOnClickListener {
if (!(categoryInfo.visible && isLastCheckedCategory(categoryInfo))) {
categoryInfo.visible = !categoryInfo.visible
holder.binding.checkbox.isChecked = categoryInfo.visible
} else {
holder.itemView.context.showToast(R.string.you_have_to_select_at_least_one_category)
}
}
holder.binding.dragView.setOnTouchListener { _: View?, event: MotionEvent ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(holder)
}
false
}
}
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): ViewHolder {
return ViewHolder(
PreferenceDialogLibraryCategoriesListitemBinding.inflate(
LayoutInflater.from(
parent.context
), parent, false
)
)
}
override fun onViewMoved(oldPosition: Int, newPosition: Int) {
val categoryInfo = categoryInfos[oldPosition]
categoryInfos.removeAt(oldPosition)
categoryInfos.add(newPosition, categoryInfo)
notifyItemMoved(oldPosition, newPosition)
}
private fun isLastCheckedCategory(categoryInfo: CategoryInfo): Boolean {
if (categoryInfo.visible) {
for (c in categoryInfos) {
if (c !== categoryInfo && c.visible) {
return false
}
}
}
return true
}
class ViewHolder(val binding: PreferenceDialogLibraryCategoriesListitemBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.checkbox.buttonTintList =
ColorStateList.valueOf(accentColor(binding.checkbox.context))
}
}
init {
val swipeAndDragHelper = SwipeAndDragHelper(this)
touchHelper = ItemTouchHelper(swipeAndDragHelper)
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.ItemGenreBinding
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.interfaces.IGenreClickListener
import code.name.monkey.retromusic.model.Genre
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import java.util.*
/**
* @author Hemanth S (h4h13).
*/
class GenreAdapter(
private val activity: FragmentActivity,
var dataSet: List<Genre>,
private val listener: IGenreClickListener
) : RecyclerView.Adapter<GenreAdapter.ViewHolder>() {
init {
this.setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return dataSet[position].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemGenreBinding.inflate(LayoutInflater.from(activity), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val genre = dataSet[position]
holder.binding.title.text = genre.name
holder.binding.text.text = String.format(
Locale.getDefault(),
"%d %s",
genre.songCount,
if (genre.songCount > 1) activity.getString(R.string.songs) else activity.getString(R.string.song)
)
loadGenreImage(genre, holder)
}
private fun loadGenreImage(genre: Genre, holder: GenreAdapter.ViewHolder) {
val genreSong = MusicUtil.songByGenre(genre.id)
GlideApp.with(activity)
.asBitmapPalette()
.load(RetroGlideExtension.getSongModel(genreSong))
.songCoverOptions(genreSong)
.into(object : RetroMusicColoredTarget(holder.binding.image) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColors(holder, colors)
}
})
// Just for a bit of shadow around image
holder.binding.image.outlineProvider = ViewOutlineProvider.BOUNDS
}
private fun setColors(holder: ViewHolder, color: MediaNotificationProcessor) {
holder.binding.imageContainerCard.setCardBackgroundColor(color.backgroundColor)
holder.binding.title.setTextColor(color.primaryTextColor)
holder.binding.text.setTextColor(color.secondaryTextColor)
}
override fun getItemCount(): Int {
return dataSet.size
}
@SuppressLint("NotifyDataSetChanged")
fun swapDataSet(list: List<Genre>) {
dataSet = list
notifyDataSetChanged()
}
inner class ViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
override fun onClick(v: View?) {
listener.onClickGenre(dataSet[layoutPosition], itemView)
}
init {
itemView.setOnClickListener(this)
}
}
}

View file

@ -0,0 +1,224 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.os.bundleOf
import androidx.fragment.app.findFragment
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.retromusic.*
import code.name.monkey.retromusic.adapter.album.AlbumAdapter
import code.name.monkey.retromusic.adapter.artist.ArtistAdapter
import code.name.monkey.retromusic.adapter.song.SongAdapter
import code.name.monkey.retromusic.fragments.home.HomeFragment
import code.name.monkey.retromusic.interfaces.IAlbumClickListener
import code.name.monkey.retromusic.interfaces.IArtistClickListener
import code.name.monkey.retromusic.interfaces.IGenreClickListener
import code.name.monkey.retromusic.model.*
import code.name.monkey.retromusic.util.PreferenceUtil
class HomeAdapter(
private val activity: AppCompatActivity
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), IArtistClickListener, IAlbumClickListener,
IGenreClickListener {
private var list = listOf<Home>()
override fun getItemViewType(position: Int): Int {
return list[position].homeSection
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout =
LayoutInflater.from(activity).inflate(R.layout.section_recycler_view, parent, false)
return when (viewType) {
RECENT_ARTISTS, TOP_ARTISTS -> ArtistViewHolder(layout)
FAVOURITES -> PlaylistViewHolder(layout)
TOP_ALBUMS, RECENT_ALBUMS -> AlbumViewHolder(layout)
else -> {
ArtistViewHolder(layout)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val home = list[position]
when (getItemViewType(position)) {
RECENT_ALBUMS -> {
val viewHolder = holder as AlbumViewHolder
viewHolder.bindView(home)
viewHolder.clickableArea.setOnClickListener {
it.findFragment<HomeFragment>().setSharedAxisXTransitions()
activity.findNavController(R.id.fragment_container).navigate(
R.id.detailListFragment,
bundleOf("type" to RECENT_ALBUMS)
)
}
}
TOP_ALBUMS -> {
val viewHolder = holder as AlbumViewHolder
viewHolder.bindView(home)
viewHolder.clickableArea.setOnClickListener {
it.findFragment<HomeFragment>().setSharedAxisXTransitions()
activity.findNavController(R.id.fragment_container).navigate(
R.id.detailListFragment,
bundleOf("type" to TOP_ALBUMS)
)
}
}
RECENT_ARTISTS -> {
val viewHolder = holder as ArtistViewHolder
viewHolder.bindView(home)
viewHolder.clickableArea.setOnClickListener {
it.findFragment<HomeFragment>().setSharedAxisXTransitions()
activity.findNavController(R.id.fragment_container).navigate(
R.id.detailListFragment,
bundleOf("type" to RECENT_ARTISTS)
)
}
}
TOP_ARTISTS -> {
val viewHolder = holder as ArtistViewHolder
viewHolder.bindView(home)
viewHolder.clickableArea.setOnClickListener {
it.findFragment<HomeFragment>().setSharedAxisXTransitions()
activity.findNavController(R.id.fragment_container).navigate(
R.id.detailListFragment,
bundleOf("type" to TOP_ARTISTS)
)
}
}
FAVOURITES -> {
val viewHolder = holder as PlaylistViewHolder
viewHolder.bindView(home)
viewHolder.clickableArea.setOnClickListener {
it.findFragment<HomeFragment>().setSharedAxisXTransitions()
activity.findNavController(R.id.fragment_container).navigate(
R.id.detailListFragment,
bundleOf("type" to FAVOURITES)
)
}
}
}
}
override fun getItemCount(): Int {
return list.size
}
@SuppressLint("NotifyDataSetChanged")
fun swapData(sections: List<Home>) {
list = sections
notifyDataSetChanged()
}
private inner class AlbumViewHolder(view: View) : AbsHomeViewItem(view) {
fun bindView(home: Home) {
title.setText(home.titleRes)
recyclerView.apply {
adapter = albumAdapter(home.arrayList as List<Album>)
layoutManager = gridLayoutManager()
}
}
}
private inner class ArtistViewHolder(view: View) : AbsHomeViewItem(view) {
fun bindView(home: Home) {
title.setText(home.titleRes)
recyclerView.apply {
layoutManager = linearLayoutManager()
adapter = artistsAdapter(home.arrayList as List<Artist>)
}
}
}
private inner class PlaylistViewHolder(view: View) : AbsHomeViewItem(view) {
fun bindView(home: Home) {
title.setText(home.titleRes)
recyclerView.apply {
val songAdapter = SongAdapter(
activity,
home.arrayList as MutableList<Song>,
R.layout.item_favourite_card, null
)
layoutManager = linearLayoutManager()
adapter = songAdapter
}
}
}
open class AbsHomeViewItem(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recyclerView: RecyclerView = itemView.findViewById(R.id.recyclerView)
val title: AppCompatTextView = itemView.findViewById(R.id.title)
val arrow: ImageView = itemView.findViewById(R.id.arrow)
val clickableArea: ViewGroup = itemView.findViewById(R.id.clickable_area)
}
private fun artistsAdapter(artists: List<Artist>) =
ArtistAdapter(activity, artists, PreferenceUtil.homeArtistGridStyle, null, this)
private fun albumAdapter(albums: List<Album>) =
AlbumAdapter(activity, albums, PreferenceUtil.homeAlbumGridStyle, null, this)
private fun gridLayoutManager() =
GridLayoutManager(activity, 1, GridLayoutManager.HORIZONTAL, false)
private fun linearLayoutManager() =
LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
override fun onArtist(artistId: Long, view: View) {
activity.findNavController(R.id.fragment_container).navigate(
R.id.artistDetailsFragment,
bundleOf(EXTRA_ARTIST_ID to artistId),
null,
FragmentNavigatorExtras(
view to artistId.toString()
)
)
}
override fun onAlbumClick(albumId: Long, view: View) {
activity.findNavController(R.id.fragment_container).navigate(
R.id.albumDetailsFragment,
bundleOf(EXTRA_ALBUM_ID to albumId),
null,
FragmentNavigatorExtras(
view to albumId.toString()
)
)
}
override fun onClickGenre(genre: Genre, view: View) {
activity.findNavController(R.id.fragment_container).navigate(
R.id.genreDetailsFragment,
bundleOf(EXTRA_GENRE to genre),
null,
FragmentNavigatorExtras(
view to "genre"
)
)
}
}

View file

@ -0,0 +1,221 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.retromusic.*
import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.menu.SongMenuHelper
import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.model.Genre
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import java.util.*
class SearchAdapter(
private val activity: FragmentActivity,
private var dataSet: List<Any>
) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() {
@SuppressLint("NotifyDataSetChanged")
fun swapDataSet(dataSet: List<Any>) {
this.dataSet = dataSet
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
if (dataSet[position] is Album) return ALBUM
if (dataSet[position] is Artist) return if ((dataSet[position] as Artist).isAlbumArtist) ALBUM_ARTIST else ARTIST
if (dataSet[position] is Genre) return GENRE
if (dataSet[position] is PlaylistWithSongs) return PLAYLIST
return if (dataSet[position] is Song) SONG else HEADER
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (viewType == HEADER) ViewHolder(
LayoutInflater.from(activity).inflate(
R.layout.sub_header,
parent,
false
), viewType
)
else if (viewType == ALBUM || viewType == ARTIST || viewType== ALBUM_ARTIST)
ViewHolder(
LayoutInflater.from(activity).inflate(
R.layout.item_list_big,
parent,
false
), viewType
)
else
ViewHolder(
LayoutInflater.from(activity).inflate(R.layout.item_list, parent, false),
viewType
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (getItemViewType(position)) {
ALBUM -> {
holder.imageTextContainer?.isVisible = true
val album = dataSet[position] as Album
holder.title?.text = album.title
holder.text?.text = album.artistName
GlideApp.with(activity).asDrawable().albumCoverOptions(album.safeGetFirstSong()).load(
RetroGlideExtension.getSongModel(album.safeGetFirstSong()))
.into(holder.image!!)
}
ARTIST -> {
holder.imageTextContainer?.isVisible = true
val artist = dataSet[position] as Artist
holder.title?.text = artist.name
holder.text?.text = MusicUtil.getArtistInfoString(activity, artist)
GlideApp.with(activity).asDrawable().artistImageOptions(artist).load(
RetroGlideExtension.getArtistModel(artist)).into(holder.image!!)
}
SONG -> {
holder.imageTextContainer?.isVisible = true
val song = dataSet[position] as Song
holder.title?.text = song.title
holder.text?.text = song.albumName
GlideApp.with(activity).asDrawable().songCoverOptions(song).load(RetroGlideExtension.getSongModel(song)).into(holder.image!!)
}
GENRE -> {
val genre = dataSet[position] as Genre
holder.title?.text = genre.name
holder.text?.text = String.format(
Locale.getDefault(),
"%d %s",
genre.songCount,
if (genre.songCount > 1) activity.getString(R.string.songs) else activity.getString(
R.string.song
)
)
}
PLAYLIST -> {
val playlist = dataSet[position] as PlaylistWithSongs
holder.title?.text = playlist.playlistEntity.playlistName
//holder.text?.text = MusicUtil.playlistInfoString(activity, playlist.songs)
}
ALBUM_ARTIST -> {
holder.imageTextContainer?.isVisible = true
val artist = dataSet[position] as Artist
holder.title?.text = artist.name
holder.text?.text = MusicUtil.getArtistInfoString(activity, artist)
GlideApp.with(activity).asDrawable().artistImageOptions(artist).load(
RetroGlideExtension.getArtistModel(artist)).into(holder.image!!)
}
else -> {
holder.title?.text = dataSet[position].toString()
holder.title?.setTextColor(ThemeStore.accentColor(activity))
}
}
}
override fun getItemCount(): Int {
return dataSet.size
}
inner class ViewHolder(itemView: View, itemViewType: Int) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
init {
itemView.setOnLongClickListener(null)
imageTextContainer?.isInvisible = true
if (itemViewType == SONG) {
imageTextContainer?.isGone = true
menu?.isVisible = true
menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) {
override val song: Song
get() = dataSet[layoutPosition] as Song
})
} else {
menu?.isVisible = false
}
when (itemViewType) {
ALBUM -> setImageTransitionName(activity.getString(R.string.transition_album_art))
ARTIST -> setImageTransitionName(activity.getString(R.string.transition_artist_image))
else -> {
val container = itemView.findViewById<View>(R.id.imageContainer)
container?.isVisible = false
}
}
}
override fun onClick(v: View?) {
val item = dataSet[layoutPosition]
when (itemViewType) {
ALBUM -> {
activity.findNavController(R.id.fragment_container).navigate(
R.id.albumDetailsFragment,
bundleOf(EXTRA_ALBUM_ID to (item as Album).id)
)
}
ARTIST -> {
activity.findNavController(R.id.fragment_container).navigate(
R.id.artistDetailsFragment,
bundleOf(EXTRA_ARTIST_ID to (item as Artist).id)
)
}
ALBUM_ARTIST ->{
activity.findNavController(R.id.fragment_container).navigate(
R.id.albumArtistDetailsFragment,
bundleOf(EXTRA_ARTIST_NAME to (item as Artist).name)
)
}
GENRE -> {
activity.findNavController(R.id.fragment_container).navigate(
R.id.genreDetailsFragment,
bundleOf(EXTRA_GENRE to (item as Genre))
)
}
PLAYLIST -> {
activity.findNavController(R.id.fragment_container).navigate(
R.id.playlistDetailsFragment,
bundleOf(EXTRA_PLAYLIST to (item as PlaylistWithSongs))
)
}
SONG -> {
MusicPlayerRemote.playNext(item as Song)
MusicPlayerRemote.playNextSong()
}
}
}
}
companion object {
private const val HEADER = 0
private const val ALBUM = 1
private const val ARTIST = 2
private const val SONG = 3
private const val GENRE = 4
private const val PLAYLIST = 5
private const val ALBUM_ARTIST = 6
}
}

View file

@ -0,0 +1,198 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import code.name.monkey.appthemehelper.util.ATHUtil
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.audiocover.AudioFileCover
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.interfaces.ICallbacks
import code.name.monkey.retromusic.util.MusicUtil
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.MediaStoreSignature
import me.zhanghai.android.fastscroll.PopupTextProvider
import java.io.File
import java.text.DecimalFormat
import kotlin.math.log10
import kotlin.math.pow
class SongFileAdapter(
override val activity: AppCompatActivity,
private var dataSet: List<File>,
private val itemLayoutRes: Int,
private val iCallbacks: ICallbacks?,
iCabHolder: ICabHolder?,
) : AbsMultiSelectAdapter<SongFileAdapter.ViewHolder, File>(
activity, iCabHolder, R.menu.menu_media_selection
), PopupTextProvider {
init {
this.setHasStableIds(true)
}
override fun getItemViewType(position: Int): Int {
return if (dataSet[position].isDirectory) FOLDER else FILE
}
override fun getItemId(position: Int): Long {
return dataSet[position].hashCode().toLong()
}
fun swapDataSet(songFiles: List<File>) {
this.dataSet = songFiles
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, index: Int) {
val file = dataSet[index]
holder.itemView.isActivated = isChecked(file)
holder.title?.text = getFileTitle(file)
if (holder.text != null) {
if (holder.itemViewType == FILE) {
holder.text?.text = getFileText(file)
} else {
holder.text?.isVisible = false
}
}
if (holder.image != null) {
loadFileImage(file, holder)
}
}
private fun getFileTitle(file: File): String {
return file.name
}
private fun getFileText(file: File): String? {
return if (file.isDirectory) null else readableFileSize(file.length())
}
private fun loadFileImage(file: File, holder: ViewHolder) {
val iconColor = ATHUtil.resolveColor(activity, R.attr.colorControlNormal)
if (file.isDirectory) {
holder.image?.let {
it.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN)
it.setImageResource(R.drawable.ic_folder)
}
holder.imageTextContainer?.setCardBackgroundColor(
ATHUtil.resolveColor(
activity,
R.attr.colorSurface
)
)
} else {
val error = activity.getTintedDrawable(R.drawable.ic_file_music, iconColor)
GlideApp.with(activity)
.load(AudioFileCover(file.path))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(error)
.placeholder(error)
.transition(RetroGlideExtension.getDefaultTransition())
.signature(MediaStoreSignature("", file.lastModified(), 0))
.into(holder.image!!)
}
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getIdentifier(position: Int): File {
return dataSet[position]
}
override fun getName(model: File): String {
return getFileTitle(model)
}
override fun onMultipleItemAction(menuItem: MenuItem, selection: List<File>) {
if (iCallbacks == null) return
iCallbacks.onMultipleItemAction(menuItem, selection as ArrayList<File>)
}
override fun getPopupText(position: Int): String {
return if (position >= dataSet.lastIndex) "" else getSectionName(position)
}
private fun getSectionName(position: Int): String {
return MusicUtil.getSectionName(dataSet[position].name)
}
inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
init {
if (menu != null && iCallbacks != null) {
menu?.setOnClickListener { v ->
val position = layoutPosition
if (isPositionInRange(position)) {
iCallbacks.onFileMenuClicked(dataSet[position], v)
}
}
}
if (imageTextContainer != null) {
imageTextContainer?.cardElevation = 0f
}
}
override fun onClick(v: View?) {
val position = layoutPosition
if (isPositionInRange(position)) {
if (isInQuickSelectMode) {
toggleChecked(position)
} else {
iCallbacks?.onFileSelected(dataSet[position])
}
}
}
override fun onLongClick(v: View?): Boolean {
val position = layoutPosition
return isPositionInRange(position) && toggleChecked(position)
}
private fun isPositionInRange(position: Int): Boolean {
return position >= 0 && position < dataSet.size
}
}
companion object {
private const val FILE = 0
private const val FOLDER = 1
fun readableFileSize(size: Long): String {
if (size <= 0) return "$size B"
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()
return DecimalFormat("#,##0.##").format(size / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
}
}
}

View file

@ -0,0 +1,55 @@
package code.name.monkey.retromusic.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.retromusic.R
import java.io.File
class StorageAdapter(
val storageList: List<Storage>,
val storageClickListener: StorageClickListener
) :
RecyclerView.Adapter<StorageAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_storage,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindData(storageList[position])
}
override fun getItemCount(): Int {
return storageList.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.title)
fun bindData(storage: Storage) {
title.text = storage.title
}
init {
itemView.setOnClickListener { storageClickListener.onStorageClicked(storageList[bindingAdapterPosition]) }
}
}
}
interface StorageClickListener {
fun onStorageClicked(storage: Storage)
}
class Storage {
lateinit var title: String
lateinit var file: File
}

View file

@ -0,0 +1,198 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.album
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.helper.SortOrder
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
import code.name.monkey.retromusic.interfaces.IAlbumClickListener
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import me.zhanghai.android.fastscroll.PopupTextProvider
open class AlbumAdapter(
override val activity: FragmentActivity,
var dataSet: List<Album>,
var itemLayoutRes: Int,
iCabHolder: ICabHolder?,
val listener: IAlbumClickListener?
) : AbsMultiSelectAdapter<AlbumAdapter.ViewHolder, Album>(
activity,
iCabHolder,
R.menu.menu_media_selection
), PopupTextProvider {
init {
this.setHasStableIds(true)
}
fun swapDataSet(dataSet: List<Album>) {
this.dataSet = dataSet
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
return createViewHolder(view, viewType)
}
protected open fun createViewHolder(view: View, viewType: Int): ViewHolder {
return ViewHolder(view)
}
private fun getAlbumTitle(album: Album): String {
return album.title
}
protected open fun getAlbumText(album: Album): String? {
return album.albumArtist.let {
if (it.isNullOrEmpty()) {
album.artistName
} else {
it
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val album = dataSet[position]
val isChecked = isChecked(album)
holder.itemView.isActivated = isChecked
holder.title?.text = getAlbumTitle(album)
holder.text?.text = getAlbumText(album)
// Check if imageContainer exists so we can have a smooth transition without
// CardView clipping, if it doesn't exist in current layout set transition name to image instead.
if (holder.imageContainer != null) {
holder.imageContainer?.transitionName = album.id.toString()
} else {
holder.image?.transitionName = album.id.toString()
}
loadAlbumCover(album, holder)
}
protected open fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) {
if (holder.paletteColorContainer != null) {
holder.title?.setTextColor(color.primaryTextColor)
holder.text?.setTextColor(color.secondaryTextColor)
holder.paletteColorContainer?.setBackgroundColor(color.backgroundColor)
}
holder.mask?.backgroundTintList = ColorStateList.valueOf(color.primaryTextColor)
holder.imageContainerCard?.setCardBackgroundColor(color.backgroundColor)
}
protected open fun loadAlbumCover(album: Album, holder: ViewHolder) {
if (holder.image == null) {
return
}
val song = album.safeGetFirstSong()
GlideApp.with(activity).asBitmapPalette().albumCoverOptions(song)
//.checkIgnoreMediaStore()
.load(RetroGlideExtension.getSongModel(song))
.into(object : RetroMusicColoredTarget(holder.image!!) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColors(colors, holder)
}
})
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getItemId(position: Int): Long {
return dataSet[position].id
}
override fun getIdentifier(position: Int): Album? {
return dataSet[position]
}
override fun getName(model: Album): String {
return model.title
}
override fun onMultipleItemAction(
menuItem: MenuItem,
selection: List<Album>
) {
SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId)
}
private fun getSongList(albums: List<Album>): List<Song> {
val songs = ArrayList<Song>()
for (album in albums) {
songs.addAll(album.songs)
}
return songs
}
override fun getPopupText(position: Int): String {
return getSectionName(position)
}
private fun getSectionName(position: Int): String {
var sectionName: String? = null
when (PreferenceUtil.albumSortOrder) {
SortOrder.AlbumSortOrder.ALBUM_A_Z, SortOrder.AlbumSortOrder.ALBUM_Z_A -> sectionName =
dataSet[position].title
SortOrder.AlbumSortOrder.ALBUM_ARTIST -> sectionName = dataSet[position].albumArtist
SortOrder.AlbumSortOrder.ALBUM_YEAR -> return MusicUtil.getYearString(
dataSet[position].year
)
}
return MusicUtil.getSectionName(sectionName)
}
inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
init {
menu?.isVisible = false
}
override fun onClick(v: View?) {
super.onClick(v)
if (isInQuickSelectMode) {
toggleChecked(layoutPosition)
} else {
image?.let {
listener?.onAlbumClick(dataSet[layoutPosition].id, imageContainer ?: it)
}
}
}
override fun onLongClick(v: View?): Boolean {
return toggleChecked(layoutPosition)
}
}
companion object {
val TAG: String = AlbumAdapter::class.java.simpleName
}
}

View file

@ -0,0 +1,215 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.album
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.fragments.AlbumCoverStyle
import code.name.monkey.retromusic.fragments.NowPlayingScreen.*
import code.name.monkey.retromusic.fragments.base.goToLyrics
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AlbumCoverPagerAdapter(
fragmentManager: FragmentManager,
private val dataSet: List<Song>
) : CustomFragmentStatePagerAdapter(fragmentManager) {
private var currentColorReceiver: AlbumCoverFragment.ColorReceiver? = null
private var currentColorReceiverPosition = -1
override fun getItem(position: Int): Fragment {
return AlbumCoverFragment.newInstance(dataSet[position])
}
override fun getCount(): Int {
return dataSet.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val o = super.instantiateItem(container, position)
if (currentColorReceiver != null && currentColorReceiverPosition == position) {
receiveColor(currentColorReceiver!!, currentColorReceiverPosition)
}
return o
}
/**
* Only the latest passed [AlbumCoverFragment.ColorReceiver] is guaranteed to receive a
* response
*/
fun receiveColor(colorReceiver: AlbumCoverFragment.ColorReceiver, position: Int) {
if (getFragment(position) is AlbumCoverFragment) {
val fragment = getFragment(position) as AlbumCoverFragment
currentColorReceiver = null
currentColorReceiverPosition = -1
fragment.receiveColor(colorReceiver, position)
} else {
currentColorReceiver = colorReceiver
currentColorReceiverPosition = position
}
}
class AlbumCoverFragment : Fragment() {
private var isColorReady: Boolean = false
private lateinit var color: MediaNotificationProcessor
private lateinit var song: Song
private var colorReceiver: ColorReceiver? = null
private var request: Int = 0
private val mainActivity get() = activity as MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
song = requireArguments().getParcelable(SONG_ARG)!!
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false)
view.setOnClickListener {
if (mainActivity.getBottomSheetBehavior().state == STATE_EXPANDED) {
showLyricsDialog()
}
}
return view
}
private fun showLyricsDialog() {
lifecycleScope.launch(Dispatchers.IO) {
val data: String? = MusicUtil.getLyrics(song)
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(
requireContext(),
R.style.ThemeOverlay_MaterialComponents_Dialog_Alert
).apply {
setTitle(song.title)
setMessage(if (data.isNullOrEmpty()) "No lyrics found" else data)
setNegativeButton(R.string.synced_lyrics) { _, _ ->
goToLyrics(requireActivity())
}
show()
}
}
}
}
private fun getLayoutWithPlayerTheme(): Int {
return when (PreferenceUtil.nowPlayingScreen) {
Card, Fit, Tiny, Classic, Gradient, Full -> R.layout.fragment_album_full_cover
Peek -> R.layout.fragment_peek_album_cover
else -> {
if (PreferenceUtil.isCarouselEffect) {
R.layout.fragment_album_carousel_cover
} else {
when (PreferenceUtil.albumCoverStyle) {
AlbumCoverStyle.Normal -> R.layout.fragment_album_cover
AlbumCoverStyle.Flat -> R.layout.fragment_album_flat_cover
AlbumCoverStyle.Circle -> R.layout.fragment_album_circle_cover
AlbumCoverStyle.Card -> R.layout.fragment_album_card_cover
AlbumCoverStyle.Full -> R.layout.fragment_album_full_cover
AlbumCoverStyle.FullCard -> R.layout.fragment_album_full_card_cover
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadAlbumCover(albumCover = view.findViewById(R.id.player_image))
}
override fun onDestroyView() {
super.onDestroyView()
colorReceiver = null
}
private fun loadAlbumCover(albumCover: ImageView) {
GlideApp.with(this).asBitmapPalette().songCoverOptions(song)
//.checkIgnoreMediaStore()
.load(RetroGlideExtension.getSongModel(song))
.dontAnimate()
.into(object : RetroMusicColoredTarget(albumCover) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColor(colors)
}
})
}
private fun setColor(color: MediaNotificationProcessor) {
this.color = color
isColorReady = true
if (colorReceiver != null) {
colorReceiver!!.onColorReady(color, request)
colorReceiver = null
}
}
internal fun receiveColor(colorReceiver: ColorReceiver, request: Int) {
if (isColorReady) {
colorReceiver.onColorReady(color, request)
} else {
this.colorReceiver = colorReceiver
this.request = request
}
}
interface ColorReceiver {
fun onColorReady(color: MediaNotificationProcessor, request: Int)
}
companion object {
private const val SONG_ARG = "song"
fun newInstance(song: Song): AlbumCoverFragment {
val frag = AlbumCoverFragment()
frag.arguments = bundleOf(SONG_ARG to song)
return frag
}
}
}
companion object {
val TAG: String = AlbumCoverPagerAdapter::class.java.simpleName
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.album
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.helper.HorizontalAdapterHelper
import code.name.monkey.retromusic.interfaces.IAlbumClickListener
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Album
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
class HorizontalAlbumAdapter(
activity: FragmentActivity,
dataSet: List<Album>,
ICabHolder: ICabHolder?,
albumClickListener: IAlbumClickListener
) : AlbumAdapter(
activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, ICabHolder, albumClickListener
) {
override fun createViewHolder(view: View, viewType: Int): ViewHolder {
val params = view.layoutParams as ViewGroup.MarginLayoutParams
HorizontalAdapterHelper.applyMarginToLayoutParams(activity, params, viewType)
return ViewHolder(view)
}
override fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) {
// holder.title?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorPrimary))
// holder.text?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorSecondary))
}
override fun loadAlbumCover(album: Album, holder: ViewHolder) {
if (holder.image == null) return
GlideApp.with(activity).asBitmapPalette().albumCoverOptions(album.safeGetFirstSong())
.load(RetroGlideExtension.getSongModel(album.safeGetFirstSong()))
.into(object : RetroMusicColoredTarget(holder.image!!) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColors(colors, holder)
}
})
}
override fun getAlbumText(album: Album): String {
return MusicUtil.getYearString(album.year)
}
override fun getItemViewType(position: Int): Int {
return HorizontalAdapterHelper.getItemViewType(position, itemCount)
}
override fun getItemCount(): Int {
return dataSet.size
}
companion object {
val TAG: String = AlbumAdapter::class.java.simpleName
}
}

View file

@ -0,0 +1,187 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.artist
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
import code.name.monkey.retromusic.extensions.hide
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
import code.name.monkey.retromusic.interfaces.IAlbumArtistClickListener
import code.name.monkey.retromusic.interfaces.IArtistClickListener
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import me.zhanghai.android.fastscroll.PopupTextProvider
class ArtistAdapter(
override val activity: FragmentActivity,
var dataSet: List<Artist>,
var itemLayoutRes: Int,
val ICabHolder: ICabHolder?,
val IArtistClickListener: IArtistClickListener,
val IAlbumArtistClickListener: IAlbumArtistClickListener? = null
) : AbsMultiSelectAdapter<ArtistAdapter.ViewHolder, Artist>(
activity, ICabHolder, R.menu.menu_media_selection
), PopupTextProvider {
var albumArtistsOnly = false
init {
this.setHasStableIds(true)
}
@SuppressLint("NotifyDataSetChanged")
fun swapDataSet(dataSet: List<Artist>) {
this.dataSet = dataSet
notifyDataSetChanged()
albumArtistsOnly = PreferenceUtil.albumArtistsOnly
}
override fun getItemId(position: Int): Long {
return dataSet[position].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
try {
LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
} catch (e: Resources.NotFoundException) {
LayoutInflater.from(activity).inflate(R.layout.item_grid_circle, parent, false)
}
return createViewHolder(view)
}
private fun createViewHolder(view: View): ViewHolder {
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val artist = dataSet[position]
val isChecked = isChecked(artist)
holder.itemView.isActivated = isChecked
holder.title?.text = artist.name
holder.text?.hide()
val transitionName =
if (albumArtistsOnly) artist.name else artist.id.toString()
if (holder.imageContainer != null) {
holder.imageContainer?.transitionName = transitionName
} else {
holder.image?.transitionName = transitionName
}
loadArtistImage(artist, holder)
}
private fun setColors(processor: MediaNotificationProcessor, holder: ViewHolder) {
holder.mask?.backgroundTintList = ColorStateList.valueOf(processor.primaryTextColor)
if (holder.paletteColorContainer != null) {
holder.paletteColorContainer?.setBackgroundColor(processor.backgroundColor)
holder.title?.setTextColor(processor.primaryTextColor)
}
holder.imageContainerCard?.setCardBackgroundColor(processor.backgroundColor)
}
private fun loadArtistImage(artist: Artist, holder: ViewHolder) {
if (holder.image == null) {
return
}
GlideApp.with(activity)
.asBitmapPalette()
.load(RetroGlideExtension.getArtistModel(artist))
.artistImageOptions(artist)
.transition(RetroGlideExtension.getDefaultTransition())
.into(object : RetroMusicColoredTarget(holder.image!!) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColors(colors, holder)
}
})
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getIdentifier(position: Int): Artist {
return dataSet[position]
}
override fun getName(model: Artist): String {
return model.name
}
override fun onMultipleItemAction(
menuItem: MenuItem,
selection: List<Artist>
) {
SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId)
}
private fun getSongList(artists: List<Artist>): List<Song> {
val songs = ArrayList<Song>()
for (artist in artists) {
songs.addAll(artist.songs) // maybe async in future?
}
return songs
}
override fun getPopupText(position: Int): String {
return getSectionName(position)
}
private fun getSectionName(position: Int): String {
return MusicUtil.getSectionName(dataSet[position].name)
}
inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
init {
menu?.isVisible = false
}
override fun onClick(v: View?) {
super.onClick(v)
if (isInQuickSelectMode) {
toggleChecked(layoutPosition)
} else {
val artist = dataSet[layoutPosition]
image?.let {
if (albumArtistsOnly && IAlbumArtistClickListener != null) {
IAlbumArtistClickListener.onAlbumArtist(artist.name, imageContainer ?: it)
} else {
IArtistClickListener.onArtist(artist.id, imageContainer ?: it)
}
}
}
}
override fun onLongClick(v: View?): Boolean {
return toggleChecked(layoutPosition)
}
}
}

View file

@ -0,0 +1,65 @@
package code.name.monkey.retromusic.adapter.backup
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.ItemListBackupBinding
import java.io.File
class BackupAdapter(
val activity: FragmentActivity,
var dataSet: MutableList<File>,
val backupClickedListener: BackupClickedListener
) : RecyclerView.Adapter<BackupAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemListBackupBinding.inflate(LayoutInflater.from(activity), parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.title.text = dataSet[position].nameWithoutExtension
}
override fun getItemCount(): Int = dataSet.size
@SuppressLint("NotifyDataSetChanged")
fun swapDataset(dataSet: List<File>) {
this.dataSet = ArrayList(dataSet)
notifyDataSetChanged()
}
inner class ViewHolder(val binding: ItemListBackupBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.menu.setOnClickListener { view ->
val popupMenu = PopupMenu(activity, view)
popupMenu.inflate(R.menu.menu_backup)
popupMenu.setOnMenuItemClickListener { menuItem ->
return@setOnMenuItemClickListener backupClickedListener.onBackupMenuClicked(
dataSet[bindingAdapterPosition],
menuItem
)
}
popupMenu.show()
}
itemView.setOnClickListener {
backupClickedListener.onBackupClicked(dataSet[bindingAdapterPosition])
}
}
}
interface BackupClickedListener {
fun onBackupClicked(file: File)
fun onBackupMenuClicked(file: File, menuItem: MenuItem): Boolean
}
}

View file

@ -0,0 +1,125 @@
package code.name.monkey.retromusic.adapter.base
import android.annotation.SuppressLint
import android.graphics.Color
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.MenuRes
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.surfaceColor
import code.name.monkey.retromusic.interfaces.ICabCallback
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.util.RetroColorUtil
import com.afollestad.materialcab.attached.AttachedCab
import com.afollestad.materialcab.attached.destroy
import com.afollestad.materialcab.attached.isActive
abstract class AbsMultiSelectAdapter<V : RecyclerView.ViewHolder?, I>(
open val activity: FragmentActivity, private val ICabHolder: ICabHolder?, @MenuRes menuRes: Int
) : RecyclerView.Adapter<V>(), ICabCallback {
private var cab: AttachedCab? = null
private val checked: MutableList<I>
private var menuRes: Int
override fun onCabCreated(cab: AttachedCab, menu: Menu): Boolean {
activity.window.statusBarColor =
RetroColorUtil.shiftBackgroundColor(activity.surfaceColor())
return true
}
override fun onCabFinished(cab: AttachedCab): Boolean {
clearChecked()
activity.window.statusBarColor = when {
VersionUtils.hasMarshmallow() -> Color.TRANSPARENT
else -> Color.BLACK
}
return true
}
override fun onCabItemClicked(item: MenuItem): Boolean {
if (item.itemId == R.id.action_multi_select_adapter_check_all) {
checkAll()
} else {
onMultipleItemAction(item, ArrayList(checked))
cab?.destroy()
clearChecked()
}
return true
}
private fun checkAll() {
if (ICabHolder != null) {
checked.clear()
for (i in 0 until itemCount) {
val identifier = getIdentifier(i)
if (identifier != null) {
checked.add(identifier)
}
}
notifyDataSetChanged()
updateCab()
}
}
protected abstract fun getIdentifier(position: Int): I?
protected abstract fun getName(model: I): String?
protected fun isChecked(identifier: I): Boolean {
return checked.contains(identifier)
}
protected val isInQuickSelectMode: Boolean
get() = cab != null && cab!!.isActive()
protected abstract fun onMultipleItemAction(menuItem: MenuItem, selection: List<I>)
protected fun setMultiSelectMenuRes(@MenuRes menuRes: Int) {
this.menuRes = menuRes
}
protected fun toggleChecked(position: Int): Boolean {
if (ICabHolder != null) {
val identifier = getIdentifier(position) ?: return false
if (!checked.remove(identifier)) {
checked.add(identifier)
}
notifyItemChanged(position)
updateCab()
return true
}
return false
}
private fun clearChecked() {
checked.clear()
notifyDataSetChanged()
}
@SuppressLint("StringFormatInvalid", "StringFormatMatches")
private fun updateCab() {
if (ICabHolder != null) {
if (cab == null || !cab!!.isActive()) {
cab = ICabHolder.openCab(menuRes, this)
}
val size = checked.size
when {
size <= 0 -> {
cab?.destroy()
}
size == 1 -> {
cab?.title(literal = getName(checked[0]))
}
else -> {
cab?.title(literal = activity.getString(R.string.x_selected, size))
}
}
}
}
init {
checked = ArrayList()
this.menuRes = menuRes
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.adapter.base;
import android.graphics.Color;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import com.google.android.material.card.MaterialCardView;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder;
import io.github.muntashirakon.music.R;
public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHolder
implements View.OnLongClickListener, View.OnClickListener {
@Nullable
public View dragView;
@Nullable
public View dummyContainer;
@Nullable
public ImageView image;
@Nullable
public MaterialCardView imageContainerCard;
@Nullable
public FrameLayout imageContainer;
@Nullable
public TextView imageText;
@Nullable
public MaterialCardView imageTextContainer;
@Nullable
public View mask;
@Nullable
public AppCompatImageView menu;
@Nullable
public View paletteColorContainer;
@Nullable
public TextView text;
@Nullable
public TextView text2;
@Nullable
public TextView time;
@Nullable
public TextView title;
public MediaEntryViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.title);
text = itemView.findViewById(R.id.text);
text2 = itemView.findViewById(R.id.text2);
image = itemView.findViewById(R.id.image);
time = itemView.findViewById(R.id.time);
imageText = itemView.findViewById(R.id.imageText);
imageTextContainer = itemView.findViewById(R.id.imageTextContainer);
imageContainerCard = itemView.findViewById(R.id.imageContainerCard);
imageContainer = itemView.findViewById(R.id.imageContainer);
menu = itemView.findViewById(R.id.menu);
dragView = itemView.findViewById(R.id.drag_view);
paletteColorContainer = itemView.findViewById(R.id.paletteColorContainer);
mask = itemView.findViewById(R.id.mask);
dummyContainer = itemView.findViewById(R.id.dummy_view);
if (imageContainerCard != null) {
imageContainerCard.setCardBackgroundColor(Color.TRANSPARENT);
}
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
@Nullable
@Override
public View getSwipeableContainerView() {
return null;
}
@Override
public void onClick(View v) {
}
@Override
public boolean onLongClick(View v) {
return false;
}
public void setImageTransitionName(@NonNull String transitionName) {
itemView.setTransitionName(transitionName);
/* if (imageContainerCard != null) {
imageContainerCard.setTransitionName(transitionName);
}
if (image != null) {
image.setTransitionName(transitionName);
}*/
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.playlist
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import code.name.monkey.retromusic.model.Playlist
import code.name.monkey.retromusic.util.MusicUtil
class LegacyPlaylistAdapter(
private val activity: FragmentActivity,
private var list: List<Playlist>,
private val layoutRes: Int,
private val playlistClickListener: PlaylistClickListener
) :
RecyclerView.Adapter<LegacyPlaylistAdapter.ViewHolder>() {
fun swapData(list: List<Playlist>) {
this.list = list
notifyDataSetChanged()
}
class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val playlist: Playlist = list[position]
holder.title?.text = playlist.name
holder.text?.text = MusicUtil.getPlaylistInfoString(activity, playlist.getSongs())
holder.itemView.setOnClickListener {
playlistClickListener.onPlaylistClick(playlist)
}
}
override fun getItemCount(): Int {
return list.size
}
interface PlaylistClickListener {
fun onPlaylistClick(playlist: Playlist)
}
}

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.playlist
import android.graphics.Color
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.isGone
import androidx.core.view.setPadding
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.db.toSongs
import code.name.monkey.retromusic.extensions.dipToPix
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.playlistPreview.PlaylistPreview
import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder
import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.interfaces.IPlaylistClickListener
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import me.zhanghai.android.fastscroll.PopupTextProvider
class PlaylistAdapter(
override val activity: FragmentActivity,
var dataSet: List<PlaylistWithSongs>,
private var itemLayoutRes: Int,
ICabHolder: ICabHolder?,
private val listener: IPlaylistClickListener
) : AbsMultiSelectAdapter<PlaylistAdapter.ViewHolder, PlaylistWithSongs>(
activity,
ICabHolder,
R.menu.menu_playlists_selection
), PopupTextProvider {
init {
setHasStableIds(true)
}
fun swapDataSet(dataSet: List<PlaylistWithSongs>) {
this.dataSet = dataSet
notifyDataSetChanged()
}
override fun getItemId(position: Int): Long {
return dataSet[position].playlistEntity.playListId
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
return createViewHolder(view)
}
fun createViewHolder(view: View): ViewHolder {
return ViewHolder(view)
}
private fun getPlaylistTitle(playlist: PlaylistEntity): String {
return playlist.playlistName.ifEmpty { "-" }
}
private fun getPlaylistText(playlist: PlaylistWithSongs): String {
return MusicUtil.getPlaylistInfoString(activity, playlist.songs.toSongs())
}
override fun getPopupText(position: Int): String {
val sectionName: String = when (PreferenceUtil.playlistSortOrder) {
PlaylistSortOrder.PLAYLIST_A_Z, PlaylistSortOrder.PLAYLIST_Z_A -> dataSet[position].playlistEntity.playlistName
PlaylistSortOrder.PLAYLIST_SONG_COUNT, PlaylistSortOrder.PLAYLIST_SONG_COUNT_DESC -> dataSet[position].songs.size.toString()
else -> {
return ""
}
}
return MusicUtil.getSectionName(sectionName)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val playlist = dataSet[position]
holder.itemView.isActivated = isChecked(playlist)
holder.title?.text = getPlaylistTitle(playlist.playlistEntity)
holder.text?.text = getPlaylistText(playlist)
holder.menu?.isGone = isChecked(playlist)
GlideApp.with(activity)
.load(
if (itemLayoutRes == R.layout.item_list) {
holder.image?.setPadding(activity.dipToPix(8F).toInt())
R.drawable.ic_playlist_play
} else PlaylistPreview(playlist)
)
.playlistOptions()
.into(holder.image!!)
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getIdentifier(position: Int): PlaylistWithSongs {
return dataSet[position]
}
override fun getName(model: PlaylistWithSongs): String {
return model.playlistEntity.playlistName
}
override fun onMultipleItemAction(menuItem: MenuItem, selection: List<PlaylistWithSongs>) {
when (menuItem.itemId) {
else -> SongsMenuHelper.handleMenuClick(
activity,
getSongList(selection),
menuItem.itemId
)
}
}
private fun getSongList(playlists: List<PlaylistWithSongs>): List<Song> {
val songs = mutableListOf<Song>()
playlists.forEach {
songs.addAll(it.songs.toSongs())
}
return songs
}
inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
init {
menu?.setOnClickListener { view ->
val popupMenu = PopupMenu(activity, view)
popupMenu.inflate(R.menu.menu_item_playlist)
popupMenu.setOnMenuItemClickListener { item ->
PlaylistMenuHelper.handleMenuClick(activity, dataSet[layoutPosition], item)
}
popupMenu.show()
}
imageTextContainer?.apply {
cardElevation = 0f
setCardBackgroundColor(Color.TRANSPARENT)
}
}
override fun onClick(v: View?) {
if (isInQuickSelectMode) {
toggleChecked(layoutPosition)
} else {
itemView.transitionName = "playlist"
listener.onPlaylistClick(dataSet[layoutPosition], itemView)
}
}
override fun onLongClick(v: View?): Boolean {
toggleChecked(layoutPosition)
return true
}
}
companion object {
val TAG: String = PlaylistAdapter::class.java.simpleName
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
abstract class AbsOffsetSongAdapter(
activity: FragmentActivity,
dataSet: MutableList<Song>,
@LayoutRes itemLayoutRes: Int,
ICabHolder: ICabHolder?
) : SongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongAdapter.ViewHolder {
if (viewType == OFFSET_ITEM) {
val view = LayoutInflater.from(activity)
.inflate(R.layout.item_list_quick_actions, parent, false)
return createViewHolder(view)
}
return super.onCreateViewHolder(parent, viewType)
}
override fun createViewHolder(view: View): SongAdapter.ViewHolder {
return ViewHolder(view)
}
override fun getItemId(position: Int): Long {
var positionFinal = position
positionFinal--
return if (positionFinal < 0) -2 else super.getItemId(positionFinal)
}
override fun getIdentifier(position: Int): Song? {
var positionFinal = position
positionFinal--
return if (positionFinal < 0) null else super.getIdentifier(positionFinal)
}
override fun getItemCount(): Int {
val superItemCount = super.getItemCount()
return if (superItemCount == 0) 0 else superItemCount + 1
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) OFFSET_ITEM else SONG
}
open inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) {
override // could also return null, just to be safe return empty song
val song: Song
get() = if (itemViewType == OFFSET_ITEM) Song.emptySong else dataSet[layoutPosition - 1]
override fun onClick(v: View?) {
if (isInQuickSelectMode && itemViewType != OFFSET_ITEM) {
toggleChecked(layoutPosition)
} else {
MusicPlayerRemote.openQueue(dataSet, layoutPosition - 1, true)
}
}
override fun onLongClick(v: View?): Boolean {
if (itemViewType == OFFSET_ITEM) return false
toggleChecked(layoutPosition)
return true
}
}
companion object {
const val OFFSET_ITEM = 0
const val SONG = 1
}
}

View file

@ -0,0 +1,172 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.db.toSongsEntity
import code.name.monkey.retromusic.dialogs.RemoveSongFromPlaylistDialog
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.accentOutlineColor
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
import com.google.android.material.button.MaterialButton
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
class OrderablePlaylistSongAdapter(
private val playlist: PlaylistEntity,
activity: FragmentActivity,
dataSet: MutableList<Song>,
itemLayoutRes: Int,
ICabHolder: ICabHolder?,
) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, ICabHolder),
DraggableItemAdapter<OrderablePlaylistSongAdapter.ViewHolder> {
val libraryViewModel: LibraryViewModel by activity.viewModel()
init {
this.setHasStableIds(true)
this.setMultiSelectMenuRes(R.menu.menu_playlists_songs_selection)
}
override fun getItemId(position: Int): Long {
// requires static value, it means need to keep the same value
// even if the item position has been changed.
return if (position != 0) {
dataSet[position - 1].id
} else {
-1
}
}
override fun createViewHolder(view: View): SongAdapter.ViewHolder {
return ViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) OFFSET_ITEM else SONG
}
override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
if (holder.itemViewType == OFFSET_ITEM) {
val viewHolder = holder as ViewHolder
viewHolder.playAction?.let {
it.setOnClickListener {
MusicPlayerRemote.openQueue(dataSet, 0, true)
}
it.accentOutlineColor()
}
viewHolder.shuffleAction?.let {
it.setOnClickListener {
MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
}
it.accentColor()
}
} else {
super.onBindViewHolder(holder, position - 1)
}
}
override fun onMultipleItemAction(menuItem: MenuItem, selection: List<Song>) {
when (menuItem.itemId) {
R.id.action_remove_from_playlist -> RemoveSongFromPlaylistDialog.create(
selection.toSongsEntity(
playlist
)
)
.show(activity.supportFragmentManager, "REMOVE_FROM_PLAYLIST")
else -> super.onMultipleItemAction(menuItem, selection)
}
}
inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) {
val playAction: MaterialButton? = itemView.findViewById(R.id.playAction)
val shuffleAction: MaterialButton? = itemView.findViewById(R.id.shuffleAction)
override var songMenuRes: Int
get() = R.menu.menu_item_playlist_song
set(value) {
super.songMenuRes = value
}
override fun onSongMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_remove_from_playlist -> {
RemoveSongFromPlaylistDialog.create(song.toSongEntity(playlist.playListId))
.show(activity.supportFragmentManager, "REMOVE_FROM_PLAYLIST")
return true
}
}
return super.onSongMenuItemClick(item)
}
init {
dragView?.isVisible = true
}
}
override fun onCheckCanStartDrag(holder: ViewHolder, position: Int, x: Int, y: Int): Boolean {
if (dataSet.size == 0 or 1 || isInQuickSelectMode) {
return false
}
val dragHandle = holder.dragView ?: return false
val handleWidth = dragHandle.width
val handleHeight = dragHandle.height
val handleLeft = dragHandle.left
val handleTop = dragHandle.top
return (x >= handleLeft && x < handleLeft + handleWidth &&
y >= handleTop && y < handleTop + handleHeight) && position != 0
}
override fun onMoveItem(fromPosition: Int, toPosition: Int) {
dataSet.add(toPosition - 1, dataSet.removeAt(fromPosition - 1))
}
override fun onGetItemDraggableRange(holder: ViewHolder, position: Int): ItemDraggableRange {
return ItemDraggableRange(1, itemCount - 1)
}
override fun onCheckCanDrop(draggingPosition: Int, dropPosition: Int): Boolean {
return true
}
override fun onItemDragStarted(position: Int) {
notifyDataSetChanged()
}
override fun onItemDragFinished(fromPosition: Int, toPosition: Int, result: Boolean) {
notifyDataSetChanged()
}
fun saveSongs(playlistEntity: PlaylistEntity) {
activity.lifecycleScope.launch(Dispatchers.IO) {
libraryViewModel.insertSongs(dataSet.toSongsEntity(playlistEntity))
}
}
}

View file

@ -0,0 +1,234 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.MusicPlayerRemote.isPlaying
import code.name.monkey.retromusic.helper.MusicPlayerRemote.playNextSong
import code.name.monkey.retromusic.helper.MusicPlayerRemote.removeFromQueue
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.ViewUtil
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange
import com.h6ah4i.android.widget.advrecyclerview.draggable.annotation.DraggableItemStateFlags
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
import me.zhanghai.android.fastscroll.PopupTextProvider
class PlayingQueueAdapter(
activity: FragmentActivity,
dataSet: MutableList<Song>,
private var current: Int,
itemLayoutRes: Int
) : SongAdapter(
activity, dataSet, itemLayoutRes, null
), DraggableItemAdapter<PlayingQueueAdapter.ViewHolder>,
SwipeableItemAdapter<PlayingQueueAdapter.ViewHolder>,
PopupTextProvider {
private var songToRemove: Song? = null
override fun createViewHolder(view: View): SongAdapter.ViewHolder {
return ViewHolder(view)
}
override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
val song = dataSet[position]
holder.time?.text = MusicUtil.getReadableDurationString(song.duration)
if (holder.itemViewType == HISTORY || holder.itemViewType == CURRENT) {
setAlpha(holder, 0.5f)
}
}
override fun getItemViewType(position: Int): Int {
if (position < current) {
return HISTORY
} else if (position > current) {
return UP_NEXT
}
return CURRENT
}
override fun loadAlbumCover(song: Song, holder: SongAdapter.ViewHolder) {
if (holder.image == null) {
return
}
GlideApp.with(activity)
.load(RetroGlideExtension.getSongModel(song))
.songCoverOptions(song)
.into(holder.image!!)
}
fun swapDataSet(dataSet: List<Song>, position: Int) {
this.dataSet = dataSet.toMutableList()
current = position
notifyDataSetChanged()
}
fun setCurrent(current: Int) {
this.current = current
notifyDataSetChanged()
}
private fun setAlpha(holder: SongAdapter.ViewHolder, alpha: Float) {
holder.image?.alpha = alpha
holder.title?.alpha = alpha
holder.text?.alpha = alpha
holder.paletteColorContainer?.alpha = alpha
holder.dragView?.alpha = alpha
holder.menu?.alpha = alpha
}
override fun getPopupText(position: Int): String {
return MusicUtil.getSectionName(dataSet[position].title)
}
override fun onCheckCanStartDrag(holder: ViewHolder, position: Int, x: Int, y: Int): Boolean {
return ViewUtil.hitTest(holder.imageText!!, x, y) || ViewUtil.hitTest(
holder.dragView!!,
x,
y
)
}
override fun onGetItemDraggableRange(holder: ViewHolder, position: Int): ItemDraggableRange? {
return null
}
override fun onMoveItem(fromPosition: Int, toPosition: Int) {
MusicPlayerRemote.moveSong(fromPosition, toPosition)
}
override fun onCheckCanDrop(draggingPosition: Int, dropPosition: Int): Boolean {
return true
}
override fun onItemDragStarted(position: Int) {
notifyDataSetChanged()
}
override fun onItemDragFinished(fromPosition: Int, toPosition: Int, result: Boolean) {
notifyDataSetChanged()
}
fun setSongToRemove(song: Song) {
songToRemove = song
}
inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) {
@DraggableItemStateFlags
private var mDragStateFlags: Int = 0
override var songMenuRes: Int
get() = R.menu.menu_item_playing_queue_song
set(value) {
super.songMenuRes = value
}
init {
dragView?.isVisible = true
}
override fun onSongMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_remove_from_playing_queue -> {
removeFromQueue(layoutPosition)
return true
}
}
return super.onSongMenuItemClick(item)
}
@DraggableItemStateFlags
override fun getDragStateFlags(): Int {
return mDragStateFlags
}
override fun setDragStateFlags(@DraggableItemStateFlags flags: Int) {
mDragStateFlags = flags
}
override fun getSwipeableContainerView(): View {
return dummyContainer!!
}
}
companion object {
private const val HISTORY = 0
private const val CURRENT = 1
private const val UP_NEXT = 2
}
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int): SwipeResultAction {
return if (result == SwipeableItemConstants.RESULT_CANCELED) {
SwipeResultActionDefault()
} else {
SwipedResultActionRemoveItem(this, position, activity)
}
}
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int {
return if (onCheckCanStartDrag(holder, position, x, y)) {
SwipeableItemConstants.REACTION_CAN_NOT_SWIPE_BOTH_H
} else {
SwipeableItemConstants.REACTION_CAN_SWIPE_BOTH_H
}
}
override fun onSwipeItemStarted(holder: ViewHolder, p1: Int) {
}
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, result: Int) {
}
internal class SwipedResultActionRemoveItem(
private val adapter: PlayingQueueAdapter,
private val position: Int,
private val activity: FragmentActivity
) : SwipeResultActionRemoveItem() {
private var songToRemove: Song? = null
private val isPlaying: Boolean = MusicPlayerRemote.isPlaying
private val songProgressMillis = 0
override fun onPerformAction() {
// currentlyShownSnackbar = null
}
override fun onSlideAnimationEnd() {
// initializeSnackBar(adapter, position, activity, isPlaying)
songToRemove = adapter.dataSet[position]
// If song removed was the playing song, then play the next song
if (isPlaying(songToRemove!!)) {
playNextSong()
}
// Swipe animation is much smoother when we do the heavy lifting after it's completed
adapter.setSongToRemove(songToRemove!!)
removeFromQueue(songToRemove!!)
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.accentOutlineColor
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil
import com.google.android.material.button.MaterialButton
class ShuffleButtonSongAdapter(
activity: FragmentActivity,
dataSet: MutableList<Song>,
itemLayoutRes: Int,
ICabHolder: ICabHolder?
) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) {
override fun createViewHolder(view: View): SongAdapter.ViewHolder {
return ViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) OFFSET_ITEM else SONG
}
override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
if (holder.itemViewType == OFFSET_ITEM) {
val viewHolder = holder as ViewHolder
viewHolder.playAction?.let {
it.setOnClickListener {
MusicPlayerRemote.openQueue(dataSet, 0, true)
}
it.accentOutlineColor()
}
viewHolder.shuffleAction?.let {
it.setOnClickListener {
MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
}
it.accentColor()
}
} else {
super.onBindViewHolder(holder, position - 1)
val landscape = RetroUtil.isLandscape
if ((PreferenceUtil.songGridSize > 2 && !landscape) || (PreferenceUtil.songGridSizeLand > 5 && landscape)) {
holder.menu?.isVisible = false
}
}
}
inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) {
val playAction: MaterialButton? = itemView.findViewById(R.id.playAction)
val shuffleAction: MaterialButton? = itemView.findViewById(R.id.shuffleAction)
override fun onClick(v: View?) {
if (itemViewType == OFFSET_ITEM) {
MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
return
}
super.onClick(v)
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
class SimpleSongAdapter(
context: FragmentActivity,
songs: ArrayList<Song>,
layoutRes: Int,
ICabHolder: ICabHolder?
) : SongAdapter(context, songs, layoutRes, ICabHolder) {
override fun swapDataSet(dataSet: List<Song>) {
this.dataSet = dataSet.toMutableList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
val fixedTrackNumber = MusicUtil.getFixedTrackNumber(dataSet[position].trackNumber)
val trackAndTime = (if (fixedTrackNumber > 0) "$fixedTrackNumber | " else "") +
MusicUtil.getReadableDurationString(dataSet[position].duration)
holder.time?.text = trackAndTime
holder.text2?.text = dataSet[position].artistName
}
override fun getItemCount(): Int {
return dataSet.size
}
}

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.adapter.song
import android.content.res.ColorStateList
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.findNavController
import code.name.monkey.retromusic.EXTRA_ALBUM_ID
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.SortOrder
import code.name.monkey.retromusic.helper.menu.SongMenuHelper
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
import code.name.monkey.retromusic.interfaces.ICabCallback
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import me.zhanghai.android.fastscroll.PopupTextProvider
/**
* Created by hemanths on 13/08/17.
*/
open class SongAdapter(
override val activity: FragmentActivity,
var dataSet: MutableList<Song>,
protected var itemLayoutRes: Int,
ICabHolder: ICabHolder?,
showSectionName: Boolean = true
) : AbsMultiSelectAdapter<SongAdapter.ViewHolder, Song>(
activity,
ICabHolder,
R.menu.menu_media_selection
), ICabCallback, PopupTextProvider {
private var showSectionName = true
init {
this.showSectionName = showSectionName
this.setHasStableIds(true)
}
open fun swapDataSet(dataSet: List<Song>) {
this.dataSet = ArrayList(dataSet)
notifyDataSetChanged()
}
override fun getItemId(position: Int): Long {
return dataSet[position].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
try {
LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
} catch (e: Resources.NotFoundException) {
LayoutInflater.from(activity).inflate(R.layout.item_list, parent, false)
}
return createViewHolder(view)
}
protected open fun createViewHolder(view: View): ViewHolder {
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val song = dataSet[position]
val isChecked = isChecked(song)
holder.itemView.isActivated = isChecked
holder.menu?.isGone = isChecked
holder.title?.text = getSongTitle(song)
holder.text?.text = getSongText(song)
holder.text2?.text = getSongText(song)
loadAlbumCover(song, holder)
val landscape = RetroUtil.isLandscape
if ((PreferenceUtil.songGridSize > 2 && !landscape) || (PreferenceUtil.songGridSizeLand > 5 && landscape)) {
holder.menu?.isVisible = false
}
}
private fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) {
if (holder.paletteColorContainer != null) {
holder.title?.setTextColor(color.primaryTextColor)
holder.text?.setTextColor(color.secondaryTextColor)
holder.paletteColorContainer?.setBackgroundColor(color.backgroundColor)
holder.menu?.imageTintList = ColorStateList.valueOf(color.primaryTextColor)
}
holder.mask?.backgroundTintList = ColorStateList.valueOf(color.primaryTextColor)
}
protected open fun loadAlbumCover(song: Song, holder: ViewHolder) {
if (holder.image == null) {
return
}
GlideApp.with(activity).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.into(object : RetroMusicColoredTarget(holder.image!!) {
override fun onColorReady(colors: MediaNotificationProcessor) {
setColors(colors, holder)
}
})
}
private fun getSongTitle(song: Song): String {
return song.title
}
private fun getSongText(song: Song): String {
return song.artistName
}
private fun getSongText2(song: Song): String {
return song.albumName
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getIdentifier(position: Int): Song? {
return dataSet[position]
}
override fun getName(model: Song): String {
return model.title
}
override fun onMultipleItemAction(menuItem: MenuItem, selection: List<Song>) {
SongsMenuHelper.handleMenuClick(activity, selection, menuItem.itemId)
}
override fun getPopupText(position: Int): String {
val sectionName: String? = when (PreferenceUtil.songSortOrder) {
SortOrder.SongSortOrder.SONG_A_Z, SortOrder.SongSortOrder.SONG_Z_A -> dataSet[position].title
SortOrder.SongSortOrder.SONG_ALBUM -> dataSet[position].albumName
SortOrder.SongSortOrder.SONG_ARTIST -> dataSet[position].artistName
SortOrder.SongSortOrder.SONG_YEAR -> return MusicUtil.getYearString(dataSet[position].year)
SortOrder.SongSortOrder.COMPOSER -> dataSet[position].composer
SortOrder.SongSortOrder.SONG_ALBUM_ARTIST -> dataSet[position].albumArtist
else -> {
return ""
}
}
return MusicUtil.getSectionName(sectionName)
}
open inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
protected open var songMenuRes = SongMenuHelper.MENU_RES
protected open val song: Song
get() = dataSet[layoutPosition]
init {
menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) {
override val song: Song
get() = this@ViewHolder.song
override val menuRes: Int
get() = songMenuRes
override fun onMenuItemClick(item: MenuItem): Boolean {
return onSongMenuItemClick(item) || super.onMenuItemClick(item)
}
})
}
protected open fun onSongMenuItemClick(item: MenuItem): Boolean {
if (image != null && image!!.isVisible) {
when (item.itemId) {
R.id.action_go_to_album -> {
activity.findNavController(R.id.fragment_container)
.navigate(
R.id.albumDetailsFragment,
bundleOf(EXTRA_ALBUM_ID to song.albumId)
)
return true
}
}
}
return false
}
override fun onClick(v: View?) {
if (isInQuickSelectMode) {
toggleChecked(layoutPosition)
} else {
MusicPlayerRemote.openQueue(dataSet, layoutPosition, true)
}
}
override fun onLongClick(v: View?): Boolean {
return toggleChecked(layoutPosition)
}
}
companion object {
val TAG: String = SongAdapter::class.java.simpleName
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts
import android.content.Context
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.util.TypedValue
import androidx.annotation.RequiresApi
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.util.PreferenceUtil
@RequiresApi(Build.VERSION_CODES.N_MR1)
object AppShortcutIconGenerator {
fun generateThemedIcon(context: Context, iconId: Int): Icon {
return if (PreferenceUtil.isColoredAppShortcuts) {
generateUserThemedIcon(context, iconId)
} else {
generateDefaultThemedIcon(context, iconId)
}
}
private fun generateDefaultThemedIcon(context: Context, iconId: Int): Icon {
// Return an Icon of iconId with default colors
return generateThemedIcon(
context,
iconId,
context.getColor(R.color.app_shortcut_default_foreground),
context.getColor(R.color.app_shortcut_default_background)
)
}
private fun generateUserThemedIcon(context: Context, iconId: Int): Icon {
// Get background color from context's theme
val typedColorBackground = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorBackground, typedColorBackground, true)
// Return an Icon of iconId with those colors
return generateThemedIcon(
context, iconId, ThemeStore.accentColor(context), typedColorBackground.data
)
}
private fun generateThemedIcon(
context: Context,
iconId: Int,
foregroundColor: Int,
backgroundColor: Int,
): Icon {
// Get and tint foreground and background drawables
val vectorDrawable = context.getTintedDrawable(iconId, foregroundColor)
val backgroundDrawable =
context.getTintedDrawable(R.drawable.ic_app_shortcut_background, backgroundColor)
// Squash the two drawables together
val layerDrawable = LayerDrawable(arrayOf(backgroundDrawable, vectorDrawable))
// Return as an Icon
return Icon.createWithBitmap(layerDrawable.toBitmap())
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType
import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType
import code.name.monkey.retromusic.appshortcuts.shortcuttype.TopTracksShortcutType
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.model.Playlist
import code.name.monkey.retromusic.model.smartplaylist.LastAddedPlaylist
import code.name.monkey.retromusic.model.smartplaylist.ShuffleAllPlaylist
import code.name.monkey.retromusic.model.smartplaylist.TopTracksPlaylist
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PLAY_PLAYLIST
import code.name.monkey.retromusic.service.MusicService.Companion.INTENT_EXTRA_PLAYLIST
import code.name.monkey.retromusic.service.MusicService.Companion.INTENT_EXTRA_SHUFFLE_MODE
import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_NONE
import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_SHUFFLE
class AppShortcutLauncherActivity : Activity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (extraNotNull(KEY_SHORTCUT_TYPE, SHORTCUT_TYPE_NONE).value) {
SHORTCUT_TYPE_SHUFFLE_ALL -> {
startServiceWithPlaylist(
SHUFFLE_MODE_SHUFFLE, ShuffleAllPlaylist()
)
DynamicShortcutManager.reportShortcutUsed(this, ShuffleAllShortcutType.id)
}
SHORTCUT_TYPE_TOP_TRACKS -> {
startServiceWithPlaylist(
SHUFFLE_MODE_NONE, TopTracksPlaylist()
)
DynamicShortcutManager.reportShortcutUsed(this, TopTracksShortcutType.id)
}
SHORTCUT_TYPE_LAST_ADDED -> {
startServiceWithPlaylist(
SHUFFLE_MODE_NONE, LastAddedPlaylist()
)
DynamicShortcutManager.reportShortcutUsed(this, LastAddedShortcutType.id)
}
}
finish()
}
private fun startServiceWithPlaylist(shuffleMode: Int, playlist: Playlist) {
val intent = Intent(this, MusicService::class.java)
intent.action = ACTION_PLAY_PLAYLIST
val bundle = bundleOf(
INTENT_EXTRA_PLAYLIST to playlist,
INTENT_EXTRA_SHUFFLE_MODE to shuffleMode
)
intent.putExtras(bundle)
startService(intent)
}
companion object {
const val KEY_SHORTCUT_TYPE = "io.github.muntashirakon.Music.appshortcuts.ShortcutType"
const val SHORTCUT_TYPE_SHUFFLE_ALL = 0L
const val SHORTCUT_TYPE_TOP_TRACKS = 1L
const val SHORTCUT_TYPE_LAST_ADDED = 2L
const val SHORTCUT_TYPE_NONE = 4L
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.core.content.getSystemService
import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType
import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType
import code.name.monkey.retromusic.appshortcuts.shortcuttype.TopTracksShortcutType
@TargetApi(Build.VERSION_CODES.N_MR1)
class DynamicShortcutManager(private val context: Context) {
private val shortcutManager: ShortcutManager? =
this.context.getSystemService()
private val defaultShortcuts: List<ShortcutInfo>
get() = listOf(
ShuffleAllShortcutType(context).shortcutInfo,
TopTracksShortcutType(context).shortcutInfo,
LastAddedShortcutType(context).shortcutInfo
)
fun initDynamicShortcuts() {
// if (shortcutManager.dynamicShortcuts.size == 0) {
shortcutManager?.dynamicShortcuts = defaultShortcuts
// }
}
fun updateDynamicShortcuts() {
shortcutManager?.updateShortcuts(defaultShortcuts)
}
companion object {
fun createShortcut(
context: Context,
id: String,
shortLabel: String,
longLabel: String,
icon: Icon,
intent: Intent
): ShortcutInfo {
return ShortcutInfo.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(icon)
.setIntent(intent)
.build()
}
fun reportShortcutUsed(context: Context, shortcutId: String) {
context.getSystemService<ShortcutManager>()?.reportShortcutUsed(shortcutId)
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts.shortcuttype
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.Build
import androidx.core.os.bundleOf
import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
@TargetApi(Build.VERSION_CODES.N_MR1)
abstract class BaseShortcutType(internal var context: Context) {
internal abstract val shortcutInfo: ShortcutInfo
/**
* Creates an Intent that will launch MainActivtiy and immediately play {@param songs} in either shuffle or normal mode
*
* @param shortcutType Describes the type of shortcut to create (ShuffleAll, TopTracks, custom playlist, etc.)
* @return
*/
internal fun getPlaySongsIntent(shortcutType: Long): Intent {
val intent = Intent(context, AppShortcutLauncherActivity::class.java)
intent.action = Intent.ACTION_VIEW
val b = bundleOf(AppShortcutLauncherActivity.KEY_SHORTCUT_TYPE to shortcutType)
intent.putExtras(b)
return intent
}
companion object {
internal const val ID_PREFIX = "io.github.muntashirakon.Music.appshortcuts.id."
val id: String
get() = ID_PREFIX + "invalid"
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts.shortcuttype
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.ShortcutInfo
import android.os.Build
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
@TargetApi(Build.VERSION_CODES.N_MR1)
class LastAddedShortcutType(context: Context) : BaseShortcutType(context) {
override val shortcutInfo: ShortcutInfo
get() = ShortcutInfo.Builder(
context,
id
).setShortLabel(context.getString(R.string.app_shortcut_last_added_short)).setLongLabel(
context.getString(R.string.app_shortcut_last_added_long)
).setIcon(
AppShortcutIconGenerator.generateThemedIcon(
context,
R.drawable.ic_app_shortcut_last_added
)
).setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_LAST_ADDED))
.build()
companion object {
val id: String
get() = ID_PREFIX + "last_added"
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appshortcuts.shortcuttype
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.ShortcutInfo
import android.os.Build
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
@TargetApi(Build.VERSION_CODES.N_MR1)
class ShuffleAllShortcutType(context: Context) : BaseShortcutType(context) {
override val shortcutInfo: ShortcutInfo
get() = ShortcutInfo.Builder(context, id)
.setShortLabel(context.getString(R.string.app_shortcut_shuffle_all_short))
.setLongLabel(context.getString(R.string.app_shortcut_shuffle_all_long))
.setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_shuffle_all))
.setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_SHUFFLE_ALL))
.build()
companion object {
val id: String
get() = ID_PREFIX + "shuffle_all"
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.appshortcuts.shortcuttype
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.ShortcutInfo
import android.os.Build
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
@TargetApi(Build.VERSION_CODES.N_MR1)
class TopTracksShortcutType(context: Context) : BaseShortcutType(context) {
override val shortcutInfo: ShortcutInfo
get() = ShortcutInfo.Builder(
context, id
).setShortLabel(context.getString(R.string.app_shortcut_top_tracks_short)).setLongLabel(
context.getString(R.string.app_shortcut_top_tracks_long)
).setIcon(
AppShortcutIconGenerator.generateThemedIcon(
context, R.drawable.ic_app_shortcut_top_tracks
)
).setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_TOP_TRACKS))
.build()
companion object {
val id: String
get() = ID_PREFIX + "top_tracks"
}
}

View file

@ -0,0 +1,237 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
class AppWidgetBig : BaseAppWidget() {
private var target: Target<Bitmap>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(
context.packageName, R.layout.app_widget_big
)
appWidgetView.setViewVisibility(
R.id.media_titles,
View.INVISIBLE
)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
appWidgetView.setImageViewBitmap(
R.id.button_next, context.getTintedDrawable(
R.drawable.ic_skip_next,
MaterialValueHelper.getPrimaryTextColor(context, false)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(
R.drawable.ic_skip_previous,
MaterialValueHelper.getPrimaryTextColor(context, false)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow_white_32dp,
MaterialValueHelper.getPrimaryTextColor(context, false)
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(
service.packageName, R.layout.app_widget_big
)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(
R.id.media_titles,
View.INVISIBLE
)
} else {
appWidgetView.setViewVisibility(
R.id.media_titles,
View.VISIBLE
)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(
R.id.text,
getSongArtistAndAlbum(song)
)
}
val primaryColor = MaterialValueHelper.getPrimaryTextColor(service, false)
// Set correct drawable for pause state
val playPauseRes =
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes,
primaryColor
).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(
R.drawable.ic_skip_next,
primaryColor
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(
R.drawable.ic_skip_previous,
primaryColor
).toBitmap()
)
// Link actions buttons to intents
linkButtons(service, appWidgetView)
// Load the album cover async and push the update on completion
val p = RetroUtil.getScreenSize(service)
val widgetImageSize = p.x.coerceAtMost(p.y)
val appContext = service.applicationContext
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(appContext)
.asBitmap()
//.checkIgnoreMediaStore()
.load(RetroGlideExtension.getSongModel(song))
.into(object : CustomTarget<Bitmap>(widgetImageSize, widgetImageSize) {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?,
) {
update(resource)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null)
}
override fun onLoadCleared(placeholder: Drawable?) {}
private fun update(bitmap: Bitmap?) {
if (bitmap == null) {
appWidgetView.setImageViewResource(
R.id.image,
R.drawable.default_audio_art
)
} else {
appWidgetView.setImageViewBitmap(R.id.image, bitmap)
}
pushUpdate(appContext, appWidgetIds, appWidgetView)
}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent =
PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.clickable_area, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
companion object {
const val NAME: String = "app_widget_big"
private var mInstance: AppWidgetBig? = null
val instance: AppWidgetBig
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetBig()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,254 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.PreferenceUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
class AppWidgetCard : BaseAppWidget() {
private var target: Target<BitmapPaletteWrapper>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_card)
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
appWidgetView.setImageViewBitmap(
R.id.button_next,
context.getTintedDrawable(
R.drawable.ic_skip_next,
secondaryColor
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(
R.drawable.ic_skip_previous,
secondaryColor
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow_white_32dp,
secondaryColor
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_card)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
} else {
appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
}
// Set correct drawable for pause state
val playPauseRes =
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(
R.drawable.ic_skip_next,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(
R.drawable.ic_skip_previous,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
// Link actions buttons to intents
linkButtons(service, appWidgetView)
if (imageSize == 0) {
imageSize =
service.resources.getDimensionPixelSize(R.dimen.app_widget_card_image_size)
}
if (cardRadius == 0f) {
cardRadius =
service.resources.getDimension(R.dimen.app_widget_card_radius)
}
// Load the album cover async and push the update on completion
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(imageSize, imageSize) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val palette = resource.palette
update(
resource.bitmap, palette.getVibrantColor(
palette.getMutedColor(
MaterialValueHelper.getSecondaryTextColor(
service, true
)
)
)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
}
override fun onLoadCleared(placeholder: Drawable?) {}
private fun update(bitmap: Bitmap?, color: Int) {
// Set correct drawable for pause state
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(playPauseRes, color).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
)
val image = getAlbumArtDrawable(service, bitmap)
val roundedBitmap = createRoundedBitmap(
image, imageSize, imageSize, cardRadius, 0F, cardRadius, 0F
)
appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
pushUpdate(service, appWidgetIds, appWidgetView)
}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent =
PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
companion object {
const val NAME = "app_widget_card"
private var mInstance: AppWidgetCard? = null
private var imageSize = 0
private var cardRadius = 0f
val instance: AppWidgetCard
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetCard()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,215 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.service.MusicService.Companion.TOGGLE_FAVORITE
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
class AppWidgetCircle : BaseAppWidget() {
private var target: Target<BitmapPaletteWrapper>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_circle)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow,
secondaryColor
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_circle)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set correct drawable for pause state
val playPauseRes =
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
val isFavorite = runBlocking(Dispatchers.IO) {
return@runBlocking MusicUtil.repository.isSongFavorite(song.id)
}
val favoriteRes =
if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border
appWidgetView.setImageViewBitmap(
R.id.button_toggle_favorite,
service.getTintedDrawable(
favoriteRes,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
// Link actions buttons to intents
linkButtons(service, appWidgetView)
if (imageSize == 0) {
val p = RetroUtil.getScreenSize(service)
imageSize = p.x.coerceAtMost(p.y)
}
// Load the album cover async and push the update on completion
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.apply(RequestOptions.circleCropTransform())
.into(object : CustomTarget<BitmapPaletteWrapper>(imageSize, imageSize) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val palette = resource.palette
update(
resource.bitmap, palette.getVibrantColor(
palette.getMutedColor(
MaterialValueHelper.getSecondaryTextColor(
service, true
)
)
)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
}
private fun update(bitmap: Bitmap?, color: Int) {
// Set correct drawable for pause state
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes, color
).toBitmap()
)
// Set favorite button drawables
appWidgetView.setImageViewBitmap(
R.id.button_toggle_favorite,
service.getTintedDrawable(
favoriteRes, color
).toBitmap()
)
if (bitmap != null) {
appWidgetView.setImageViewBitmap(R.id.image, bitmap)
}
pushUpdate(service, appWidgetIds, appWidgetView)
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent =
PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
// Favorite track
pendingIntent = buildPendingIntent(context, TOGGLE_FAVORITE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_favorite, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
}
companion object {
const val NAME = "app_widget_circle"
private var mInstance: AppWidgetCircle? = null
private var imageSize = 0
val instance: AppWidgetCircle
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetCircle()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,250 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.PreferenceUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
class AppWidgetClassic : BaseAppWidget() {
private var target: Target<BitmapPaletteWrapper>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_classic)
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
appWidgetView.setImageViewBitmap(
R.id.button_next,
context.getTintedDrawable(
R.drawable.ic_skip_next,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(
R.drawable.ic_skip_previous,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow_white_32dp,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_classic)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
} else {
appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
}
// Link actions buttons to intents
linkButtons(service, appWidgetView)
if (imageSize == 0) {
imageSize =
service.resources.getDimensionPixelSize(R.dimen.app_widget_classic_image_size)
}
if (cardRadius == 0f) {
cardRadius = service.resources.getDimension(R.dimen.app_widget_card_radius)
}
// Load the album cover async and push the update on completion
val appContext = service.applicationContext
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
//.checkIgnoreMediaStore()
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(imageSize, imageSize) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val palette = resource.palette
update(
resource.bitmap,
palette.getVibrantColor(
palette.getMutedColor(
MaterialValueHelper.getSecondaryTextColor(
service,
true
)
)
)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, Color.WHITE)
}
override fun onLoadCleared(placeholder: Drawable?) {}
private fun update(bitmap: Bitmap?, color: Int) {
// Set correct drawable for pause state
val playPauseRes =
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes,
color
).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(
R.drawable.ic_skip_next,
color
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(
R.drawable.ic_skip_previous,
color
).toBitmap()
)
val image = getAlbumArtDrawable(service, bitmap)
val roundedBitmap =
createRoundedBitmap(
image,
imageSize,
imageSize,
cardRadius,
0F,
cardRadius,
0F
)
appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
pushUpdate(appContext, appWidgetIds, appWidgetView)
}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent = PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
companion object {
const val NAME = "app_widget_classic"
private var mInstance: AppWidgetClassic? = null
private var imageSize = 0
private var cardRadius = 0f
val instance: AppWidgetClassic
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetClassic()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,261 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.DensityUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
class AppWidgetMD3 : BaseAppWidget() {
private var target: Target<BitmapPaletteWrapper>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_md3)
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
appWidgetView.setImageViewBitmap(
R.id.button_next,
context.getTintedDrawable(
R.drawable.ic_skip_next,
secondaryColor
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(
R.drawable.ic_skip_previous,
secondaryColor
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow_white_32dp,
secondaryColor
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_md3)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
} else {
appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
}
// Set correct drawable for pause state
val playPauseRes =
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(
playPauseRes,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(
R.drawable.ic_skip_next,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(
R.drawable.ic_skip_previous,
MaterialValueHelper.getSecondaryTextColor(service, true)
).toBitmap()
)
// Link actions buttons to intents
linkButtons(service, appWidgetView)
if (imageSize == 0) {
imageSize =
service.resources.getDimensionPixelSize(R.dimen.app_widget_card_image_size)
}
if (cardRadius == 0f) {
cardRadius =
DensityUtil.dip2px(service, 8F).toFloat()
}
// Load the album cover async and push the update on completion
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(imageSize, imageSize) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val palette = resource.palette
update(
resource.bitmap, palette.getVibrantColor(
palette.getMutedColor(
MaterialValueHelper.getSecondaryTextColor(
service, true
)
)
)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
}
override fun onLoadCleared(placeholder: Drawable?) {}
private fun update(bitmap: Bitmap?, color: Int) {
// Set correct drawable for pause state
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(playPauseRes, color).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
)
val image = getAlbumArtDrawable(service, bitmap)
val roundedBitmap = createRoundedBitmap(
image,
imageSize,
imageSize,
cardRadius,
cardRadius,
cardRadius,
cardRadius
)
appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
pushUpdate(service, appWidgetIds, appWidgetView)
}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent =
PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
companion object {
const val NAME = "app_widget_md3"
private var mInstance: AppWidgetMD3? = null
private var imageSize = 0
private var cardRadius = 0F
val instance: AppWidgetMD3
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetMD3()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,238 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.PreferenceUtil
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
class AppWidgetSmall : BaseAppWidget() {
private var target: Target<BitmapPaletteWrapper>? = null // for cancellation
/**
* Initialize given widgets to default state, where we launch Music on default click and hide
* actions if service not running.
*/
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_small)
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
appWidgetView.setImageViewBitmap(
R.id.button_next,
context.getTintedDrawable(
R.drawable.ic_skip_next,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(
R.drawable.ic_skip_previous,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(
R.drawable.ic_play_arrow_white_32dp,
MaterialValueHelper.getSecondaryTextColor(context, true)
).toBitmap()
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Update all active widget instances by pushing changes
*/
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_small)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
} else {
if (song.title.isEmpty() || song.artistName.isEmpty()) {
appWidgetView.setTextViewText(R.id.text_separator, "")
} else {
appWidgetView.setTextViewText(R.id.text_separator, "")
}
appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(R.id.text, song.artistName)
}
// Link actions buttons to intents
linkButtons(service, appWidgetView)
if (imageSize == 0) {
imageSize = service.resources.getDimensionPixelSize(R.dimen.app_widget_small_image_size)
}
if (cardRadius == 0f) {
cardRadius = service.resources.getDimension(R.dimen.app_widget_card_radius)
}
// Load the album cover async and push the update on completion
val appContext = service.applicationContext
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
//.checkIgnoreMediaStore()
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(imageSize, imageSize) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val palette = resource.palette
update(
resource.bitmap, palette.getVibrantColor(
palette.getMutedColor(
MaterialValueHelper.getSecondaryTextColor(
service, true
)
)
)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
}
override fun onLoadCleared(placeholder: Drawable?) {
update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
}
private fun update(bitmap: Bitmap?, color: Int) {
// Set correct drawable for pause state
val playPauseRes = if (isPlaying) R.drawable.ic_pause
else R.drawable.ic_play_arrow_white_32dp
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(playPauseRes, color).toBitmap()
)
// Set prev/next button drawables
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
)
val image = getAlbumArtDrawable(service, bitmap)
val roundedBitmap = createRoundedBitmap(
image, imageSize, imageSize, cardRadius, 0f, 0f, 0f
)
appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
pushUpdate(appContext, appWidgetIds, appWidgetView)
}
})
}
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent =
PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
companion object {
const val NAME: String = "app_widget_small"
private var mInstance: AppWidgetSmall? = null
private var imageSize = 0
private var cardRadius = 0f
val instance: AppWidgetSmall
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetSmall()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,170 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
import code.name.monkey.retromusic.extensions.getTintedDrawable
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import code.name.monkey.retromusic.util.PreferenceUtil
class AppWidgetText : BaseAppWidget() {
override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_text)
appWidgetView.setImageViewBitmap(
R.id.button_next,
context.getTintedDrawable(R.drawable.ic_skip_next, ContextCompat.getColor(
context, R.color.md_white_1000
)).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
context.getTintedDrawable(R.drawable.ic_skip_previous, ContextCompat.getColor(
context, R.color.md_white_1000
)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
context.getTintedDrawable(R.drawable.ic_play_arrow_white_32dp, ContextCompat.getColor(
context, R.color.md_white_1000
)
).toBitmap()
)
appWidgetView.setTextColor(
R.id.title, ContextCompat.getColor(context, R.color.md_white_1000)
)
appWidgetView.setTextColor(
R.id.text, ContextCompat.getColor(context, R.color.md_white_1000)
)
linkButtons(context, appWidgetView)
pushUpdate(context, appWidgetIds, appWidgetView)
}
/**
* Link up various button actions using [PendingIntent].
*/
private fun linkButtons(context: Context, views: RemoteViews) {
val action = Intent(context, MainActivity::class.java)
.putExtra(
MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel
)
val serviceName = ComponentName(context, MusicService::class.java)
// Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
var pendingIntent = PendingIntent.getActivity(
context, 0, action, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
views.setOnClickPendingIntent(R.id.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
}
override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_text)
val isPlaying = service.isPlaying
val song = service.currentSong
// Set the titles and artwork
if (song.title.isEmpty() && song.artistName.isEmpty()) {
appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
} else {
appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
appWidgetView.setTextViewText(R.id.title, song.title)
appWidgetView.setTextViewText(R.id.text, song.artistName)
}
// Link actions buttons to intents
linkButtons(service, appWidgetView)
// Set correct drawable for pause state
val playPauseRes = if (isPlaying) R.drawable.ic_pause
else R.drawable.ic_play_arrow_white_32dp
appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause,
service.getTintedDrawable(playPauseRes, ContextCompat.getColor(
service, R.color.md_white_1000)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_next,
service.getTintedDrawable(
R.drawable.ic_skip_next,
ContextCompat.getColor(
service,
R.color.md_white_1000
)
).toBitmap()
)
appWidgetView.setImageViewBitmap(
R.id.button_prev,
service.getTintedDrawable(
R.drawable.ic_skip_previous,
ContextCompat.getColor(
service, R.color.md_white_1000
)
).toBitmap()
)
pushUpdate(service.applicationContext, appWidgetIds, appWidgetView)
}
companion object {
const val NAME: String = "app_widget_text"
private var mInstance: AppWidgetText? = null
val instance: AppWidgetText
@Synchronized get() {
if (mInstance == null) {
mInstance = AppWidgetText()
}
return mInstance!!
}
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets
import android.appwidget.AppWidgetManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import code.name.monkey.retromusic.service.MusicService
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val widgetManager = AppWidgetManager.getInstance(context)
// Start music service if there are any existing widgets
if (widgetManager.getAppWidgetIds(
ComponentName(
context, AppWidgetBig::class.java
)
).isNotEmpty() || widgetManager.getAppWidgetIds(
ComponentName(
context, AppWidgetClassic::class.java
)
).isNotEmpty() || widgetManager.getAppWidgetIds(
ComponentName(
context, AppWidgetSmall::class.java
)
).isNotEmpty() || widgetManager.getAppWidgetIds(
ComponentName(
context, AppWidgetCard::class.java
)
).isNotEmpty()
) {
val serviceIntent = Intent(context, MusicService::class.java)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // not allowed on Oreo
context.startService(serviceIntent)
}
}
}
}

View file

@ -0,0 +1,192 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.appwidgets.base
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.APP_WIDGET_UPDATE
import code.name.monkey.retromusic.service.MusicService.Companion.EXTRA_APP_WIDGET_NAME
import code.name.monkey.retromusic.service.MusicService.Companion.FAVORITE_STATE_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.META_CHANGED
import code.name.monkey.retromusic.service.MusicService.Companion.PLAY_STATE_CHANGED
abstract class BaseAppWidget : AppWidgetProvider() {
/**
* {@inheritDoc}
*/
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
defaultAppWidget(context, appWidgetIds)
val updateIntent = Intent(APP_WIDGET_UPDATE)
updateIntent.putExtra(EXTRA_APP_WIDGET_NAME, NAME)
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
context.sendBroadcast(updateIntent)
}
/**
* Handle a change notification coming over from [MusicService]
*/
fun notifyChange(service: MusicService, what: String) {
if (hasInstances(service)) {
if (META_CHANGED == what || PLAY_STATE_CHANGED == what || FAVORITE_STATE_CHANGED == what) {
performUpdate(service, null)
}
}
}
protected fun pushUpdate(
context: Context,
appWidgetIds: IntArray?,
views: RemoteViews
) {
val appWidgetManager = AppWidgetManager.getInstance(context)
if (appWidgetIds != null) {
appWidgetManager.updateAppWidget(appWidgetIds, views)
} else {
appWidgetManager.updateAppWidget(ComponentName(context, javaClass), views)
}
}
/**
* Check against [AppWidgetManager] if there are any instances of this widget.
*/
private fun hasInstances(context: Context): Boolean {
val appWidgetManager = AppWidgetManager.getInstance(context)
val mAppWidgetIds = appWidgetManager.getAppWidgetIds(
ComponentName(
context, javaClass
)
)
return mAppWidgetIds.isNotEmpty()
}
protected fun buildPendingIntent(
context: Context,
action: String,
serviceName: ComponentName
): PendingIntent {
val intent = Intent(action)
intent.component = serviceName
return if (VersionUtils.hasOreo()) {
PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getService(
context, 0, intent, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
}
}
protected abstract fun defaultAppWidget(context: Context, appWidgetIds: IntArray)
abstract fun performUpdate(service: MusicService, appWidgetIds: IntArray?)
protected fun getAlbumArtDrawable(context: Context, bitmap: Bitmap?): Drawable {
return if (bitmap == null) {
ContextCompat.getDrawable(context, R.drawable.default_audio_art)!!
} else {
BitmapDrawable(context.resources, bitmap)
}
}
protected fun getSongArtistAndAlbum(song: Song): String {
val builder = StringBuilder()
builder.append(song.artistName)
if (song.artistName.isNotEmpty() && song.albumName.isNotEmpty()) {
builder.append("")
}
builder.append(song.albumName)
return builder.toString()
}
companion object {
const val NAME: String = "app_widget"
fun createRoundedBitmap(
drawable: Drawable?,
width: Int,
height: Int,
tl: Float,
tr: Float,
bl: Float,
br: Float
): Bitmap? {
if (drawable == null) {
return null
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val c = Canvas(bitmap)
drawable.setBounds(0, 0, width, height)
drawable.draw(c)
val rounded = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(rounded)
val paint = Paint()
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.isAntiAlias = true
canvas.drawPath(
composeRoundedRectPath(
RectF(0f, 0f, width.toFloat(), height.toFloat()), tl, tr, bl, br
), paint
)
return rounded
}
protected fun composeRoundedRectPath(
rect: RectF,
tl: Float,
tr: Float,
bl: Float,
br: Float
): Path {
val path = Path()
path.moveTo(rect.left + tl, rect.top)
path.lineTo(rect.right - tr, rect.top)
path.quadTo(rect.right, rect.top, rect.right, rect.top + tr)
path.lineTo(rect.right, rect.bottom - br)
path.quadTo(rect.right, rect.bottom, rect.right - br, rect.bottom)
path.lineTo(rect.left + bl, rect.bottom)
path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - bl)
path.lineTo(rect.left, rect.top + tl)
path.quadTo(rect.left, rect.top, rect.left + tl, rect.top)
path.close()
return path
}
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.auto;
import androidx.annotation.NonNull;
/**
* Created by Beesham Sarendranauth (Beesham)
*/
public class AutoMediaIDHelper {
// Media IDs used on browseable items of MediaBrowser
public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY_ROOT__";
public static final String MEDIA_ID_ROOT = "__ROOT__";
public static final String MEDIA_ID_MUSICS_BY_SEARCH = "__BY_SEARCH__"; // TODO
public static final String MEDIA_ID_MUSICS_BY_HISTORY = "__BY_HISTORY__";
public static final String MEDIA_ID_MUSICS_BY_TOP_TRACKS = "__BY_TOP_TRACKS__";
public static final String MEDIA_ID_MUSICS_BY_SUGGESTIONS = "__BY_SUGGESTIONS__";
public static final String MEDIA_ID_MUSICS_BY_PLAYLIST = "__BY_PLAYLIST__";
public static final String MEDIA_ID_MUSICS_BY_ALBUM = "__BY_ALBUM__";
public static final String MEDIA_ID_MUSICS_BY_ARTIST = "__BY_ARTIST__";
public static final String MEDIA_ID_MUSICS_BY_ALBUM_ARTIST = "__BY_ALBUM_ARTIST__";
public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__";
public static final String MEDIA_ID_MUSICS_BY_SHUFFLE = "__BY_SHUFFLE__";
public static final String MEDIA_ID_MUSICS_BY_QUEUE = "__BY_QUEUE__";
public static final String RECENT_ROOT = "__RECENT__";
private static final String CATEGORY_SEPARATOR = "__/__";
private static final String LEAF_SEPARATOR = "__|__";
/**
* Create a String value that represents a playable or a browsable media.
* <p/>
* Encode the media browseable categories, if any, and the unique music ID, if any,
* into a single String mediaID.
* <p/>
* MediaIDs are of the form <categoryType>__/__<categoryValue>__|__<musicUniqueId>, to make it
* easy to find the category (like genre) that a music was selected from, so we
* can correctly build the playing queue. This is specially useful when
* one music can appear in more than one list, like "by genre -> genre_1"
* and "by artist -> artist_1".
*
* @param mediaID Unique ID for playable items, or null for browseable items.
* @param categories Hierarchy of categories representing this item's browsing parents.
* @return A hierarchy-aware media ID.
*/
public static String createMediaID(String mediaID, String... categories) {
StringBuilder sb = new StringBuilder();
if (categories != null) {
for (int i = 0; i < categories.length; i++) {
if (!isValidCategory(categories[i])) {
throw new IllegalArgumentException("Invalid category: " + categories[i]);
}
sb.append(categories[i]);
if (i < categories.length - 1) {
sb.append(CATEGORY_SEPARATOR);
}
}
}
if (mediaID != null) {
sb.append(LEAF_SEPARATOR).append(mediaID);
}
return sb.toString();
}
public static String extractCategory(@NonNull String mediaID) {
int pos = mediaID.indexOf(LEAF_SEPARATOR);
if (pos >= 0) {
return mediaID.substring(0, pos);
}
return mediaID;
}
public static String extractMusicID(@NonNull String mediaID) {
int pos = mediaID.indexOf(LEAF_SEPARATOR);
if (pos >= 0) {
return mediaID.substring(pos + LEAF_SEPARATOR.length());
}
return null;
}
public static boolean isBrowseable(@NonNull String mediaID) {
return !mediaID.contains(LEAF_SEPARATOR);
}
private static boolean isValidCategory(String category) {
return category == null ||
(!category.contains(CATEGORY_SEPARATOR) && !category.contains(LEAF_SEPARATOR));
}
}

View file

@ -0,0 +1,283 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*/
package code.name.monkey.retromusic.auto
import android.content.Context
import android.content.res.Resources
import android.support.v4.media.MediaBrowserCompat
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.model.CategoryInfo
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.repository.*
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil
import java.lang.ref.WeakReference
/**
* Created by Beesham Sarendranauth (Beesham)
*/
class AutoMusicProvider(
val mContext: Context,
private val songsRepository: SongRepository,
private val albumsRepository: AlbumRepository,
private val artistsRepository: ArtistRepository,
private val genresRepository: GenreRepository,
private val playlistsRepository: PlaylistRepository,
private val topPlayedRepository: TopPlayedRepository
) {
private var mMusicService: WeakReference<MusicService>? = null
fun setMusicService(service: MusicService) {
mMusicService = WeakReference(service)
}
fun getChildren(mediaId: String?, resources: Resources): List<MediaBrowserCompat.MediaItem> {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
when (mediaId) {
AutoMediaIDHelper.MEDIA_ID_ROOT -> {
mediaItems.addAll(getRootChildren(resources))
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST -> for (playlist in playlistsRepository.playlists()) {
mediaItems.add(
AutoMediaItem.with(mContext)
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST, playlist.id)
.icon(R.drawable.ic_playlist_play)
.title(playlist.name)
.subTitle(playlist.getInfoString(mContext))
.asPlayable()
.build()
)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM -> for (album in albumsRepository.albums()) {
mediaItems.add(
AutoMediaItem.with(mContext)
.path(mediaId, album.id)
.title(album.title)
.subTitle(album.albumArtist ?: album.artistName)
.icon(MusicUtil.getMediaStoreAlbumCoverUri(album.id))
.asPlayable()
.build()
)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST -> for (artist in artistsRepository.artists()) {
mediaItems.add(
AutoMediaItem.with(mContext)
.asPlayable()
.path(mediaId, artist.id)
.title(artist.name)
.build()
)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST -> for (artist in artistsRepository.albumArtists()) {
mediaItems.add(
AutoMediaItem.with(mContext)
.asPlayable()
// we just pass album id here as we don't have album artist id's
.path(mediaId, artist.safeGetFirstAlbum().id)
.title(artist.name)
.build()
)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE -> for (genre in genresRepository.genres()) {
mediaItems.add(
AutoMediaItem.with(mContext)
.asPlayable()
.path(mediaId, genre.id)
.title(genre.name)
.build()
)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE ->
mMusicService?.get()?.playingQueue
?.let {
for (song in it) {
mediaItems.add(
AutoMediaItem.with(mContext)
.asPlayable()
.path(mediaId, song.id)
.title(song.title)
.subTitle(song.artistName)
.icon(MusicUtil.getMediaStoreAlbumCoverUri(song.albumId))
.build()
)
}
}
else -> {
getPlaylistChildren(mediaId, mediaItems)
}
}
return mediaItems
}
private fun getPlaylistChildren(
mediaId: String?,
mediaItems: MutableList<MediaBrowserCompat.MediaItem>
) {
val songs = when (mediaId) {
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS -> {
topPlayedRepository.topTracks()
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY -> {
topPlayedRepository.recentlyPlayedTracks()
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS -> {
topPlayedRepository.notRecentlyPlayedTracks().take(8)
}
else -> {
emptyList()
}
}
songs.forEach { song ->
mediaItems.add(
getPlayableSong(mediaId, song)
)
}
}
private fun getRootChildren(resources: Resources): List<MediaBrowserCompat.MediaItem> {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
val libraryCategories = PreferenceUtil.libraryCategory
libraryCategories.forEach {
if (it.visible) {
when (it.category) {
CategoryInfo.Category.Albums -> {
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM)
.gridLayout(true)
.icon(R.drawable.ic_album)
.title(resources.getString(R.string.albums)).build()
)
}
CategoryInfo.Category.Artists -> {
if (PreferenceUtil.albumArtistsOnly) {
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST)
.icon(R.drawable.ic_album_artist)
.title(resources.getString(R.string.album_artist)).build()
)
} else {
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST)
.icon(R.drawable.ic_artist)
.title(resources.getString(R.string.artists)).build()
)
}
}
CategoryInfo.Category.Genres -> {
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE)
.icon(R.drawable.ic_guitar)
.title(resources.getString(R.string.genres)).build()
)
}
CategoryInfo.Category.Playlists -> {
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST)
.icon(R.drawable.ic_playlist_play)
.title(resources.getString(R.string.playlists)).build()
)
}
else -> {
}
}
}
}
mediaItems.add(
AutoMediaItem.with(mContext)
.asPlayable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE)
.icon(R.drawable.ic_shuffle)
.title(resources.getString(R.string.action_shuffle_all))
.subTitle(MusicUtil.getPlaylistInfoString(mContext, songsRepository.songs()))
.build()
)
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE)
.icon(R.drawable.ic_queue_music)
.title(resources.getString(R.string.queue))
.subTitle(MusicUtil.getPlaylistInfoString(mContext, MusicPlayerRemote.playingQueue))
.asBrowsable().build()
)
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS)
.icon(R.drawable.ic_trending_up)
.title(resources.getString(R.string.my_top_tracks))
.subTitle(
MusicUtil.getPlaylistInfoString(
mContext,
topPlayedRepository.topTracks()
)
)
.asBrowsable().build()
)
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS)
.icon(R.drawable.ic_face)
.title(resources.getString(R.string.suggestion_songs))
.subTitle(
MusicUtil.getPlaylistInfoString(
mContext,
topPlayedRepository.notRecentlyPlayedTracks().takeIf {
it.size > 9
} ?: emptyList()
)
)
.asBrowsable().build()
)
mediaItems.add(
AutoMediaItem.with(mContext)
.asBrowsable()
.path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY)
.icon(R.drawable.ic_history)
.title(resources.getString(R.string.history))
.subTitle(
MusicUtil.getPlaylistInfoString(
mContext,
topPlayedRepository.recentlyPlayedTracks()
)
)
.asBrowsable().build()
)
return mediaItems
}
private fun getPlayableSong(mediaId: String?, song: Song): MediaBrowserCompat.MediaItem {
return AutoMediaItem.with(mContext)
.asPlayable()
.path(mediaId, song.id)
.title(song.title)
.subTitle(song.artistName)
.icon(MusicUtil.getMediaStoreAlbumCoverUri(song.albumId))
.build()
}
}

View file

@ -0,0 +1,100 @@
package code.name.monkey.retromusic.auto
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.os.bundleOf
internal object AutoMediaItem {
fun with(context: Context): Builder {
return Builder(context)
}
internal class Builder(private val mContext: Context) {
private var mBuilder: MediaDescriptionCompat.Builder?
private var mFlags = 0
fun path(fullPath: String): Builder {
mBuilder?.setMediaId(fullPath)
return this
}
fun path(path: String?, id: Long): Builder {
return path(AutoMediaIDHelper.createMediaID(id.toString(), path))
}
fun title(title: String): Builder {
mBuilder?.setTitle(title)
return this
}
fun subTitle(subTitle: String): Builder {
mBuilder?.setSubtitle(subTitle)
return this
}
fun icon(uri: Uri?): Builder {
mBuilder?.setIconUri(uri)
return this
}
fun icon(iconDrawableId: Int): Builder {
mBuilder?.setIconBitmap(
ResourcesCompat.getDrawable(
mContext.resources,
iconDrawableId,
mContext.theme
)?.toBitmap()
)
return this
}
fun gridLayout(isGrid: Boolean): Builder {
val hints = bundleOf(
CONTENT_STYLE_SUPPORTED to true,
CONTENT_STYLE_BROWSABLE_HINT to
if (isGrid) CONTENT_STYLE_GRID_ITEM_HINT_VALUE
else CONTENT_STYLE_LIST_ITEM_HINT_VALUE,
CONTENT_STYLE_PLAYABLE_HINT to
if (isGrid) CONTENT_STYLE_GRID_ITEM_HINT_VALUE
else CONTENT_STYLE_LIST_ITEM_HINT_VALUE
)
mBuilder?.setExtras(hints)
return this
}
fun asBrowsable(): Builder {
mFlags = mFlags or MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
return this
}
fun asPlayable(): Builder {
mFlags = mFlags or MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
return this
}
fun build(): MediaBrowserCompat.MediaItem {
val result = MediaBrowserCompat.MediaItem(mBuilder!!.build(), mFlags)
mBuilder = null
mFlags = 0
return result
}
init {
mBuilder = MediaDescriptionCompat.Builder()
}
companion object{
// Hints - see https://developer.android.com/training/cars/media#default-content-style
const val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"
const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"
const val CONTENT_STYLE_PLAYABLE_HINT = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"
const val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1
const val CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.*
@Dao
interface BlackListStoreDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBlacklistPath(blackListStoreEntity: BlackListStoreEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBlacklistPath(blackListStoreEntities: List<BlackListStoreEntity>)
@Delete
suspend fun deleteBlacklistPath(blackListStoreEntity: BlackListStoreEntity)
@Query("DELETE FROM BlackListStoreEntity")
suspend fun clearBlacklist()
@Query("SELECT * FROM BlackListStoreEntity")
fun blackListPaths(): List<BlackListStoreEntity>
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class BlackListStoreEntity(
@PrimaryKey
val path: String
)

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface HistoryDao {
companion object {
private const val HISTORY_LIMIT = 100
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSongInHistory(historyEntity: HistoryEntity)
@Query("DELETE FROM HistoryEntity WHERE id= :songId")
fun deleteSongInHistory(songId: Long)
@Query("SELECT * FROM HistoryEntity WHERE id = :songId LIMIT 1")
suspend fun isSongPresentInHistory(songId: Long): HistoryEntity?
@Update
suspend fun updateHistorySong(historyEntity: HistoryEntity)
@Query("SELECT * FROM HistoryEntity ORDER BY time_played DESC LIMIT $HISTORY_LIMIT")
fun historySongs(): List<HistoryEntity>
@Query("SELECT * FROM HistoryEntity ORDER BY time_played DESC LIMIT $HISTORY_LIMIT")
fun observableHistorySongs(): LiveData<List<HistoryEntity>>
@Query("DELETE FROM HistoryEntity")
suspend fun clearHistory()
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class HistoryEntity(
@PrimaryKey
val id: Long,
val title: String,
@ColumnInfo(name = "track_number")
val trackNumber: Int,
val year: Int,
val duration: Long,
val data: String,
@ColumnInfo(name = "date_modified")
val dateModified: Long,
@ColumnInfo(name = "album_id")
val albumId: Long,
@ColumnInfo(name = "album_name")
val albumName: String,
@ColumnInfo(name = "artist_id")
val artistId: Long,
@ColumnInfo(name = "artist_name")
val artistName: String,
val composer: String?,
@ColumnInfo(name = "album_artist")
val albumArtist: String?,
@ColumnInfo(name = "time_played")
val timePlayed: Long
)

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.*
@Dao
interface LyricsDao {
@Query("SELECT * FROM LyricsEntity WHERE songId =:songId LIMIT 1")
fun lyricsWithSongId(songId: Int): LyricsEntity?
@Insert
fun insertLyrics(lyricsEntity: LyricsEntity)
@Delete
fun deleteLyrics(lyricsEntity: LyricsEntity)
@Update
fun updateLyrics(lyricsEntity: LyricsEntity)
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class LyricsEntity(
@PrimaryKey val songId: Int,
val lyrics: String
)

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.*
@Dao
interface PlayCountDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertSongInPlayCount(playCountEntity: PlayCountEntity)
@Update
fun updateSongInPlayCount(playCountEntity: PlayCountEntity)
@Delete
fun deleteSongInPlayCount(playCountEntity: PlayCountEntity)
@Query("SELECT * FROM PlayCountEntity WHERE id =:songId")
fun checkSongExistInPlayCount(songId: Long): List<PlayCountEntity>
@Query("SELECT * FROM PlayCountEntity ORDER BY play_count DESC")
fun playCountSongs(): List<PlayCountEntity>
@Query("DELETE FROM SongEntity WHERE id =:songId")
fun deleteSong(songId: Long)
@Query("UPDATE PlayCountEntity SET play_count = play_count + 1 WHERE id = :id")
fun updateQuantity(id: Long)
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class PlayCountEntity(
@PrimaryKey
val id: Long,
val title: String,
@ColumnInfo(name = "track_number")
val trackNumber: Int,
val year: Int,
val duration: Long,
val data: String,
@ColumnInfo(name = "date_modified")
val dateModified: Long,
@ColumnInfo(name = "album_id")
val albumId: Long,
@ColumnInfo(name = "album_name")
val albumName: String,
@ColumnInfo(name = "artist_id")
val artistId: Long,
@ColumnInfo(name = "artist_name")
val artistName: String,
val composer: String?,
@ColumnInfo(name = "album_artist")
val albumArtist: String?,
@ColumnInfo(name = "time_played")
val timePlayed: Long,
@ColumnInfo(name = "play_count")
var playCount: Int
)

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface PlaylistDao {
@Insert
suspend fun createPlaylist(playlistEntity: PlaylistEntity): Long
@Query("UPDATE PlaylistEntity SET playlist_name = :name WHERE playlist_id = :playlistId")
suspend fun renamePlaylist(playlistId: Long, name: String)
@Query("SELECT * FROM PlaylistEntity WHERE playlist_name = :name")
fun playlist(name: String): List<PlaylistEntity>
@Query("SELECT * FROM PlaylistEntity")
suspend fun playlists(): List<PlaylistEntity>
@Query("DELETE FROM SongEntity WHERE playlist_creator_id = :playlistId")
suspend fun deletePlaylistSongs(playlistId: Long)
@Query("DELETE FROM SongEntity WHERE playlist_creator_id = :playlistId AND id = :songId")
suspend fun deleteSongFromPlaylist(playlistId: Long, songId: Long)
@Transaction
@Query("SELECT * FROM PlaylistEntity")
suspend fun playlistsWithSongs(): List<PlaylistWithSongs>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSongsToPlaylist(songEntities: List<SongEntity>)
@Query("SELECT * FROM SongEntity WHERE playlist_creator_id = :playlistId AND id = :songId")
suspend fun isSongExistsInPlaylist(playlistId: Long, songId: Long): List<SongEntity>
@Query("SELECT * FROM SongEntity WHERE playlist_creator_id = :playlistId ORDER BY song_key asc")
fun songsFromPlaylist(playlistId: Long): LiveData<List<SongEntity>>
@Delete
suspend fun deletePlaylist(playlistEntity: PlaylistEntity)
@Delete
suspend fun deletePlaylists(playlistEntities: List<PlaylistEntity>)
@Delete
suspend fun deletePlaylistSongs(songs: List<SongEntity>)
@Query("SELECT * FROM SongEntity WHERE playlist_creator_id= :playlistId")
fun favoritesSongsLiveData(playlistId: Long): LiveData<List<SongEntity>>
@Query("SELECT * FROM SongEntity WHERE playlist_creator_id= :playlistId")
fun favoritesSongs(playlistId: Long): List<SongEntity>
@Query("SELECT EXISTS(SELECT * FROM PlaylistEntity WHERE playlist_id = :playlistId)")
fun checkPlaylistExists(playlistId: Long): LiveData<Boolean>
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Entity
@Parcelize
class PlaylistEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "playlist_id")
val playListId: Long = 0,
@ColumnInfo(name = "playlist_name")
val playlistName: String
) : Parcelable

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.parcelize.Parcelize
@Parcelize
data class PlaylistWithSongs(
@Embedded val playlistEntity: PlaylistEntity,
@Relation(
parentColumn = "playlist_id",
entityColumn = "playlist_creator_id"
)
val songs: List<SongEntity>
) : Parcelable

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [PlaylistEntity::class, SongEntity::class, HistoryEntity::class, PlayCountEntity::class, BlackListStoreEntity::class, LyricsEntity::class],
version = 23,
exportSchema = false
)
abstract class RetroDatabase : RoomDatabase() {
abstract fun playlistDao(): PlaylistDao
abstract fun blackListStore(): BlackListStoreDao
abstract fun playCountDao(): PlayCountDao
abstract fun historyDao(): HistoryDao
abstract fun lyricsDao(): LyricsDao
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(indices = [Index(value = ["playlist_creator_id", "id"], unique = true)])
class SongEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "song_key")
val songPrimaryKey: Long = 0L,
@ColumnInfo(name = "playlist_creator_id")
val playlistCreatorId: Long,
val id: Long,
val title: String,
@ColumnInfo(name = "track_number")
val trackNumber: Int,
val year: Int,
val duration: Long,
val data: String,
@ColumnInfo(name = "date_modified")
val dateModified: Long,
@ColumnInfo(name = "album_id")
val albumId: Long,
@ColumnInfo(name = "album_name")
val albumName: String,
@ColumnInfo(name = "artist_id")
val artistId: Long,
@ColumnInfo(name = "artist_name")
val artistName: String,
val composer: String?,
@ColumnInfo(name = "album_artist")
val albumArtist: String?
) : Parcelable

View file

@ -0,0 +1,147 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.db
import code.name.monkey.retromusic.model.Song
fun List<HistoryEntity>.fromHistoryToSongs(): List<Song> {
return map {
it.toSong()
}
}
fun List<SongEntity>.toSongs(): List<Song> {
return map {
it.toSong()
}
}
fun Song.toHistoryEntity(timePlayed: Long): HistoryEntity {
return HistoryEntity(
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist,
timePlayed = timePlayed
)
}
fun Song.toSongEntity(playListId: Long): SongEntity {
return SongEntity(
playlistCreatorId = playListId,
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist
)
}
fun SongEntity.toSong(): Song {
return Song(
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist
)
}
fun PlayCountEntity.toSong(): Song {
return Song(
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist
)
}
fun HistoryEntity.toSong(): Song {
return Song(
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist
)
}
fun Song.toPlayCount(): PlayCountEntity {
return PlayCountEntity(
id = id,
title = title,
trackNumber = trackNumber,
year = year,
duration = duration,
data = data,
dateModified = dateModified,
albumId = albumId,
albumName = albumName,
artistId = artistId,
artistName = artistName,
composer = composer,
albumArtist = albumArtist,
timePlayed = System.currentTimeMillis(),
playCount = 1
)
}
fun List<Song>.toSongsEntity(playlistEntity: PlaylistEntity): List<SongEntity> {
return map {
it.toSongEntity(playlistEntity.playListId)
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_PLAYLISTS
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.model.Song
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class AddToPlaylistDialog : DialogFragment() {
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
companion object {
fun create(playlistEntities: List<PlaylistEntity>, song: Song): AddToPlaylistDialog {
val list: MutableList<Song> = mutableListOf()
list.add(song)
return create(playlistEntities, list)
}
fun create(playlistEntities: List<PlaylistEntity>, songs: List<Song>): AddToPlaylistDialog {
return AddToPlaylistDialog().apply {
arguments = bundleOf(
EXTRA_SONG to songs,
EXTRA_PLAYLISTS to playlistEntities
)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val playlistEntities = extraNotNull<List<PlaylistEntity>>(EXTRA_PLAYLISTS).value
val songs = extraNotNull<List<Song>>(EXTRA_SONG).value
val playlistNames = mutableListOf<String>()
playlistNames.add(requireContext().resources.getString(R.string.action_new_playlist))
for (entity: PlaylistEntity in playlistEntities) {
playlistNames.add(entity.playlistName)
}
return materialDialog(R.string.add_playlist_title)
.setItems(playlistNames.toTypedArray()) { dialog, which->
if (which == 0) {
showCreateDialog(songs)
} else {
libraryViewModel.addToPlaylist(requireContext(), playlistNames[which], songs)
}
dialog.dismiss()
}
.setNegativeButton(R.string.action_cancel, null)
.create()
.colorButtons()
}
private fun showCreateDialog(songs: List<Song>) {
CreatePlaylistDialog.create(songs).show(requireActivity().supportFragmentManager, "Dialog")
}
}

View file

@ -0,0 +1,140 @@
package code.name.monkey.retromusic.dialogs
import android.Manifest
import android.app.Dialog
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.core.app.ActivityCompat
import androidx.fragment.app.DialogFragment
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.util.getExternalStorageDirectory
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.updateListItems
import java.io.File
class BlacklistFolderChooserDialog : DialogFragment() {
private var initialPath: String = getExternalStorageDirectory().absolutePath
private var parentFolder: File? = null
private var parentContents: Array<File>? = null
private var canGoUp = false
private var callback: FolderCallback? = null
private val contentsArray: Array<String?>
get() {
if (parentContents == null) {
return if (canGoUp) {
arrayOf("..")
} else arrayOf()
}
val results = arrayOfNulls<String>(parentContents!!.size + if (canGoUp) 1 else 0)
if (canGoUp) {
results[0] = ".."
}
for (i in parentContents!!.indices) {
results[if (canGoUp) i + 1 else i] = parentContents?.getOrNull(i)?.name
}
return results
}
private fun listFiles(): Array<File>? {
val results = mutableListOf<File>()
parentFolder?.listFiles()?.let { files ->
files.forEach { file -> if (file.isDirectory) results.add(file) }
return results.sortedBy { it.name }.toTypedArray()
}
return null
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
var mSavedInstanceState = savedInstanceState
if (VersionUtils.hasMarshmallow()
&& ActivityCompat.checkSelfPermission(
requireActivity(), Manifest.permission.READ_EXTERNAL_STORAGE
)
!= PackageManager.PERMISSION_GRANTED
) {
return materialDialog().show {
title(res = R.string.md_error_label)
message(res = R.string.md_storage_perm_error)
positiveButton(res = android.R.string.ok)
}
}
if (mSavedInstanceState == null) {
mSavedInstanceState = Bundle()
}
if (!mSavedInstanceState.containsKey("current_path")) {
mSavedInstanceState.putString("current_path", initialPath)
}
parentFolder = File(mSavedInstanceState.getString("current_path", File.pathSeparator))
checkIfCanGoUp()
parentContents = listFiles()
return materialDialog()
.title(text = parentFolder?.absolutePath)
.listItems(
items = contentsArray.toCharSequence(),
waitForPositiveButton = false
) { _: MaterialDialog, i: Int, _: CharSequence ->
onSelection(i)
}
.noAutoDismiss()
.positiveButton(res = R.string.add_action) {
callback?.onFolderSelection(requireContext(), parentFolder!!)
dismiss()
}
.negativeButton(res = android.R.string.cancel) { dismiss() }
}
private fun onSelection(i: Int) {
if (canGoUp && i == 0) {
parentFolder = parentFolder?.parentFile
if (parentFolder?.absolutePath == "/storage/emulated") {
parentFolder = parentFolder?.parentFile
}
checkIfCanGoUp()
} else {
parentFolder = parentContents?.getOrNull(if (canGoUp) i - 1 else i)
canGoUp = true
if (parentFolder?.absolutePath == "/storage/emulated") {
parentFolder = getExternalStorageDirectory()
}
}
reload()
}
private fun checkIfCanGoUp() {
canGoUp = parentFolder?.parent != null
}
private fun reload() {
parentContents = listFiles()
val dialog = dialog as MaterialDialog?
dialog?.setTitle(parentFolder?.absolutePath)
dialog?.updateListItems(items = contentsArray.toCharSequence())
}
private fun Array<String?>.toCharSequence(): List<CharSequence> {
return map { it as CharSequence }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("current_path", parentFolder?.absolutePath)
}
fun setCallback(callback: FolderCallback?) {
this.callback = callback
}
interface FolderCallback {
fun onFolderSelection(context: Context, folder: File)
}
companion object {
fun create(): BlacklistFolderChooserDialog {
return BlacklistFolderChooserDialog()
}
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.TextUtils
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.DialogPlaylistBinding
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.extra
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.model.Song
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class CreatePlaylistDialog : DialogFragment() {
private var _binding: DialogPlaylistBinding? = null
private val binding get() = _binding!!
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
companion object {
fun create(song: Song): CreatePlaylistDialog {
val list = mutableListOf<Song>()
list.add(song)
return create(list)
}
fun create(songs: List<Song>): CreatePlaylistDialog {
return CreatePlaylistDialog().apply {
arguments = bundleOf(EXTRA_SONG to songs)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogPlaylistBinding.inflate(layoutInflater)
val songs: List<Song> = extra<List<Song>>(EXTRA_SONG).value ?: emptyList()
val playlistView: TextInputEditText = binding.actionNewPlaylist
val playlistContainer: TextInputLayout = binding.actionNewPlaylistContainer
return materialDialog(R.string.new_playlist_title)
.setView(binding.root)
.setPositiveButton(
R.string.create_action
) { _, _ ->
val playlistName = playlistView.text.toString()
if (!TextUtils.isEmpty(playlistName)) {
libraryViewModel.addToPlaylist(requireContext(), playlistName, songs)
} else {
playlistContainer.error = "Playlist name can't be empty"
}
}
.setNegativeButton(R.string.action_cancel, null)
.create()
.colorButtons()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_PLAYLIST
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.fragments.ReloadType
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class DeletePlaylistDialog : DialogFragment() {
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
companion object {
fun create(playlist: PlaylistEntity): DeletePlaylistDialog {
val list = mutableListOf<PlaylistEntity>()
list.add(playlist)
return create(list)
}
fun create(playlists: List<PlaylistEntity>): DeletePlaylistDialog {
return DeletePlaylistDialog().apply {
arguments = bundleOf(EXTRA_PLAYLIST to playlists)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val playlists = extraNotNull<List<PlaylistEntity>>(EXTRA_PLAYLIST).value
val title: Int
val message: CharSequence
//noinspection ConstantConditions
if (playlists.size > 1) {
title = R.string.delete_playlists_title
message = String.format(getString(R.string.delete_x_playlists), playlists.size).parseAsHtml()
} else {
title = R.string.delete_playlist_title
message = String.format(getString(R.string.delete_playlist_x), playlists[0].playlistName).parseAsHtml()
}
return materialDialog(title)
.setTitle(title)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_delete) { _, _ ->
libraryViewModel.deleteSongsFromPlaylist(playlists)
libraryViewModel.deleteRoomPlaylist(playlists)
libraryViewModel.forceReload(ReloadType.Playlists)
}
.create()
.colorButtons()
}
}

View file

@ -0,0 +1,162 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.provider.MediaStore
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.fragments.ReloadType
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.SAFUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.getViewModel
class DeleteSongsDialog : DialogFragment() {
lateinit var libraryViewModel: LibraryViewModel
companion object {
fun create(song: Song): DeleteSongsDialog {
val list = ArrayList<Song>()
list.add(song)
return create(list)
}
fun create(songs: List<Song>): DeleteSongsDialog {
return DeleteSongsDialog().apply {
arguments = bundleOf(
EXTRA_SONG to ArrayList(songs)
)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
libraryViewModel = activity?.getViewModel() as LibraryViewModel
val songs = extraNotNull<List<Song>>(EXTRA_SONG).value
if (VersionUtils.hasR()) {
val deleteResultLauncher =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
MusicPlayerRemote.playNextSong()
}
MusicPlayerRemote.removeFromQueue(songs)
reloadTabs()
}
dismiss()
}
val pendingIntent =
MediaStore.createDeleteRequest(requireActivity().contentResolver, songs.map {
MusicUtil.getSongFileUri(it.id)
})
deleteResultLauncher.launch(
IntentSenderRequest.Builder(pendingIntent.intentSender).build()
)
return super.onCreateDialog(savedInstanceState)
} else {
val pair = if (songs.size > 1) {
Pair(
R.string.delete_songs_title,
String.format(getString(R.string.delete_x_songs), songs.size).parseAsHtml()
)
} else {
Pair(
R.string.delete_song_title,
String.format(getString(R.string.delete_song_x), songs[0].title).parseAsHtml()
)
}
return materialDialog()
.title(pair.first)
.message(text = pair.second)
.noAutoDismiss()
.negativeButton(android.R.string.cancel)
{
dismiss()
}
.positiveButton(R.string.action_delete)
{
if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
MusicPlayerRemote.playNextSong()
}
if (!SAFUtil.isSAFRequiredForSongs(songs)) {
CoroutineScope(Dispatchers.IO).launch {
dismiss()
MusicUtil.deleteTracks(requireContext(), songs)
reloadTabs()
}
} else {
if (SAFUtil.isSDCardAccessGranted(requireActivity())) {
deleteSongs(songs)
} else {
startActivityForResult(
Intent(requireActivity(), code.name.monkey.retromusic.activities.saf.SAFGuideActivity::class.java),
code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE
)
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE -> {
SAFUtil.openTreePicker(this)
}
SAFUtil.REQUEST_SAF_PICK_TREE,
SAFUtil.REQUEST_SAF_PICK_FILE -> {
if (resultCode == Activity.RESULT_OK) {
SAFUtil.saveTreeUri(requireActivity(), data)
val songs = extraNotNull<List<Song>>(EXTRA_SONG).value
deleteSongs(songs)
}
}
}
}
fun deleteSongs(songs: List<Song>) {
CoroutineScope(Dispatchers.IO).launch {
dismiss()
MusicUtil.deleteTracks(requireActivity(), songs, null, null)
reloadTabs()
}
}
private fun reloadTabs() {
libraryViewModel.forceReload(ReloadType.Songs)
libraryViewModel.forceReload(ReloadType.HomeSections)
libraryViewModel.forceReload(ReloadType.Artists)
libraryViewModel.forceReload(ReloadType.Albums)
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ImportPlaylistDialog : DialogFragment() {
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return materialDialog(R.string.import_playlist)
.setMessage(R.string.import_playlist_message)
.setPositiveButton(R.string.import_label) { _, _ ->
try {
libraryViewModel.importPlaylists()
} catch (e: Exception) {
e.printStackTrace()
}
}
.create()
.colorButtons()
}
}

View file

@ -0,0 +1,58 @@
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.DialogPlaybackSpeedBinding
import code.name.monkey.retromusic.extensions.accent
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.util.PreferenceUtil
import com.google.android.material.slider.Slider
class PlaybackSpeedDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogPlaybackSpeedBinding.inflate(layoutInflater)
binding.playbackSpeedSlider.accent()
binding.playbackPitchSlider.accent()
binding.playbackSpeedSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
binding.speedValue.text = "$value"
})
binding.playbackPitchSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
binding.pitchValue.text = "$value"
})
binding.playbackSpeedSlider.value = PreferenceUtil.playbackSpeed
binding.playbackPitchSlider.value = PreferenceUtil.playbackPitch
return materialDialog(R.string.playback_settings)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ ->
updatePlaybackAndPitch(
binding.playbackSpeedSlider.value,
binding.playbackPitchSlider.value
)
}
.setNeutralButton(R.string.reset_action) {_, _ ->
updatePlaybackAndPitch(
1F,
1F
)
}
.setView(binding.root)
.create()
.colorButtons()
}
private fun updatePlaybackAndPitch(speed: Float, pitch: Float) {
PreferenceUtil.playbackSpeed = speed
PreferenceUtil.playbackPitch = pitch
}
companion object {
fun newInstance(): PlaybackSpeedDialog {
return PlaybackSpeedDialog()
}
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.SongEntity
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class RemoveSongFromPlaylistDialog : DialogFragment() {
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
companion object {
fun create(song: SongEntity): RemoveSongFromPlaylistDialog {
val list = mutableListOf<SongEntity>()
list.add(song)
return create(list)
}
fun create(songs: List<SongEntity>): RemoveSongFromPlaylistDialog {
return RemoveSongFromPlaylistDialog().apply {
arguments = bundleOf(
EXTRA_SONG to songs
)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val songs = extraNotNull<List<SongEntity>>(EXTRA_SONG).value
val pair = if (songs.size > 1) {
Pair(
R.string.remove_songs_from_playlist_title,
String.format(getString(R.string.remove_x_songs_from_playlist), songs.size)
.parseAsHtml()
)
} else {
Pair(
R.string.remove_song_from_playlist_title,
String.format(
getString(R.string.remove_song_x_from_playlist),
songs[0].title
).parseAsHtml()
)
}
return materialDialog(pair.first)
.setMessage(pair.second)
.setPositiveButton(R.string.remove_action) { _, _ ->
libraryViewModel.deleteSongsInPlaylist(songs)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
.colorButtons()
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_PLAYLIST_ID
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.fragments.ReloadType
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class RenamePlaylistDialog : DialogFragment() {
private val libraryViewModel by sharedViewModel<LibraryViewModel>()
companion object {
fun create(playlistEntity: PlaylistEntity): RenamePlaylistDialog {
return RenamePlaylistDialog().apply {
arguments = bundleOf(
EXTRA_PLAYLIST_ID to playlistEntity
)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val playlistEntity = extraNotNull<PlaylistEntity>(EXTRA_PLAYLIST_ID).value
val layout = layoutInflater.inflate(R.layout.dialog_playlist, null)
val inputEditText: TextInputEditText = layout.findViewById(R.id.actionNewPlaylist)
val nameContainer: TextInputLayout = layout.findViewById(R.id.actionNewPlaylistContainer)
nameContainer.accentColor()
inputEditText.setText(playlistEntity.playlistName)
return materialDialog(R.string.rename_playlist_title)
.setView(layout)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_rename) { _, _ ->
val name = inputEditText.text.toString()
if (name.isNotEmpty()) {
libraryViewModel.renameRoomPlaylist(playlistEntity.playListId, name)
libraryViewModel.forceReload(ReloadType.Playlists)
} else {
nameContainer.error = "Playlist name should'nt be empty"
}
}
.create()
.colorButtons()
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.media.MediaScannerConnection
import android.os.Bundle
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.EXTRA_PLAYLIST
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.helper.M3UWriter
import code.name.monkey.retromusic.util.PlaylistsUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SavePlaylistDialog : DialogFragment() {
companion object {
fun create(playlistWithSongs: PlaylistWithSongs): SavePlaylistDialog {
return SavePlaylistDialog().apply {
arguments = bundleOf(
EXTRA_PLAYLIST to playlistWithSongs
)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val playlistWithSongs = extraNotNull<PlaylistWithSongs>(EXTRA_PLAYLIST).value
if (VersionUtils.hasR()) {
createNewFile(
"audio/mpegurl",
playlistWithSongs.playlistEntity.playlistName
) { outputStream, data ->
try {
if (outputStream != null) {
lifecycleScope.launch(Dispatchers.IO) {
M3UWriter.writeIO(
outputStream,
playlistWithSongs
)
withContext(Dispatchers.Main) {
showToast(
requireContext().getString(R.string.saved_playlist_to,
data?.lastPathSegment),
Toast.LENGTH_LONG
)
dismiss()
}
}
}
} catch (e: Exception) {
showToast(
"Something went wrong : " + e.message
)
}
}
} else {
lifecycleScope.launch(Dispatchers.IO) {
val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs)
MediaScannerConnection.scanFile(
requireActivity(),
arrayOf<String>(file.path),
null
) { _, _ ->
}
withContext(Dispatchers.Main) {
showToast(
getString(R.string.saved_playlist_to, file),
Toast.LENGTH_LONG
)
dismiss()
}
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return materialDialog(R.string.save_playlist_title)
.setView(R.layout.loading)
.create().colorButtons()
}
}

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.AlarmManager
import android.app.Dialog
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import android.os.SystemClock
import android.widget.CheckBox
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.fragment.app.DialogFragment
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.DialogSleepTimerBinding
import code.name.monkey.retromusic.extensions.addAccentColor
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PENDING_QUIT
import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_QUIT
import code.name.monkey.retromusic.util.PreferenceUtil
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
class SleepTimerDialog : DialogFragment() {
private var seekArcProgress: Int = 0
private lateinit var timerUpdater: TimerUpdater
private lateinit var dialog: MaterialDialog
private lateinit var shouldFinishLastSong: CheckBox
private lateinit var timerDisplay: TextView
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
timerUpdater = TimerUpdater()
val binding = DialogSleepTimerBinding.inflate(layoutInflater)
shouldFinishLastSong = binding.shouldFinishLastSong
timerDisplay = binding.timerDisplay
val finishMusic = PreferenceUtil.isSleepTimerFinishMusic
shouldFinishLastSong.apply {
addAccentColor()
isChecked = finishMusic
}
binding.seekBar.apply {
addAccentColor()
seekArcProgress = PreferenceUtil.lastSleepTimerValue
updateTimeDisplayTime()
progress = seekArcProgress
}
binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
if (i < 1) {
seekBar.progress = 1
return
}
seekArcProgress = i
updateTimeDisplayTime()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
PreferenceUtil.lastSleepTimerValue = seekArcProgress
}
})
return materialDialog(R.string.action_sleep_timer)
.setView(binding.root)
.setPositiveButton(R.string.action_set) { _, _ ->
PreferenceUtil.isSleepTimerFinishMusic = shouldFinishLastSong.isChecked
val minutes = seekArcProgress
val pi = makeTimerPendingIntent(PendingIntent.FLAG_CANCEL_CURRENT)
val nextSleepTimerElapsedTime = SystemClock.elapsedRealtime() + minutes * 60 * 1000
PreferenceUtil.nextSleepTimerElapsedRealTime = nextSleepTimerElapsedTime.toInt()
val am = requireContext().getSystemService<AlarmManager>()
am?.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextSleepTimerElapsedTime, pi)
Toast.makeText(
requireContext(),
requireContext().resources.getString(R.string.sleep_timer_set, minutes),
Toast.LENGTH_SHORT
).show()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
val previous = makeTimerPendingIntent(PendingIntent.FLAG_NO_CREATE)
if (previous != null) {
val am = requireContext().getSystemService<AlarmManager>()
am?.cancel(previous)
previous.cancel()
Toast.makeText(
requireContext(),
requireContext().resources.getString(R.string.sleep_timer_canceled),
Toast.LENGTH_SHORT
).show()
val musicService = MusicPlayerRemote.musicService
if (musicService != null && musicService.pendingQuit) {
musicService.pendingQuit = false
Toast.makeText(
requireContext(),
requireContext().resources.getString(R.string.sleep_timer_canceled),
Toast.LENGTH_SHORT
).show()
}
}
}
.create()
.colorButtons()
}
private fun updateTimeDisplayTime() {
timerDisplay.text = "$seekArcProgress min"
}
private fun makeTimerPendingIntent(flag: Int): PendingIntent? {
return PendingIntent.getService(
requireActivity(), 0, makeTimerIntent(), flag or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
}
private fun makeTimerIntent(): Intent {
val intent = Intent(requireActivity(), MusicService::class.java)
return if (shouldFinishLastSong.isChecked) {
intent.setAction(ACTION_PENDING_QUIT)
} else intent.setAction(ACTION_QUIT)
}
private fun updateCancelButton() {
val musicService = MusicPlayerRemote.musicService
if (musicService != null && musicService.pendingQuit) {
dialog.getActionButton(WhichButton.NEUTRAL).text =
dialog.context.getString(R.string.cancel_current_timer)
} else {
dialog.getActionButton(WhichButton.NEUTRAL).text = null
}
}
private inner class TimerUpdater :
CountDownTimer(
PreferenceUtil.nextSleepTimerElapsedRealTime - SystemClock.elapsedRealtime(),
1000
) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
updateCancelButton()
}
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Spanned
import android.util.Log
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.DialogFileDetailsBinding
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
import org.jaudiotagger.audio.AudioFileIO
import java.io.File
class SongDetailDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context: Context = requireContext()
val binding = DialogFileDetailsBinding.inflate(layoutInflater)
val song = requireArguments().getParcelable<Song>(EXTRA_SONG)
with(binding) {
fileName.text = makeTextWithTitle(context, R.string.label_file_name, "-")
filePath.text = makeTextWithTitle(context, R.string.label_file_path, "-")
fileSize.text = makeTextWithTitle(context, R.string.label_file_size, "-")
fileFormat.text = makeTextWithTitle(context, R.string.label_file_format, "-")
trackLength.text = makeTextWithTitle(context, R.string.label_track_length, "-")
bitrate.text = makeTextWithTitle(context, R.string.label_bit_rate, "-")
samplingRate.text = makeTextWithTitle(context, R.string.label_sampling_rate, "-")
}
if (song != null) {
val songFile = File(song.data)
if (songFile.exists()) {
binding.fileName.text =
makeTextWithTitle(context, R.string.label_file_name, songFile.name)
binding.filePath.text =
makeTextWithTitle(context, R.string.label_file_path, songFile.absolutePath)
binding.dateModified.text = makeTextWithTitle(
context, R.string.label_last_modified,
MusicUtil.getDateModifiedString(songFile.lastModified())
)
binding.fileSize.text =
makeTextWithTitle(
context,
R.string.label_file_size,
getFileSizeString(songFile.length())
)
try {
val audioFile = AudioFileIO.read(songFile)
val audioHeader = audioFile.audioHeader
binding.fileFormat.text =
makeTextWithTitle(context, R.string.label_file_format, audioHeader.format)
binding.trackLength.text = makeTextWithTitle(
context,
R.string.label_track_length,
MusicUtil.getReadableDurationString((audioHeader.trackLength * 1000).toLong())
)
binding.bitrate.text = makeTextWithTitle(
context,
R.string.label_bit_rate,
audioHeader.bitRate + " kb/s"
)
binding.samplingRate.text =
makeTextWithTitle(
context,
R.string.label_sampling_rate,
audioHeader.sampleRate + " Hz"
)
} catch (e: Exception) {
Log.e(TAG, "error while reading the song file", e)
// fallback
binding.trackLength.text = makeTextWithTitle(
context,
R.string.label_track_length,
MusicUtil.getReadableDurationString(song.duration)
)
}
} else {
// fallback
binding.fileName.text =
makeTextWithTitle(context, R.string.label_file_name, song.title)
binding.trackLength.text = makeTextWithTitle(
context,
R.string.label_track_length,
MusicUtil.getReadableDurationString(song.duration)
)
}
}
return materialDialog(R.string.action_details)
.setPositiveButton(android.R.string.ok, null)
.setView(binding.root)
.create()
.colorButtons()
}
companion object {
val TAG: String = SongDetailDialog::class.java.simpleName
fun create(song: Song): SongDetailDialog {
return SongDetailDialog().apply {
arguments = bundleOf(
EXTRA_SONG to song
)
}
}
private fun makeTextWithTitle(context: Context, titleResId: Int, text: String?): Spanned {
return ("<b>" + context.resources.getString(titleResId) + ": " + "</b>" + text)
.parseAsHtml()
}
private fun getFileSizeString(sizeInBytes: Long): String {
val fileSizeInKB = sizeInBytes / 1024
val fileSizeInMB = fileSizeInKB / 1024
return "$fileSizeInMB MB"
}
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.dialogs
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.ShareInstagramStory
import code.name.monkey.retromusic.extensions.colorButtons
import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
class SongShareDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val song: Song? = requireArguments().getParcelable(EXTRA_SONG)
val listening: String =
String.format(
getString(R.string.currently_listening_to_x_by_x),
song?.title,
song?.artistName
)
return materialDialog(R.string.what_do_you_want_to_share)
.setItems(
arrayOf(
getString(R.string.the_audio_file),
"\u201C" + listening + "\u201D",
getString(R.string.social_stories)
)
) { _, which ->
withAction(which, song, listening)
}
.setNegativeButton(R.string.action_cancel, null)
.create()
.colorButtons()
}
private fun withAction(
which: Int,
song: Song?,
currentlyListening: String
) {
when (which) {
0 -> {
startActivity(Intent.createChooser(song?.let {
MusicUtil.createShareSongFileIntent(
it,
requireContext()
)
}, null))
}
1 -> {
startActivity(
Intent.createChooser(
Intent()
.setAction(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_TEXT, currentlyListening)
.setType("text/plain"),
null
)
)
}
2 -> {
if (song != null) {
startActivity(
Intent(
requireContext(),
ShareInstagramStory::class.java
).putExtra(
ShareInstagramStory.EXTRA_SONG,
song
)
)
}
}
}
}
companion object {
fun create(song: Song): SongShareDialog {
return SongShareDialog().apply {
arguments = bundleOf(
EXTRA_SONG to song
)
}
}
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.extensions
import android.app.Activity
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DimenRes
import androidx.appcompat.app.AppCompatActivity
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import com.google.android.material.appbar.MaterialToolbar
fun AppCompatActivity.applyToolbar(toolbar: MaterialToolbar) {
ToolbarContentTintHelper.colorBackButton(toolbar)
setSupportActionBar(toolbar)
}
inline fun <reified T : Any> Activity.extra(key: String, default: T? = null) = lazy {
val value = intent?.extras?.get(key)
if (value is T) value else default
}
inline fun <reified T : Any> Intent.extra(key: String, default: T? = null) = lazy {
val value = extras?.get(key)
if (value is T) value else default
}
inline fun <reified T : Any> Activity.extraNotNull(key: String, default: T? = null) = lazy {
val value = intent?.extras?.get(key)
requireNotNull(if (value is T) value else default) { key }
}
fun Activity.dip(@DimenRes id: Int): Int {
return resources.getDimensionPixelSize(id)
}
inline val Activity.rootView: View get() = findViewById<ViewGroup>(android.R.id.content).getChildAt(0)

View file

@ -0,0 +1,235 @@
package code.name.monkey.retromusic.extensions
import android.app.ActivityManager
import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.core.view.*
import androidx.fragment.app.FragmentActivity
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.util.PreferenceUtil
fun AppCompatActivity.toggleScreenOn() {
if (PreferenceUtil.isScreenOnEnabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
fun AppCompatActivity.keepScreenOn(keepScreenOn: Boolean) {
if (keepScreenOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
fun AppCompatActivity.setEdgeToEdgeOrImmersive() {
if (PreferenceUtil.isFullScreenMode) {
setImmersiveFullscreen()
} else {
setDrawBehindSystemBars()
}
}
fun AppCompatActivity.setImmersiveFullscreen() {
if (PreferenceUtil.isFullScreenMode) {
WindowInsetsControllerCompat(window, window.decorView).apply {
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.systemBars())
}
if (VersionUtils.hasP()) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets ->
if (insets.displayCutout != null) {
insets
} else {
// Consume insets if display doesn't have a Cutout
WindowInsetsCompat.CONSUMED
}
}
}
}
fun AppCompatActivity.exitFullscreen() {
WindowInsetsControllerCompat(window, window.decorView).apply {
show(WindowInsetsCompat.Type.systemBars())
}
}
fun AppCompatActivity.hideStatusBar() {
hideStatusBar(PreferenceUtil.isFullScreenMode)
}
private fun AppCompatActivity.hideStatusBar(fullscreen: Boolean) {
val statusBar = window.decorView.rootView.findViewById<View>(R.id.status_bar)
if (statusBar != null) {
statusBar.isGone = fullscreen
}
}
fun AppCompatActivity.setDrawBehindSystemBars() {
if (VersionUtils.hasOreo()) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = Color.TRANSPARENT
window.statusBarColor = Color.TRANSPARENT
if (VersionUtils.hasQ()) {
window.isNavigationBarContrastEnforced = false
}
} else {
setNavigationBarColorPreOreo(surfaceColor())
if (VersionUtils.hasMarshmallow()) {
setStatusBarColor(Color.TRANSPARENT)
} else {
setStatusBarColor(Color.BLACK)
}
}
}
fun FragmentActivity.setTaskDescriptionColor(color: Int) {
var colorFinal = color
// Task description requires fully opaque color
colorFinal = ColorUtil.stripAlpha(colorFinal)
// Sets color of entry in the system recents page
if (VersionUtils.hasP()) {
setTaskDescription(
ActivityManager.TaskDescription(
title as String?,
-1,
colorFinal
)
)
} else {
setTaskDescription(ActivityManager.TaskDescription(title as String?))
}
}
fun AppCompatActivity.setTaskDescriptionColorAuto() {
setTaskDescriptionColor(surfaceColor())
}
@Suppress("Deprecation")
fun AppCompatActivity.setLightStatusBar(enabled: Boolean) {
if (VersionUtils.hasMarshmallow()) {
val decorView = window.decorView
val systemUiVisibility = decorView.systemUiVisibility
if (enabled) {
decorView.systemUiVisibility =
systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
decorView.systemUiVisibility =
systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
}
}
fun AppCompatActivity.setLightStatusBarAuto() {
setLightStatusBar(surfaceColor().isColorLight)
}
fun AppCompatActivity.setLightStatusBarAuto(bgColor: Int) {
setLightStatusBar(bgColor.isColorLight)
}
@Suppress("Deprecation")
fun AppCompatActivity.setLightNavigationBar(enabled: Boolean) {
if (VersionUtils.hasOreo()) {
val decorView = window.decorView
var systemUiVisibility = decorView.systemUiVisibility
systemUiVisibility = if (enabled) {
systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
systemUiVisibility and SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibility
}
}
fun AppCompatActivity.setLightNavigationBarAuto() {
setLightNavigationBar(surfaceColor().isColorLight)
}
fun AppCompatActivity.setLightNavigationBarAuto(bgColor: Int) {
setLightNavigationBar(bgColor.isColorLight)
}
/**
* This will set the color of the view with the id "status_bar" on KitKat and Lollipop. On
* Lollipop if no such view is found it will set the statusbar color using the native method.
*
* @param color the new statusbar color (will be shifted down on Lollipop and above)
*/
fun AppCompatActivity.setStatusBarColor(color: Int) {
val statusBar = window.decorView.rootView.findViewById<View>(R.id.status_bar)
if (statusBar != null) {
when {
VersionUtils.hasMarshmallow() -> statusBar.setBackgroundColor(color)
else -> statusBar.setBackgroundColor(
ColorUtil.darkenColor(
color
)
)
}
} else {
when {
VersionUtils.hasMarshmallow() -> window.statusBarColor = color
else -> window.statusBarColor = ColorUtil.darkenColor(color)
}
}
setLightStatusBarAuto(surfaceColor())
}
fun AppCompatActivity.setStatusBarColorAuto() {
// we don't want to use statusbar color because we are doing the color darkening on our own to support KitKat
setStatusBarColor(surfaceColor())
setLightStatusBarAuto(surfaceColor())
}
fun AppCompatActivity.setNavigationBarColor(color: Int) {
if (VersionUtils.hasOreo()) {
window.navigationBarColor = color
} else {
window.navigationBarColor = ColorUtil.darkenColor(color)
}
setLightNavigationBarAuto(color)
}
fun AppCompatActivity.setNavigationBarColorPreOreo(color: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
window.navigationBarColor = ColorUtil.darkenColor(color)
}
}
fun AppCompatActivity.setStatusBarColorPreMarshmallow(color: Int) {
val statusBar = window.decorView.rootView.findViewById<View>(R.id.status_bar)
if (statusBar != null) {
statusBar.setBackgroundColor(
ColorUtil.darkenColor(
color
)
)
} else {
window.statusBarColor = ColorUtil.darkenColor(color)
}
}
fun AppCompatActivity.hideSoftKeyboard() {
val currentFocus: View? = currentFocus
if (currentFocus != null) {
val inputMethodManager =
getSystemService<InputMethodManager>()
inputMethodManager?.hideSoftInputFromWindow(currentFocus.windowToken, 0)
}
}

View file

@ -0,0 +1,305 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.extensions
import android.app.Dialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.widget.Button
import android.widget.CheckBox
import android.widget.SeekBar
import androidx.annotation.AttrRes
import androidx.annotation.CheckResult
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.Fragment
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.ATHUtil
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.util.PreferenceUtil.materialYou
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
fun Int.ripAlpha(): Int {
return ColorUtil.stripAlpha(this)
}
fun Dialog.colorControlNormal() = resolveColor(android.R.attr.colorControlNormal)
fun Toolbar.backgroundTintList() {
val surfaceColor = ATHUtil.resolveColor(context, R.attr.colorSurface, Color.BLACK)
val colorStateList = ColorStateList.valueOf(surfaceColor)
backgroundTintList = colorStateList
}
fun Context.accentColor() = ThemeStore.accentColor(this)
fun Fragment.accentColor() = ThemeStore.accentColor(requireContext())
fun Context.surfaceColor() = resolveColor(R.attr.colorSurface, Color.WHITE)
fun Fragment.surfaceColor() = resolveColor(R.attr.colorSurface, Color.WHITE)
fun Context.surfaceColor(fallBackColor: Int) = resolveColor(R.attr.colorSurface, fallBackColor)
fun Fragment.surfaceColor(fallBackColor: Int) = resolveColor(R.attr.colorSurface, fallBackColor)
fun Context.textColorSecondary() = resolveColor(android.R.attr.textColorSecondary)
fun Fragment.textColorSecondary() = resolveColor(android.R.attr.textColorSecondary)
fun Context.colorControlNormal() = resolveColor(android.R.attr.colorControlNormal)
fun Fragment.colorControlNormal() = resolveColor(android.R.attr.colorControlNormal)
fun Context.colorBackground() = resolveColor(android.R.attr.colorBackground)
fun Fragment.colorBackground() = resolveColor(android.R.attr.colorBackground)
fun Context.textColorPrimary() = resolveColor(android.R.attr.textColorPrimary)
fun Fragment.textColorPrimary() = resolveColor(android.R.attr.textColorPrimary)
fun Context.defaultFooterColor() = resolveColor(R.attr.defaultFooterColor)
fun Fragment.defaultFooterColor() = resolveColor(R.attr.defaultFooterColor)
fun Context.resolveColor(@AttrRes attr: Int, fallBackColor: Int = 0) =
ATHUtil.resolveColor(this, attr, fallBackColor)
fun Fragment.resolveColor(@AttrRes attr: Int, fallBackColor: Int = 0) =
ATHUtil.resolveColor(requireContext(), attr, fallBackColor)
fun Dialog.resolveColor(@AttrRes attr: Int, fallBackColor: Int = 0) =
ATHUtil.resolveColor(context, attr, fallBackColor)
// Don't apply accent colors if Material You is enabled
// Material Components will take care of applying material you colors
fun CheckBox.addAccentColor() {
if (materialYou) return
buttonTintList = ColorStateList.valueOf(ThemeStore.accentColor(context))
}
fun SeekBar.addAccentColor() {
if (materialYou) return
val colorState = ColorStateList.valueOf(ThemeStore.accentColor(context))
progressTintList = colorState
thumbTintList = colorState
}
fun Slider.addAccentColor() {
if (materialYou) return
val accentColor = ThemeStore.accentColor(context)
trackActiveTintList = accentColor.colorStateList
trackInactiveTintList = ColorUtil.withAlpha(accentColor, 0.5F).colorStateList
thumbTintList = accentColor.colorStateList
}
fun Slider.accent() {
if (materialYou) return
val accentColor = context.accentColor()
thumbTintList = accentColor.colorStateList
trackActiveTintList = accentColor.colorStateList
trackInactiveTintList = ColorUtil.withAlpha(accentColor, 0.1F).colorStateList
}
fun Button.accentTextColor() {
if (materialYou) return
setTextColor(context.accentColor())
}
fun MaterialButton.accentBackgroundColor() {
if (materialYou) return
backgroundTintList = ColorStateList.valueOf(context.accentColor())
}
fun MaterialButton.accentOutlineColor() {
if (materialYou) return
val color = ThemeStore.accentColor(context)
val colorStateList = ColorStateList.valueOf(color)
iconTint = colorStateList
strokeColor = colorStateList
setTextColor(colorStateList)
rippleColor = colorStateList
}
fun MaterialButton.elevatedAccentColor() {
if (materialYou) return
val color = context.darkAccentColorVariant()
rippleColor = ColorStateList.valueOf(color)
setBackgroundColor(color)
setTextColor(MaterialValueHelper.getPrimaryTextColor(context, color.isColorLight))
iconTint = ColorStateList.valueOf(context.accentColor())
}
fun SeekBar.applyColor(@ColorInt color: Int) {
thumbTintList = ColorStateList.valueOf(color)
progressTintList = ColorStateList.valueOf(color)
progressBackgroundTintList = ColorStateList.valueOf(color)
}
fun ExtendedFloatingActionButton.accentColor() {
if (materialYou) return
val color = ThemeStore.accentColor(context)
val textColor = MaterialValueHelper.getPrimaryTextColor(context, ColorUtil.isColorLight(color))
val colorStateList = ColorStateList.valueOf(color)
val textColorStateList = ColorStateList.valueOf(textColor)
backgroundTintList = colorStateList
setTextColor(textColorStateList)
iconTint = textColorStateList
}
fun FloatingActionButton.accentColor() {
if (materialYou) return
val color = ThemeStore.accentColor(context)
val textColor = MaterialValueHelper.getPrimaryTextColor(context, ColorUtil.isColorLight(color))
backgroundTintList = ColorStateList.valueOf(color)
imageTintList = ColorStateList.valueOf(textColor)
}
fun MaterialButton.applyColor(color: Int) {
val backgroundColorStateList = ColorStateList.valueOf(color)
val textColorColorStateList = ColorStateList.valueOf(
MaterialValueHelper.getPrimaryTextColor(
context,
ColorUtil.isColorLight(color)
)
)
backgroundTintList = backgroundColorStateList
setTextColor(textColorColorStateList)
iconTint = textColorColorStateList
}
fun MaterialButton.accentColor() {
if (materialYou) return
applyColor(ThemeStore.accentColor(context))
}
fun MaterialButton.applyOutlineColor(color: Int) {
val colorStateList = ColorStateList.valueOf(color)
iconTint = colorStateList
strokeColor = colorStateList
setTextColor(colorStateList)
rippleColor = colorStateList
}
fun TextInputLayout.accentColor() {
if (materialYou) return
val accentColor = ThemeStore.accentColor(context)
val colorState = ColorStateList.valueOf(accentColor)
boxStrokeColor = accentColor
defaultHintTextColor = colorState
isHintAnimationEnabled = true
}
fun CircularProgressIndicator.accentColor() {
if (materialYou) return
val color = ThemeStore.accentColor(context)
setIndicatorColor(color)
trackColor = ColorUtil.withAlpha(color, 0.2f)
}
fun CircularProgressIndicator.applyColor(color: Int) {
setIndicatorColor(color)
trackColor = ColorUtil.withAlpha(color, 0.2f)
}
fun AppCompatImageView.accentColor(): Int = ThemeStore.accentColor(context)
fun TextInputLayout.setTint(background: Boolean = true) {
if (materialYou) return
val accentColor = ThemeStore.accentColor(context)
val colorState = ColorStateList.valueOf(accentColor)
if (background) {
backgroundTintList = colorState
defaultHintTextColor = colorState
} else {
boxStrokeColor = accentColor
defaultHintTextColor = colorState
isHintAnimationEnabled = true
}
}
@CheckResult
fun Drawable.tint(@ColorInt color: Int): Drawable {
val tintedDrawable = DrawableCompat.wrap(this).mutate()
setTint(color)
return tintedDrawable
}
@CheckResult
fun Drawable.tint(context: Context, @ColorRes color: Int): Drawable =
tint(context.getColorCompat(color))
@ColorInt
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
@ColorInt
fun Context.darkAccentColor(): Int {
return ColorUtils.blendARGB(
accentColor(),
surfaceColor(),
if (surfaceColor().isColorLight) 0.9f else 0.92f
)
}
@ColorInt
fun Context.darkAccentColorVariant(): Int {
return ColorUtils.blendARGB(
accentColor(),
surfaceColor(),
if (surfaceColor().isColorLight) 0.9f else 0.95f
)
}
@ColorInt
fun Context.accentColorVariant(): Int {
return if (surfaceColor().isColorLight) {
accentColor().darkerColor
} else {
accentColor().lighterColor
}
}
inline val @receiver:ColorInt Int.isColorLight
get() = ColorUtil.isColorLight(this)
inline val @receiver:ColorInt Int.lightColor
get() = ColorUtil.withAlpha(this, 0.5F)
inline val @receiver:ColorInt Int.lighterColor
get() = ColorUtil.lightenColor(this)
inline val @receiver:ColorInt Int.darkerColor
get() = ColorUtil.darkenColor(this)
inline val Int.colorStateList : ColorStateList
get() = ColorStateList.valueOf(this)

View file

@ -0,0 +1,26 @@
package code.name.monkey.retromusic.extensions
import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
fun Context.showToast(@StringRes stringRes: Int, duration: Int = Toast.LENGTH_SHORT) {
showToast(getString(stringRes), duration)
}
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
val Context.isLandscape: Boolean get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val Context.isTablet: Boolean get() = resources.configuration.smallestScreenWidthDp >= 600
fun Context.getTintedDrawable(@DrawableRes id: Int, @ColorInt color: Int): Drawable {
return ContextCompat.getDrawable(this, id)?.tint(color)!!
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.extensions
import android.database.Cursor
// exception is rethrown manually in order to have a readable stacktrace
internal fun Cursor.getInt(columnName: String): Int {
try {
return getInt(getColumnIndexOrThrow(columnName))
} catch (ex: Throwable) {
throw IllegalStateException("invalid column $columnName", ex)
}
}
internal fun Cursor.getLong(columnName: String): Long {
try {
return getLong(getColumnIndexOrThrow(columnName))
} catch (ex: Throwable) {
throw IllegalStateException("invalid column $columnName", ex)
}
}
internal fun Cursor.getString(columnName: String): String {
try {
return getString(getColumnIndexOrThrow(columnName))
} catch (ex: Throwable) {
throw IllegalStateException("invalid column $columnName", ex)
}
}
internal fun Cursor.getStringOrNull(columnName: String): String? {
try {
return getString(getColumnIndexOrThrow(columnName))
} catch (ex: Throwable) {
throw IllegalStateException("invalid column $columnName", ex)
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.extensions
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import code.name.monkey.retromusic.BuildConfig
import code.name.monkey.retromusic.R
import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun Fragment.materialDialog(title: Int): MaterialAlertDialogBuilder {
return if (BuildConfig.DEBUG) {
MaterialAlertDialogBuilder(
requireContext(),
R.style.MaterialAlertDialogTheme
)
} else {
MaterialAlertDialogBuilder(
requireContext()
)
}.setTitle(title)
}
fun AlertDialog.colorButtons(): AlertDialog {
setOnShowListener {
getButton(AlertDialog.BUTTON_POSITIVE).accentTextColor()
getButton(AlertDialog.BUTTON_NEGATIVE).accentTextColor()
getButton(AlertDialog.BUTTON_NEUTRAL).accentTextColor()
}
return this
}
fun Fragment.materialDialog(): MaterialDialog {
return MaterialDialog(requireContext())
.cornerRadius(res = R.dimen.m3_dialog_corner_size)
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This 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 software 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.
*
*/
package code.name.monkey.retromusic.extensions
import android.app.Activity
import androidx.annotation.DimenRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
fun AppCompatActivity.dimToPixel(@DimenRes dimenRes: Int): Int {
return resources.getDimensionPixelSize(dimenRes)
}
fun Activity.dipToPix(dpInFloat: Float): Float {
val scale = resources.displayMetrics.density
return dpInFloat * scale + 0.5f
}
fun Fragment.dipToPix(dpInFloat: Float): Float {
val scale = resources.displayMetrics.density
return dpInFloat * scale + 0.5f
}

Some files were not shown because too many files have changed in this diff Show more