diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt index 0505fe14..7370fdd5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt @@ -39,6 +39,7 @@ import code.name.monkey.retromusic.mvp.presenter.AlbumDetailsPresenter import code.name.monkey.retromusic.mvp.presenter.AlbumDetailsView import code.name.monkey.retromusic.rest.model.LastFmAlbum import code.name.monkey.retromusic.util.* +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.afollestad.materialcab.MaterialCab import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.activity_album.* @@ -247,7 +248,7 @@ class AlbumDetailsActivity : AbsSlidingMusicPanelActivity(), AlbumDetailsView, C .dontAnimate() .dontTransform() .into(object : RetroMusicColoredTarget(artistImage) { - override fun onColorReady(color: Int) { + override fun onColorReady(colors: MediaNotificationProcessor) { } }) } @@ -261,8 +262,8 @@ class AlbumDetailsActivity : AbsSlidingMusicPanelActivity(), AlbumDetailsView, C .dontAnimate() .dontTransform() .into(object : RetroMusicColoredTarget(image) { - override fun onColorReady(color: Int) { - setColors(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/ArtistDetailActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/ArtistDetailActivity.kt index 86a6bbf3..08c3f68f 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/ArtistDetailActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/ArtistDetailActivity.kt @@ -33,6 +33,7 @@ import code.name.monkey.retromusic.mvp.presenter.ArtistDetailsPresenter import code.name.monkey.retromusic.mvp.presenter.ArtistDetailsView import code.name.monkey.retromusic.rest.model.LastFmArtist import code.name.monkey.retromusic.util.* +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.afollestad.materialcab.MaterialCab import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.activity_artist_content.* @@ -199,8 +200,8 @@ class ArtistDetailActivity : AbsSlidingMusicPanelActivity(), ArtistDetailsView, artist.songCount ) songTitle.text = songText - albumTitle.text =albumText - songAdapter.swapDataSet(artist.songs) + albumTitle.text = albumText + songAdapter.swapDataSet(artist.songs) albumAdapter.swapDataSet(artist.albums!!) } @@ -246,9 +247,10 @@ class ArtistDetailActivity : AbsSlidingMusicPanelActivity(), ArtistDetailsView, private fun loadArtistImage() { ArtistGlideRequest.Builder.from(Glide.with(this), artist).generatePalette(this).build() .dontAnimate().into(object : RetroMusicColoredTarget(image) { - override fun onColorReady(color: Int) { - setColors(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor) } + }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt index 5af09dfe..e73109cc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt @@ -13,6 +13,7 @@ import code.name.monkey.retromusic.fragments.player.lockscreen.LockScreenPlayerC import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import com.r0adkll.slidr.Slidr import com.r0adkll.slidr.model.SlidrConfig @@ -91,8 +92,8 @@ class LockScreenActivity : AbsMusicServiceActivity() { SongGlideRequest.Builder.from(Glide.with(this), song).checkIgnoreMediaStore(this) .generatePalette(this).build().dontAnimate() .into(object : RetroMusicColoredTarget(image) { - override fun onColorReady(color: Int) { - fragment?.setDark(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + fragment?.setDark(colors.backgroundColor) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt b/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt index ae633470..3b3f3604 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt @@ -32,6 +32,7 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.Share +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.activity_share_instagram.* @@ -70,9 +71,9 @@ class ShareInstagramStory : AbsBaseActivity() { .generatePalette(this@ShareInstagramStory) .build() .into(object : RetroMusicColoredTarget(image) { - override fun onColorReady(color: Int) { - val isColorLight = ColorUtil.isColorLight(color) - setColors(isColorLight, color) + override fun onColorReady(colors: MediaNotificationProcessor) { + val isColorLight = ColorUtil.isColorLight(colors.backgroundColor) + setColors(isColorLight, colors.backgroundColor) } }) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt index 4cdd335b..33ccf01f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt @@ -24,6 +24,7 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import me.zhanghai.android.fastscroll.PopupTextProvider @@ -116,8 +117,8 @@ open class AlbumAdapter( //setColors(defaultFooterColor, holder) } - override fun onColorReady(color: Int) { - setColors(color, holder) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor, holder) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index bc3de307..1b77d3ec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -15,6 +15,7 @@ import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide class AlbumCoverPagerAdapter( @@ -121,8 +122,8 @@ class AlbumCoverPagerAdapter( .checkIgnoreMediaStore(requireContext()) .generatePalette(requireContext()).build() .into(object : RetroMusicColoredTarget(albumCover) { - override fun onColorReady(color: Int) { - setColor(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColor(colors.backgroundColor) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumFullWidthAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumFullWidthAdapter.kt index 8c3b9df8..0bea5f5b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumFullWidthAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumFullWidthAdapter.kt @@ -27,6 +27,7 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.util.NavigationUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.views.MetalRecyclerViewPager import com.bumptech.glide.Glide @@ -81,7 +82,8 @@ class AlbumFullWidthAdapter( .generatePalette(activity) .build() .into(object : RetroMusicColoredTarget(holder.image!!) { - override fun onColorReady(color: Int) { + override fun onColorReady(colors: MediaNotificationProcessor) { + } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt index e8150213..04c4deb8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt @@ -11,6 +11,7 @@ import code.name.monkey.retromusic.helper.HorizontalAdapterHelper import code.name.monkey.retromusic.interfaces.CabHolder import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.util.MusicUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide class HorizontalAlbumAdapter( @@ -44,8 +45,8 @@ class HorizontalAlbumAdapter( setColors(albumArtistFooterColor, holder) } - override fun onColorReady(color: Int) { - setColors(color, holder) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor,holder) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt index e54aa833..e80b3ac9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt @@ -22,6 +22,7 @@ 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.NavigationUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import me.zhanghai.android.fastscroll.PopupTextProvider import java.util.* @@ -94,8 +95,8 @@ class ArtistAdapter( setColors(defaultFooterColor, holder) } - override fun onColorReady(color: Int) { - setColors(color, holder) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor,holder) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index 17b06be8..08097dde 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -23,6 +23,7 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.afollestad.materialcab.MaterialCab import com.bumptech.glide.Glide import me.zhanghai.android.fastscroll.PopupTextProvider @@ -102,8 +103,8 @@ open class SongAdapter( setColors(defaultFooterColor, holder) } - override fun onColorReady(color: Int) { - setColors(color, holder) + override fun onColorReady(colors: MediaNotificationProcessor) { + setColors(colors.backgroundColor, holder) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt index 2360e3b8..c82617b5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt @@ -20,25 +20,30 @@ import androidx.fragment.app.DialogFragment import code.name.monkey.retromusic.R import code.name.monkey.retromusic.loaders.PlaylistLoader import code.name.monkey.retromusic.model.Song -import code.name.monkey.retromusic.util.PlaylistsUtil +import code.name.monkey.retromusic.room.playlist.PlaylistDatabaseModel +import code.name.monkey.retromusic.room.playlist.PlaylistEntity import code.name.monkey.retromusic.util.PreferenceUtil -import com.afollestad.materialdialogs.LayoutMode import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.list.listItems +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class AddToPlaylistDialog : DialogFragment() { override fun onCreateDialog( savedInstanceState: Bundle? ): Dialog { + + val names = requireArguments().getParcelableArrayList("names") val playlists = PlaylistLoader.getAllPlaylists(requireContext()) val playlistNames: MutableList = mutableListOf() playlistNames.add(requireContext().resources.getString(R.string.action_new_playlist)) - for (p in playlists) { - playlistNames.add(p.name) + for (p in names!!) { + playlistNames.add(p.playlistName) } + return MaterialDialog(requireContext()).show { title(R.string.add_playlist_title) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) @@ -52,12 +57,18 @@ class AddToPlaylistDialog : DialogFragment() { } } else { dialog.dismiss() - PlaylistsUtil.addToPlaylist( + /*PlaylistsUtil.addToPlaylist( requireContext(), songs, playlists[index - 1].id, true - ) + )*/ + + GlobalScope.launch(Dispatchers.IO) { + PlaylistDatabaseModel().also { + it.addSongsToPlaylist(songs, names[index - 1]) + } + } } } } @@ -78,5 +89,17 @@ class AddToPlaylistDialog : DialogFragment() { dialog.arguments = args return dialog } + + fun create( + songs: ArrayList, + names: List + ): AddToPlaylistDialog { + val dialog = AddToPlaylistDialog() + val args = Bundle() + args.putParcelableArrayList("songs", ArrayList(songs)) + args.putParcelableArrayList("names", ArrayList(names)) + dialog.arguments = args + return dialog + } } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt index 3dd81153..650cc688 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt @@ -24,7 +24,10 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R.layout import code.name.monkey.retromusic.R.string import code.name.monkey.retromusic.extensions.appHandleColor +import code.name.monkey.retromusic.fragments.playlists.PlaylistViewModel import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.room.playlist.PlaylistDatabaseModel +import code.name.monkey.retromusic.room.playlist.PlaylistEntity import code.name.monkey.retromusic.util.PlaylistsUtil import code.name.monkey.retromusic.util.PreferenceUtil import com.afollestad.materialdialogs.LayoutMode @@ -57,13 +60,14 @@ class CreatePlaylistDialog : DialogFragment() { ?: return@positiveButton if (playlistView.text.toString().trim { it <= ' ' }.isNotEmpty()) { - val playlistId = PlaylistsUtil.createPlaylist( + /*val playlistId = PlaylistsUtil.createPlaylist( requireContext(), playlistView.text.toString() ) if (playlistId != -1) { PlaylistsUtil.addToPlaylist(requireContext(), songs, playlistId, true) - } + }*/ + PlaylistDatabaseModel().savePlaylist(PlaylistEntity(playlistView.text.toString())) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/blur/BlurPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/blur/BlurPlayerFragment.kt index e750c573..2a5fc0ed 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/blur/BlurPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/blur/BlurPlayerFragment.kt @@ -18,6 +18,7 @@ import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_blur.* @@ -103,11 +104,15 @@ class BlurPlayerFragment : AbsPlayerFragment(), SharedPreferences.OnSharedPrefer .checkIgnoreMediaStore(requireContext()) .generatePalette(requireContext()).build() .dontAnimate() - .transform(BlurTransformation.Builder(requireContext()).blurRadius(blurAmount.toFloat()).build()) + .transform( + BlurTransformation.Builder(requireContext()).blurRadius(blurAmount.toFloat()) + .build() + ) .into(object : RetroMusicColoredTarget(colorBackground) { - override fun onColorReady(color: Int) { - if (color == defaultFooterColor) { - colorBackground.setColorFilter(color) + + override fun onColorReady(colors: MediaNotificationProcessor) { + if (colors.backgroundColor == defaultFooterColor) { + colorBackground.setColorFilter(colors.backgroundColor) } } }) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt index 45f41775..719f0ffa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt @@ -19,6 +19,7 @@ import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_card_blur_player.* @@ -133,11 +134,14 @@ class CardBlurFragment : AbsPlayerFragment(), SharedPreferences.OnSharedPreferen .checkIgnoreMediaStore(requireContext()) .generatePalette(requireContext()).build() .dontAnimate() - .transform(BlurTransformation.Builder(requireContext()).blurRadius(blurAmount.toFloat()).build()) + .transform( + BlurTransformation.Builder(requireContext()).blurRadius(blurAmount.toFloat()) + .build() + ) .into(object : RetroMusicColoredTarget(colorBackground) { - override fun onColorReady(color: Int) { - if (color == defaultFooterColor) { - colorBackground.setColorFilter(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + if (colors.backgroundColor == defaultFooterColor) { + colorBackground.setColorFilter(colors.backgroundColor) } } }) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt index 9186d78c..e621ba56 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt @@ -175,7 +175,7 @@ class ClassicPlayerFragment : AbsPlayerFragment(), View.OnLayoutChangeListener, MusicPlayerRemote.position, R.layout.item_queue ) - recyclerView.apply { + recyclerView .apply { adapter = queueAdapter layoutManager = LinearLayoutManager(requireContext()) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt index af489528..8464602a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt @@ -1,7 +1,6 @@ package code.name.monkey.retromusic.fragments.player.color import android.animation.ValueAnimator -import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.view.LayoutInflater @@ -13,7 +12,6 @@ import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.MaterialValueHelper import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.extensions.getSuitableColorFor import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.glide.SongGlideRequest.Builder @@ -21,7 +19,7 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.NavigationUtil -import code.name.monkey.retromusic.util.RetroColorUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import com.bumptech.glide.request.animation.GlideAnimation import kotlinx.android.synthetic.main.fragment_color_player.* @@ -131,7 +129,7 @@ class ColorFragment : AbsPlayerFragment() { .generatePalette(requireContext()) .build() .into(object : RetroMusicColoredTarget(playerImage) { - override fun onColorReady(color: Int) { + override fun onColorReady(colors: MediaNotificationProcessor) { } @@ -141,20 +139,26 @@ class ColorFragment : AbsPlayerFragment() { ) { super.onResourceReady(resource, glideAnimation) resource?.let { - val palette = resource.palette - val swatch = RetroColorUtil.getSwatch(palette) - val textColor = RetroColorUtil.getTextColor(palette) - val backgroundColor = getSuitableColorFor( - palette, - ATHUtil.resolveColor(requireContext(), R.attr.colorSurface), - Color.BLACK - ) - if (ATHUtil.isWindowBackgroundDark(requireContext())) { - ColorUtil.desaturateColor(backgroundColor, 0.5f) - } - setColors(backgroundColor, textColor) + val colors = + MediaNotificationProcessor(requireContext(), resource.bitmap) + + + /* val palette = resource.palette + val swatch = RetroColorUtil.getSwatch(palette) + + val textColor = RetroColorUtil.getTextColor(palette) + val backgroundColor = getSuitableColorFor( + palette, + ATHUtil.resolveColor(requireContext(), R.attr.colorSurface), + Color.BLACK + ) + if (ATHUtil.isWindowBackgroundDark(requireContext())) { + ColorUtil.desaturateColor(backgroundColor, 0.5f) + } + */ + setColors(colors) } } @@ -168,19 +172,23 @@ class ColorFragment : AbsPlayerFragment() { true ) else MaterialValueHelper.getPrimaryTextColor(requireContext(), false) - setColors(backgroundColor, textColor) + //setColors(backgroundColor, textColor) } }) } - private fun setColors(backgroundColor: Int, componentsColor: Int) { - this.lastColor = componentsColor - this.backgroundColor = backgroundColor - playbackControlsFragment.setDark(componentsColor, backgroundColor) - colorGradientBackground?.setBackgroundColor(backgroundColor) - playerActivity?.setLightNavigationBar(ColorUtil.isColorLight(backgroundColor)) + private fun setColors(colors: MediaNotificationProcessor) { + this.lastColor = colors.backgroundColor + this.backgroundColor = colors.backgroundColor + playbackControlsFragment.setDark(colors) + colorGradientBackground?.setBackgroundColor(colors.backgroundColor) + playerActivity?.setLightNavigationBar(ColorUtil.isColorLight(colors.backgroundColor)) callbacks?.onPaletteColorChanged() - ToolbarContentTintHelper.colorizeToolbar(playerToolbar, componentsColor, requireActivity()) + ToolbarContentTintHelper.colorizeToolbar( + playerToolbar, + colors.secondaryTextColor, + requireActivity() + ) } companion object { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorPlaybackControlsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorPlaybackControlsFragment.kt index faffd1f2..247b0b15 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorPlaybackControlsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorPlaybackControlsFragment.kt @@ -20,6 +20,7 @@ import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.ViewUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import kotlinx.android.synthetic.main.fragment_color_player_playback_controls.* class ColorPlaybackControlsFragment : AbsPlayerControlsFragment() { @@ -95,26 +96,23 @@ class ColorPlaybackControlsFragment : AbsPlayerControlsFragment() { updateShuffleState() } - fun setDark(textColor: Int, background: Int) { - setDark(textColor) - TintHelper.setTintAuto(playPauseButton, background, false) - TintHelper.setTintAuto(playPauseButton, textColor, true) + fun setDark(colors: MediaNotificationProcessor) { + setDark(colors.secondaryTextColor) + TintHelper.setTintAuto(playPauseButton, colors.backgroundColor, false) + TintHelper.setTintAuto(playPauseButton, colors.primaryTextColor, true) + + title.setTextColor(colors.primaryTextColor) + text.setTextColor(colors.secondaryTextColor) + songInfo.setTextColor(colors.secondaryTextColor) + ViewUtil.setProgressDrawable(progressSlider, colors.primaryTextColor, true) + songCurrentProgress.setTextColor(colors.secondaryTextColor) + songTotalTime.setTextColor(colors.secondaryTextColor) + volumeFragment?.setTintableColor(colors.primaryTextColor) } override fun setDark(color: Int) { lastPlaybackControlsColor = color - lastDisabledPlaybackControlsColor = ColorUtil.withAlpha(color, 0.5f) - - title.setTextColor(lastPlaybackControlsColor) - text.setTextColor(lastDisabledPlaybackControlsColor) - songInfo.setTextColor(lastDisabledPlaybackControlsColor) - - ViewUtil.setProgressDrawable(progressSlider, lastPlaybackControlsColor, true) - - volumeFragment?.setTintableColor(color) - - songCurrentProgress.setTextColor(lastDisabledPlaybackControlsColor) - songTotalTime.setTextColor(lastDisabledPlaybackControlsColor) + lastDisabledPlaybackControlsColor = ColorUtil.withAlpha(color, 0.25f) updateRepeatState() updateShuffleState() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt index dabc9197..7d34dfbd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt @@ -24,6 +24,7 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.util.NavigationUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_full.* import kotlinx.coroutines.CoroutineScope @@ -241,7 +242,8 @@ class FullPlayerFragment : AbsPlayerFragment(), MusicProgressViewUpdateHelper.Ca .generatePalette(requireContext()) .build() .into(object : RetroMusicColoredTarget(artistImage) { - override fun onColorReady(color: Int) { + override fun onColorReady(colors: MediaNotificationProcessor) { + } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/peak/PeakPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/peak/PeakPlayerFragment.kt index 4e9a9391..9a6e7221 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/peak/PeakPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/peak/PeakPlayerFragment.kt @@ -30,6 +30,7 @@ import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_peak_player.* @@ -125,8 +126,8 @@ class PeakPlayerFragment : AbsPlayerFragment() { .generatePalette(requireContext()) .build() .into(object : RetroMusicColoredTarget(playerImage) { - override fun onColorReady(color: Int) { - playbackControlsFragment.setDark(color) + override fun onColorReady(colors: MediaNotificationProcessor) { + playbackControlsFragment.setDark(colors.backgroundColor) } }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt index 7fbb8a12..90342fe4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt @@ -20,8 +20,7 @@ import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.glide.palette.BitmapPaletteTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper -import code.name.monkey.retromusic.util.PreferenceUtil -import code.name.monkey.retromusic.util.RetroColorUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.request.animation.GlideAnimation @@ -33,11 +32,12 @@ abstract class RetroMusicColoredTarget(view: ImageView) : BitmapPaletteTarget(vi protected val albumArtistFooterColor: Int get() = ATHUtil.resolveColor(getView().context, R.attr.colorControlNormal) - abstract fun onColorReady(color: Int) + abstract fun onColorReady(colors: MediaNotificationProcessor) override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) { super.onLoadFailed(e, errorDrawable) - onColorReady(defaultFooterColor) + val colors = MediaNotificationProcessor(getView().context, errorDrawable) + onColorReady(colors) } override fun onResourceReady( @@ -45,15 +45,8 @@ abstract class RetroMusicColoredTarget(view: ImageView) : BitmapPaletteTarget(vi glideAnimation: GlideAnimation? ) { super.onResourceReady(resource, glideAnimation) - val defaultColor = defaultFooterColor - resource?.let { - onColorReady( - if (PreferenceUtil.getInstance(getView().context).isDominantColor) - RetroColorUtil.getDominantColor(it.bitmap, defaultColor) - else - RetroColorUtil.getColor(it.palette, defaultColor) - ) + onColorReady(MediaNotificationProcessor(getView().context, it.bitmap)) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt index ca2db94e..409123bd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt @@ -29,9 +29,14 @@ import code.name.monkey.retromusic.dialogs.SongDetailDialog import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.PaletteColorHolder import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.room.playlist.PlaylistDatabaseModel import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.RingtoneManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch object SongMenuHelper { val MENU_RES = R.menu.menu_item_song @@ -61,8 +66,15 @@ object SongMenuHelper { return true } R.id.action_add_to_playlist -> { - AddToPlaylistDialog.create(song) - .show(activity.supportFragmentManager, "ADD_PLAYLIST") + /* AddToPlaylistDialog.create(song) + .show(activity.supportFragmentManager, "ADD_PLAYLIST")*/ + + GlobalScope.launch(Dispatchers.IO) { + val names = async { PlaylistDatabaseModel().getPlaylistNames() }.await() + println(names.toString()) + AddToPlaylistDialog.create(arrayListOf(song), names) + .show(activity.supportFragmentManager, "ADD_PLAYLIST") + } return true } R.id.action_play_next -> { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt index 36d110f5..5e68ac42 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt @@ -21,6 +21,11 @@ import code.name.monkey.retromusic.dialogs.AddToPlaylistDialog import code.name.monkey.retromusic.dialogs.DeleteSongsDialog import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.room.playlist.PlaylistDatabaseModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.* @@ -40,8 +45,12 @@ object SongsMenuHelper { return true } R.id.action_add_to_playlist -> { - AddToPlaylistDialog.create(songs) - .show(activity.supportFragmentManager, "ADD_PLAYLIST") + GlobalScope.launch(Dispatchers.IO) { + val names = async { PlaylistDatabaseModel().getPlaylistNames() }.await() + println(names.toString()) + /*AddToPlaylistDialog.create(songs, names.await()) + .show(activity.supportFragmentManager, "ADD_PLAYLIST")*/ + } return true } R.id.action_delete_from_device -> { diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Song.kt b/app/src/main/java/code/name/monkey/retromusic/model/Song.kt index 0bd4c9a5..d662fa9a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Song.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/Song.kt @@ -14,12 +14,10 @@ package code.name.monkey.retromusic.model import android.os.Parcelable -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import code.name.monkey.retromusic.BuildConfig import code.name.monkey.retromusic.room.SongEntity import code.name.monkey.retromusic.room.SongQueueEntity +import code.name.monkey.retromusic.room.playlist.PlaylistEntity +import code.name.monkey.retromusic.room.playlist.PlaylistSongEntity import kotlinx.android.parcel.Parcelize @Parcelize @@ -75,6 +73,24 @@ open class Song( ) } + fun toPlaylistSong(song: Song, playlistEntity: PlaylistEntity): PlaylistSongEntity { + return PlaylistSongEntity( + playlistEntity.playlistId, + playlistEntity.playlistName, song.id, + song.title, + song.trackNumber, + song.year, + song.duration, + song.data, + song.dateModified, + song.albumId, + song.albumName, + song.artistId, + song.artistName, + song.composer + ) + } + @JvmStatic val emptySong = Song( -1, diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabase.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabase.kt new file mode 100644 index 00000000..b2bd5585 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabase.kt @@ -0,0 +1,22 @@ +package code.name.monkey.retromusic.room.playlist + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import code.name.monkey.retromusic.App + +@Database( + entities = [PlaylistSongEntity::class, PlaylistEntity::class], + version = 6, + exportSchema = false +) +abstract class PlaylistDatabase : RoomDatabase() { + abstract fun playlistDao(): PlaylistSongDao + + companion object { + fun instance() = Room.databaseBuilder( + App.getContext().applicationContext, + PlaylistDatabase::class.java, "retro_playlist" + ).fallbackToDestructiveMigration().build() + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabaseModel.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabaseModel.kt new file mode 100644 index 00000000..da0c640f --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistDatabaseModel.kt @@ -0,0 +1,27 @@ +package code.name.monkey.retromusic.room.playlist + +import code.name.monkey.retromusic.model.Song +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class PlaylistDatabaseModel() { + private val playlistSongDao = PlaylistDatabase.instance().playlistDao(); + + suspend fun getPlaylistNames(): List = playlistSongDao.getPlaylists() + + fun savePlaylist(playlistEntity: PlaylistEntity) = GlobalScope.launch(Dispatchers.IO) { + playlistSongDao.addPlaylist(playlistEntity) + } + + fun addSongsToPlaylist(songs: List, playlistEntity: PlaylistEntity) = + GlobalScope.launch(Dispatchers.IO) { + songs.map { Song.toPlaylistSong(it, playlistEntity) }.map { + val isExist = + playlistSongDao.checkPlaylistSongExist(it.playlistId, it.id).isEmpty() + if (isExist) { + playlistSongDao.insertSingle(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistEntity.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistEntity.kt new file mode 100644 index 00000000..5ac443ef --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistEntity.kt @@ -0,0 +1,17 @@ +package code.name.monkey.retromusic.room.playlist + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.android.parcel.Parcelize + +@Parcelize +@Entity(tableName = "playlist_table") +class PlaylistEntity( + @ColumnInfo(name = "playlist_name") val playlistName: String +) : Parcelable { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "playlist_id") + var playlistId: Int = 0 +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistRepository.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistRepository.kt new file mode 100644 index 00000000..efc5b76b --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistRepository.kt @@ -0,0 +1,11 @@ +package code.name.monkey.retromusic.room.playlist + +class PlaylistRepository(private val playlistSongDao: PlaylistSongDao) { + suspend fun insertPlaylist(playlistEntity: PlaylistEntity) { + playlistSongDao.addPlaylist(playlistEntity) + } + + suspend fun insertPlaylistSongs(songs: List) { + playlistSongDao.addPlaylistSongs(songs) + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongDao.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongDao.kt new file mode 100644 index 00000000..e4fbad00 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongDao.kt @@ -0,0 +1,24 @@ +package code.name.monkey.retromusic.room.playlist + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface PlaylistSongDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addPlaylistSongs(playlistSongs: List) + + @Query("SELECT * FROM playlist_songs WHERE playlist_id =:playlistId AND id=:id") + suspend fun checkPlaylistSongExist(playlistId: Int, id: Int): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addPlaylist(playlistEntity: PlaylistEntity) + + @Query("SELECT * FROM playlist_table") + suspend fun getPlaylists(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSingle(playlistSongEntity: PlaylistSongEntity) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongEntity.kt b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongEntity.kt new file mode 100644 index 00000000..c8ca1a30 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/room/playlist/PlaylistSongEntity.kt @@ -0,0 +1,35 @@ +package code.name.monkey.retromusic.room.playlist + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity(tableName = "playlist_songs") +class PlaylistSongEntity( + @ForeignKey( + entity = PlaylistEntity::class, + childColumns = ["playlist_id"], + parentColumns = ["playlist_id"], + onDelete = ForeignKey.CASCADE + ) + @ColumnInfo(name = "playlist_id") val playlistId: Int, + @ColumnInfo(name = "playlist_name") val playlistName: String, + + @ColumnInfo(name = "id") val id: Int, + @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "track_number") val trackNumber: Int, + @ColumnInfo(name = "year") val year: Int, + @ColumnInfo(name = "duration") val duration: Long, + @ColumnInfo(name = "data") val data: String, + @ColumnInfo(name = "date_modified") val dateModified: Long, + @ColumnInfo(name = "album_id") val albumId: Int, + @ColumnInfo(name = "album_name") val albumName: String, + @ColumnInfo(name = "artist_id") val artistId: Int, + @ColumnInfo(name = "artist_name") val artistName: String, + @ColumnInfo(name = "composer") val composer: String? +) { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "song_id") + var songId: Int = 0 +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt index 3bdc3ab5..a6b54306 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt @@ -109,11 +109,14 @@ class PlayingNotificationOreo : PlayingNotification() { resource: BitmapPaletteWrapper, glideAnimation: GlideAnimation ) { - val mediaNotificationProcessor = MediaNotificationProcessor( - service, - service - ) { i, _ -> update(resource.bitmap, i) } - mediaNotificationProcessor.processNotification(resource.bitmap) + /* val mediaNotificationProcessor = MediaNotificationProcessor( + service, + service + ) { i, _ -> update(resource.bitmap, i) } + mediaNotificationProcessor.processNotification(resource.bitmap)*/ + + val colors = MediaNotificationProcessor(service, resource.bitmap) + update(resource.bitmap, colors.backgroundColor) } override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java b/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java new file mode 100644 index 00000000..4705f820 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package code.name.monkey.retromusic.util.color; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +/** + * Utility class for image analysis and processing. + * + * @hide + */ +public class ImageUtils { + // Amount (max is 255) that two channels can differ before the color is no longer "gray". + private static final int TOLERANCE = 20; + // Alpha amount for which values below are considered transparent. + private static final int ALPHA_TOLERANCE = 50; + // Size of the smaller bitmap we're actually going to scan. + private static final int COMPACT_BITMAP_SIZE = 64; // pixels + private int[] mTempBuffer; + private Bitmap mTempCompactBitmap; + private Canvas mTempCompactBitmapCanvas; + private Paint mTempCompactBitmapPaint; + private final Matrix mTempMatrix = new Matrix(); + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect + * gray". + *

+ * Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than + * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements + * will survive the squeezing process, contaminating the result with color. + */ + public boolean isGrayscale(Bitmap bitmap) { + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + + // shrink to a more manageable (yet hopefully no more or less colorful) size + if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) { + if (mTempCompactBitmap == null) { + mTempCompactBitmap = Bitmap.createBitmap( + COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Config.ARGB_8888 + ); + mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap); + mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTempCompactBitmapPaint.setFilterBitmap(true); + } + mTempMatrix.reset(); + mTempMatrix.setScale( + (float) COMPACT_BITMAP_SIZE / width, + (float) COMPACT_BITMAP_SIZE / height, + 0, 0); + mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase + mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint); + bitmap = mTempCompactBitmap; + width = height = COMPACT_BITMAP_SIZE; + } + final int size = height * width; + ensureBufferSize(size); + bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); + for (int i = 0; i < size; i++) { + if (!isGrayscale(mTempBuffer[i])) { + return false; + } + } + return true; + } + + /** + * Makes sure that {@code mTempBuffer} has at least length {@code size}. + */ + private void ensureBufferSize(int size) { + if (mTempBuffer == null || mTempBuffer.length < size) { + mTempBuffer = new int[size]; + } + } + + /** + * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect + * gray"; if all three channels are approximately equal, this will return true. + *

+ * Note that really transparent colors are always grayscale. + */ + public static boolean isGrayscale(int color) { + int alpha = 0xFF & (color >> 24); + if (alpha < ALPHA_TOLERANCE) { + return true; + } + int r = 0xFF & (color >> 16); + int g = 0xFF & (color >> 8); + int b = 0xFF & color; + return Math.abs(r - g) < TOLERANCE + && Math.abs(r - b) < TOLERANCE + && Math.abs(g - b) < TOLERANCE; + } + + /** + * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. + */ + public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, + int maxHeight) { + if (drawable == null) { + return null; + } + int originalWidth = drawable.getIntrinsicWidth(); + int originalHeight = drawable.getIntrinsicHeight(); + if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) && + (drawable instanceof BitmapDrawable)) { + return ((BitmapDrawable) drawable).getBitmap(); + } + if (originalHeight <= 0 || originalWidth <= 0) { + return null; + } + // create a new bitmap, scaling down to fit the max dimensions of + // a large notification icon if necessary + float ratio = Math.min((float) maxWidth / (float) originalWidth, + (float) maxHeight / (float) originalHeight); + ratio = Math.min(1.0f, ratio); + int scaledWidth = (int) (ratio * originalWidth); + int scaledHeight = (int) (ratio * originalHeight); + Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); + // and paint our app bitmap on it + Canvas canvas = new Canvas(result); + drawable.setBounds(0, 0, scaledWidth, scaledHeight); + drawable.draw(canvas); + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java b/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java index 16804d8a..85d567b4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java @@ -1,15 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (C) 2017 The Android Open Source Project * - * Licensed under the GNU General Public License v3 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * 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. + * http://www.apache.org/licenses/LICENSE-2.0 * - * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License */ package code.name.monkey.retromusic.util.color; @@ -20,16 +22,19 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Handler; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.ColorInt; +import androidx.annotation.FloatRange; +import androidx.annotation.NonNull; import androidx.palette.graphics.Palette; import java.util.List; -import code.name.monkey.appthemehelper.util.ColorUtil; +import static androidx.core.graphics.ColorUtils.RGBToXYZ; /** - * @author Hemanth S (h4h13). + * A class the processes media notifications and extracts the right text and background colors. */ public class MediaNotificationProcessor { @@ -37,20 +42,24 @@ public class MediaNotificationProcessor { * The fraction below which we select the vibrant instead of the light/dark vibrant color */ private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f; + /** * Minimum saturation that a muted color must have if there exists if deciding between two * colors */ private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f; + /** * Minimum fraction that any color must have to be picked up as a text color */ private static final double MINIMUM_IMAGE_FRACTION = 0.002; + /** * The population fraction to select the dominant color as the text color over a the colored * ones. */ private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f; + /** * The population fraction to select a white or black color as the background over a color. */ @@ -58,85 +67,159 @@ public class MediaNotificationProcessor { private static final float BLACK_MAX_LIGHTNESS = 0.08f; private static final float WHITE_MIN_LIGHTNESS = 0.90f; private static final int RESIZE_BITMAP_AREA = 150 * 150; - private final Context mContext; + private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); /** - * The context of the notification. This is the app context of the package posting the - * notification. + * The lightness difference that has to be added to the primary text color to obtain the + * secondary text color when the background is light. */ - private final Context mPackageContext; + private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20; + /** + * The lightness difference that has to be added to the primary text color to obtain the + * secondary text color when the background is dark. + * A bit less then the above value, since it looks better on dark backgrounds. + */ + private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10; private float[] mFilteredBackgroundHsl = null; - private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl); + private Palette.Filter mBlackWhiteFilter = new Palette.Filter() { + @Override + public boolean isAllowed(int rgb, @NonNull float[] hsl) { + return !isWhiteOrBlack(hsl); + } + }; private boolean mIsLowPriority; - private onColorThing onColorThing; + private int backgroundColor; + private int secondaryTextColor; + private int primaryTextColor; + private int actionBarColor; + private Drawable drawable; + private Context context; - public MediaNotificationProcessor(Context context, Context packageContext, onColorThing thing) { - this(context, packageContext); - onColorThing = thing; + public MediaNotificationProcessor(Context context, Drawable drawable) { + this.context = context; + this.drawable = drawable; + getMediaPalette(); } - @VisibleForTesting - MediaNotificationProcessor(Context context, Context packageContext) { - mContext = context; - mPackageContext = packageContext; + public MediaNotificationProcessor(Context context, Bitmap bitmap) { + this.context = context; + this.drawable = new BitmapDrawable(context.getResources(), bitmap); + getMediaPalette(); + } + + + public MediaNotificationProcessor(Context context) { + this.context = context; + } + + private static boolean isColorLight(int backgroundColor) { + return calculateLuminance(backgroundColor) > 0.5f; } /** - * Processes a builder of a media notification and calculates the appropriate colors that should - * be used. - * - * @param notification the notification that is being processed - * @param builder the recovered builder for the notification. this will be modified + * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. + *

Defined as the Y component in the XYZ representation of {@code color}.

*/ - public int processNotification(Bitmap image) { + @FloatRange(from = 0.0, to = 1.0) + private static double calculateLuminance(@ColorInt int color) { + final double[] result = getTempDouble3Array(); + colorToXYZ(color, result); + // Luminance is the Y component + return result[1] / 100; + } + + private static double[] getTempDouble3Array() { + double[] result = TEMP_ARRAY.get(); + if (result == null) { + result = new double[3]; + TEMP_ARRAY.set(result); + } + return result; + } + + private static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { + RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); + } + + public void getPaletteAsync(final OnPaletteLoadedListener onPaletteLoadedListener, Drawable drawable) { + this.drawable = drawable; + final Handler handler = new Handler(); + new Thread(new Runnable() { + @Override + public void run() { + getMediaPalette(); + handler.post(new Runnable() { + @Override + public void run() { + onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this); + } + }); + } + }).start(); + + } + + public void getPaletteAsync(OnPaletteLoadedListener onPaletteLoadedListener, Bitmap bitmap) { + this.drawable = new BitmapDrawable(context.getResources(), bitmap); + getPaletteAsync(onPaletteLoadedListener, this.drawable); + } + + /** + * Processes a drawable and calculates the appropriate colors that should + * be used. + */ + private void getMediaPalette() { Bitmap bitmap; - Drawable drawable = new BitmapDrawable(mPackageContext.getResources(), image); + if (drawable != null) { + // We're transforming the builder, let's make sure all baked in RemoteViews are + // rebuilt! - int backgroundColor = 0; + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + int area = width * height; + if (area > RESIZE_BITMAP_AREA) { + double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); + width = (int) (factor * width); + height = (int) (factor * height); - int width = drawable.getIntrinsicWidth(); - int height = drawable.getIntrinsicHeight(); - int area = width * height; - if (area > RESIZE_BITMAP_AREA) { - double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); - width = (int) (factor * width); - height = (int) (factor * height); + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + + // for the background we only take the left side of the image to ensure + // a smooth transition + Palette.Builder paletteBuilder = Palette.from(bitmap) + .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) + .clearFilters() // we want all colors, red / white / black ones too! + .resizeBitmapArea(RESIZE_BITMAP_AREA); + Palette palette = paletteBuilder.generate(); + backgroundColor = findBackgroundColorAndFilter(drawable); + // we want most of the full region again, slightly shifted to the right + float textColorStartWidthFraction = 0.4f; + paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, + bitmap.getWidth(), + bitmap.getHeight()); + if (mFilteredBackgroundHsl != null) { + paletteBuilder.addFilter(new Palette.Filter() { + @Override + public boolean isAllowed(int rgb, @NonNull float[] hsl) { + // at least 10 degrees hue difference + float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); + return diff > 10 && diff < 350; + } + }); + } + paletteBuilder.addFilter(mBlackWhiteFilter); + palette = paletteBuilder.generate(); + int foregroundColor = selectForegroundColor(backgroundColor, palette); + ensureColors(backgroundColor, foregroundColor); + } } - bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, width, height); - drawable.draw(canvas); - // for the background we only take the left side of the image to ensure - // a smooth transition - Palette.Builder paletteBuilder = Palette.from(bitmap) - .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) - .clearFilters() // we want all colors, red / white / black ones too! - .resizeBitmapArea(RESIZE_BITMAP_AREA); - Palette palette = paletteBuilder.generate(); - backgroundColor = findBackgroundColorAndFilter(palette); - // we want most of the full region again, slightly shifted to the right - - float textColorStartWidthFraction = 0.4f; - paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, - bitmap.getWidth(), - bitmap.getHeight()); - if (mFilteredBackgroundHsl != null) { - paletteBuilder.addFilter((rgb, hsl) -> { - // at least 10 degrees hue difference - float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); - return diff > 10 && diff < 350; - }); - } - paletteBuilder.addFilter(mBlackWhiteFilter); - palette = paletteBuilder.generate(); - int foregroundColor = selectForegroundColor(backgroundColor, palette); - - onColorThing.bothColor(backgroundColor, foregroundColor); - return backgroundColor; } private int selectForegroundColor(int backgroundColor, Palette palette) { - if (ColorUtil.INSTANCE.isColorLight(backgroundColor)) { + if (isColorLight(backgroundColor)) { return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(), palette.getVibrantSwatch(), palette.getDarkMutedSwatch(), @@ -153,6 +236,10 @@ public class MediaNotificationProcessor { } } + public boolean isLight() { + return isColorLight(backgroundColor); + } + private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch, Palette.Swatch dominantSwatch, int fallbackColor) { @@ -224,7 +311,27 @@ public class MediaNotificationProcessor { && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); } - private int findBackgroundColorAndFilter(Palette palette) { + public int findBackgroundColorAndFilter(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + int area = width * height; + + double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); + width = (int) (factor * width); + height = (int) (factor * height); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + + // for the background we only take the left side of the image to ensure + // a smooth transition + Palette.Builder paletteBuilder = Palette.from(bitmap) + .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) + .clearFilters() // we want all colors, red / white / black ones too! + .resizeBitmapArea(RESIZE_BITMAP_AREA); + Palette palette = paletteBuilder.generate(); // by default we use the dominant palette Palette.Swatch dominantSwatch = palette.getDominantSwatch(); if (dominantSwatch == null) { @@ -232,6 +339,7 @@ public class MediaNotificationProcessor { mFilteredBackgroundHsl = null; return Color.WHITE; } + if (!isWhiteOrBlack(dominantSwatch.getHsl())) { mFilteredBackgroundHsl = dominantSwatch.getHsl(); return dominantSwatch.getRgb(); @@ -287,7 +395,88 @@ public class MediaNotificationProcessor { mIsLowPriority = isLowPriority; } - public interface onColorThing { - void bothColor(int i, int i2); + private void ensureColors(int backgroundColor, int mForegroundColor) { + { + double backLum = NotificationColorUtil.calculateLuminance(backgroundColor); + double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor); + double contrast = NotificationColorUtil.calculateContrast(mForegroundColor, + backgroundColor); + // We only respect the given colors if worst case Black or White still has + // contrast + boolean backgroundLight = backLum > textLum + && NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.BLACK) + || backLum <= textLum + && !NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.WHITE); + if (contrast < 4.5f) { + if (backgroundLight) { + secondaryTextColor = NotificationColorUtil.findContrastColor( + mForegroundColor, + backgroundColor, + true /* findFG */, + 4.5f); + primaryTextColor = NotificationColorUtil.changeColorLightness( + secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT); + } else { + secondaryTextColor = + NotificationColorUtil.findContrastColorAgainstDark( + mForegroundColor, + backgroundColor, + true /* findFG */, + 4.5f); + primaryTextColor = NotificationColorUtil.changeColorLightness( + secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK); + } + } else { + primaryTextColor = mForegroundColor; + secondaryTextColor = NotificationColorUtil.changeColorLightness( + primaryTextColor, backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT + : LIGHTNESS_TEXT_DIFFERENCE_DARK); + if (NotificationColorUtil.calculateContrast(secondaryTextColor, + backgroundColor) < 4.5f) { + // oh well the secondary is not good enough + if (backgroundLight) { + secondaryTextColor = NotificationColorUtil.findContrastColor( + secondaryTextColor, + backgroundColor, + true /* findFG */, + 4.5f); + } else { + secondaryTextColor + = NotificationColorUtil.findContrastColorAgainstDark( + secondaryTextColor, + backgroundColor, + true /* findFG */, + 4.5f); + } + primaryTextColor = NotificationColorUtil.changeColorLightness( + secondaryTextColor, backgroundLight + ? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT + : -LIGHTNESS_TEXT_DIFFERENCE_DARK); + } + } + } + actionBarColor = NotificationColorUtil.resolveActionBarColor(context, + backgroundColor); } -} + + + public int getPrimaryTextColor() { + return primaryTextColor; + } + + public int getSecondaryTextColor() { + return secondaryTextColor; + } + + public int getActionBarColor() { + return actionBarColor; + } + + public int getBackgroundColor() { + return backgroundColor; + } + + public interface OnPaletteLoadedListener { + void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor); + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java new file mode 100644 index 00000000..a663535d --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java @@ -0,0 +1,990 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package code.name.monkey.retromusic.util.color; + +import android.app.Notification; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.ColorInt; +import androidx.annotation.FloatRange; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +import java.util.WeakHashMap; + +import code.name.monkey.retromusic.R; + +/** + * Helper class to process legacy (Holo) notifications to make them look like material notifications. + * + * @hide + */ +public class NotificationColorUtil { + + private static final String TAG = "NotificationColorUtil"; + private static final boolean DEBUG = false; + + private static final Object sLock = new Object(); + private static NotificationColorUtil sInstance; + + private final ImageUtils mImageUtils = new ImageUtils(); + private final WeakHashMap> mGrayscaleBitmapCache = + new WeakHashMap>(); + + private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp) + + private NotificationColorUtil(Context context) { + mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize( + R.dimen.notification_large_icon_width); + } + + public static NotificationColorUtil getInstance(Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new NotificationColorUtil(context); + } + return sInstance; + } + } + + /** + * Clears all color spans of a text + * + * @param charSequence the input text + * @return the same text but without color spans + */ + public static CharSequence clearColorSpans(CharSequence charSequence) { + if (charSequence instanceof Spanned) { + Spanned ss = (Spanned) charSequence; + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + Object resultSpan = span; + if (resultSpan instanceof CharacterStyle) { + resultSpan = ((CharacterStyle) span).getUnderlying(); + } + if (resultSpan instanceof TextAppearanceSpan) { + TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; + if (originalSpan.getTextColor() != null) { + resultSpan = new TextAppearanceSpan( + originalSpan.getFamily(), + originalSpan.getTextStyle(), + originalSpan.getTextSize(), + null, + originalSpan.getLinkTextColor()); + } + } else if (resultSpan instanceof ForegroundColorSpan + || (resultSpan instanceof BackgroundColorSpan)) { + continue; + } else { + resultSpan = span; + } + builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + ss.getSpanFlags(span)); + } + return builder; + } + return charSequence; + } + + +// /** +// * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on +// * the text. +// * +// * @param charSequence The text to process. +// * @return The color inverted text. +// */ +// public CharSequence invertCharSequenceColors(CharSequence charSequence) { +// if (charSequence instanceof Spanned) { +// Spanned ss = (Spanned) charSequence; +// Object[] spans = ss.getSpans(0, ss.length(), Object.class); +// SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); +// for (Object span : spans) { +// Object resultSpan = span; +// if (resultSpan instanceof CharacterStyle) { +// resultSpan = ((CharacterStyle) span).getUnderlying(); +// } +// if (resultSpan instanceof TextAppearanceSpan) { +// TextAppearanceSpan processedSpan = processTextAppearanceSpan( +// (TextAppearanceSpan) span); +// if (processedSpan != resultSpan) { +// resultSpan = processedSpan; +// } else { +// // we need to still take the orgininal for wrapped spans +// resultSpan = span; +// } +// } else if (resultSpan instanceof ForegroundColorSpan) { +// ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan; +// int foregroundColor = originalSpan.getForegroundColor(); +// resultSpan = new ForegroundColorSpan(processColor(foregroundColor)); +// } else { +// resultSpan = span; +// } +// builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), +// ss.getSpanFlags(span)); +// } +// return builder; +// } +// return charSequence; +// } + +// private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { +// ColorStateList colorStateList = span.getTextColor(); +// if (colorStateList != null) { +// int[] colors = colorStateList.getColors(); +// boolean changed = false; +// for (int i = 0; i < colors.length; i++) { +// if (ImageUtils.isGrayscale(colors[i])) { +// +// // Allocate a new array so we don't change the colors in the old color state +// // list. +// if (!changed) { +// colors = Arrays.copyOf(colors, colors.length); +// } +// colors[i] = processColor(colors[i]); +// changed = true; +// } +// } +// if (changed) { +// return new TextAppearanceSpan( +// span.getFamily(), span.getTextStyle(), span.getTextSize(), +// new ColorStateList(colorStateList.getStates(), colors), +// span.getLinkTextColor()); +// } +// } +// return span; +// } + + /** + * Finds a suitable color such that there's enough contrast. + * + * @param color the color to start searching from. + * @param other the color to ensure contrast against. Assumed to be lighter than {@param color} + * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. + * @param minRatio the minimum contrast ratio required. + * @return a color with the same hue as {@param color}, potentially darkened to meet the + * contrast ratio. + */ + public static int findContrastColor(int color, int other, boolean findFg, double minRatio) { + int fg = findFg ? color : other; + int bg = findFg ? other : color; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; + } + + double[] lab = new double[3]; + ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab); + + double low = 0, high = lab[0]; + final double a = lab[1], b = lab[2]; + for (int i = 0; i < 15 && high - low > 0.00001; i++) { + final double l = (low + high) / 2; + if (findFg) { + fg = ColorUtilsFromCompat.LABToColor(l, a, b); + } else { + bg = ColorUtilsFromCompat.LABToColor(l, a, b); + } + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + low = l; + } else { + high = l; + } + } + return ColorUtilsFromCompat.LABToColor(low, a, b); + } + + /** + * Finds a suitable alpha such that there's enough contrast. + * + * @param color the color to start searching from. + * @param backgroundColor the color to ensure contrast against. + * @param minRatio the minimum contrast ratio required. + * @return the same color as {@param color} with potentially modified alpha to meet contrast + */ + public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) { + int fg = color; + int bg = backgroundColor; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; + } + int startAlpha = Color.alpha(color); + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + int low = startAlpha, high = 255; + for (int i = 0; i < 15 && high - low > 0; i++) { + final int alpha = (low + high) / 2; + fg = Color.argb(alpha, r, g, b); + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + high = alpha; + } else { + low = alpha; + } + } + return Color.argb(high, r, g, b); + } + + /** + * Finds a suitable color such that there's enough contrast. + * + * @param color the color to start searching from. + * @param other the color to ensure contrast against. Assumed to be darker than {@param color} + * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. + * @param minRatio the minimum contrast ratio required. + * @return a color with the same hue as {@param color}, potentially darkened to meet the + * contrast ratio. + */ + public static int findContrastColorAgainstDark(int color, int other, boolean findFg, + double minRatio) { + int fg = findFg ? color : other; + int bg = findFg ? other : color; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; + } + + float[] hsl = new float[3]; + ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl); + + float low = hsl[2], high = 1; + for (int i = 0; i < 15 && high - low > 0.00001; i++) { + final float l = (low + high) / 2; + hsl[2] = l; + if (findFg) { + fg = ColorUtilsFromCompat.HSLToColor(hsl); + } else { + bg = ColorUtilsFromCompat.HSLToColor(hsl); + } + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + high = l; + } else { + low = l; + } + } + return findFg ? fg : bg; + } + + public static int ensureTextContrastOnBlack(int color) { + return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12); + } + + /** + * Finds a large text color with sufficient contrast over bg that has the same or darker hue as + * the original color, depending on the value of {@code isBgDarker}. + * + * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. + */ + public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) { + return isBgDarker + ? findContrastColorAgainstDark(color, bg, true, 3) + : findContrastColor(color, bg, true, 3); + } + + /** + * Finds a text color with sufficient contrast over bg that has the same or darker hue as the + * original color, depending on the value of {@code isBgDarker}. + * + * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. + */ + private static int ensureTextContrast(int color, int bg, boolean isBgDarker) { + return isBgDarker + ? findContrastColorAgainstDark(color, bg, true, 4.5) + : findContrastColor(color, bg, true, 4.5); + } + + /** + * Finds a background color for a text view with given text color and hint text color, that + * has the same hue as the original color. + */ + public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) { + color = findContrastColor(color, hintColor, false, 3.0); + return findContrastColor(color, textColor, false, 4.5); + } + + private static String contrastChange(int colorOld, int colorNew, int bg) { + return String.format("from %.2f:1 to %.2f:1", + ColorUtilsFromCompat.calculateContrast(colorOld, bg), + ColorUtilsFromCompat.calculateContrast(colorNew, bg)); + } + + /** + * Change a color by a specified value + * + * @param baseColor the base color to lighten + * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L + * increase in the LAB color space. A negative value will darken the color and + * a positive will lighten it. + * @return the changed color + */ + public static int changeColorLightness(int baseColor, int amount) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(baseColor, result); + result[0] = Math.max(Math.min(100, result[0] + amount), 0); + return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); + } + + // public static int resolvePrimaryColor(Context context, int backgroundColor) { +// boolean useDark = shouldUseDark(backgroundColor); +// if (useDark) { +// return context.getColor( +// com.android.internal.R.color.notification_primary_text_color_light); +// } else { +// return context.getColor( +// com.android.internal.R.color.notification_primary_text_color_dark); +// } +// } +// +// public static int resolveSecondaryColor(Context context, int backgroundColor) { +// boolean useDark = shouldUseDark(backgroundColor); +// if (useDark) { +// return context.getColor( +// com.android.internal.R.color.notification_secondary_text_color_light); +// } else { +// return context.getColor( +// com.android.internal.R.color.notification_secondary_text_color_dark); +// } +// } +// + public static int resolveActionBarColor(Context context, int backgroundColor) { + if (backgroundColor == Notification.COLOR_DEFAULT) { + return Color.BLACK; + } + return getShiftedColor(backgroundColor, 7); + } + +// /** +// * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} +// */ +// public static int resolveColor(Context context, int color) { +// if (color == Notification.COLOR_DEFAULT) { +// return context.getColor(com.android.internal.R.color.notification_icon_default_color); +// } +// return color; +// } +// +// +// public static int resolveContrastColor(Context context, int notificationColor, +// int backgroundColor) { +// return NotificationColorUtil.resolveContrastColor(context, notificationColor, +// backgroundColor, false /* isDark */); +// } + +// /** +// * Resolves a Notification's color such that it has enough contrast to be used as the +// * color for the Notification's action and header text. +// * +// * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT} +// * @param backgroundColor the background color to ensure the contrast against. +// * @param isDark whether or not the {@code notificationColor} will be placed on a background +// * that is darker than the color itself +// * @return a color of the same hue with enough contrast against the backgrounds. +// */ +// public static int resolveContrastColor(Context context, int notificationColor, +// int backgroundColor, boolean isDark) { +// final int resolvedColor = resolveColor(context, notificationColor); +// +// final int actionBg = context.getColor( +// com.android.internal.R.color.notification_action_list); +// +// int color = resolvedColor; +// color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark); +// color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark); +// +// if (color != resolvedColor) { +// if (DEBUG){ +// Log.w(TAG, String.format( +// "Enhanced contrast of notification for %s %s (over action)" +// + " and %s (over background) by changing #%s to %s", +// context.getPackageName(), +// NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), +// NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor), +// Integer.toHexString(resolvedColor), Integer.toHexString(color))); +// } +// } +// return color; +// } + + /** + * Get a color that stays in the same tint, but darkens or lightens it by a certain + * amount. + * This also looks at the lightness of the provided color and shifts it appropriately. + * + * @param color the base color to use + * @param amount the amount from 1 to 100 how much to modify the color + * @return the now color that was modified + */ + public static int getShiftedColor(int color, int amount) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(color, result); + if (result[0] >= 4) { + result[0] = Math.max(0, result[0] - amount); + } else { + result[0] = Math.min(100, result[0] + amount); + } + return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); + } + +// public static int resolveAmbientColor(Context context, int notificationColor) { +// final int resolvedColor = resolveColor(context, notificationColor); +// +// int color = resolvedColor; +// color = NotificationColorUtil.ensureTextContrastOnBlack(color); +// +// if (color != resolvedColor) { +// if (DEBUG){ +// Log.w(TAG, String.format( +// "Ambient contrast of notification for %s is %s (over black)" +// + " by changing #%s to #%s", +// context.getPackageName(), +// NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK), +// Integer.toHexString(resolvedColor), Integer.toHexString(color))); +// } +// } +// return color; +// } + + private static boolean shouldUseDark(int backgroundColor) { + boolean useDark = backgroundColor == Notification.COLOR_DEFAULT; + if (!useDark) { + useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; + } + return useDark; + } + + public static double calculateLuminance(int backgroundColor) { + return ColorUtilsFromCompat.calculateLuminance(backgroundColor); + } + + public static double calculateContrast(int foregroundColor, int backgroundColor) { + return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor); + } + + public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) { + return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5; + } + + /** + * Composite two potentially translucent colors over each other and returns the result. + */ + public static int compositeColors(int foreground, int background) { + return ColorUtilsFromCompat.compositeColors(foreground, background); + } + + public static boolean isColorLight(int backgroundColor) { + return calculateLuminance(backgroundColor) > 0.5f; + } + + /** + * Checks whether a Bitmap is a small grayscale icon. + * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp". + * + * @param bitmap The bitmap to test. + * @return True if the bitmap is grayscale; false if it is color or too large to examine. + */ + public boolean isGrayscaleIcon(Bitmap bitmap) { + // quick test: reject large bitmaps + if (bitmap.getWidth() > mGrayscaleIconMaxSize + || bitmap.getHeight() > mGrayscaleIconMaxSize) { + return false; + } + + synchronized (sLock) { + Pair cached = mGrayscaleBitmapCache.get(bitmap); + if (cached != null) { + if (cached.second == bitmap.getGenerationId()) { + return cached.first; + } + } + } + boolean result; + int generationId; + synchronized (mImageUtils) { + result = mImageUtils.isGrayscale(bitmap); + + // generationId and the check whether the Bitmap is grayscale can't be read atomically + // here. However, since the thread is in the process of posting the notification, we can + // assume that it doesn't modify the bitmap while we are checking the pixels. + generationId = bitmap.getGenerationId(); + } + synchronized (sLock) { + mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); + } + return result; + } + + private int processColor(int color) { + return Color.argb(Color.alpha(color), + 255 - Color.red(color), + 255 - Color.green(color), + 255 - Color.blue(color)); + } + + /** + * Framework copy of functions needed from android.support.v4.graphics.ColorUtils. + */ + private static class ColorUtilsFromCompat { + private static final double XYZ_WHITE_REFERENCE_X = 95.047; + private static final double XYZ_WHITE_REFERENCE_Y = 100; + private static final double XYZ_WHITE_REFERENCE_Z = 108.883; + private static final double XYZ_EPSILON = 0.008856; + private static final double XYZ_KAPPA = 903.3; + + private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; + private static final int MIN_ALPHA_SEARCH_PRECISION = 1; + + private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); + + private ColorUtilsFromCompat() { + } + + /** + * Composite two potentially translucent colors over each other and returns the result. + */ + public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { + int bgAlpha = Color.alpha(background); + int fgAlpha = Color.alpha(foreground); + int a = compositeAlpha(fgAlpha, bgAlpha); + + int r = compositeComponent(Color.red(foreground), fgAlpha, + Color.red(background), bgAlpha, a); + int g = compositeComponent(Color.green(foreground), fgAlpha, + Color.green(background), bgAlpha, a); + int b = compositeComponent(Color.blue(foreground), fgAlpha, + Color.blue(background), bgAlpha, a); + + return Color.argb(a, r, g, b); + } + + private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { + return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); + } + + private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { + if (a == 0) return 0; + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); + } + + /** + * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. + *

Defined as the Y component in the XYZ representation of {@code color}.

+ */ + @FloatRange(from = 0.0, to = 1.0) + public static double calculateLuminance(@ColorInt int color) { + final double[] result = getTempDouble3Array(); + colorToXYZ(color, result); + // Luminance is the Y component + return result[1] / 100; + } + + /** + * Returns the contrast ratio between {@code foreground} and {@code background}. + * {@code background} must be opaque. + *

+ * Formula defined + * here. + */ + public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { + if (Color.alpha(background) != 255) { + Log.wtf(TAG, "background can not be translucent: #" + + Integer.toHexString(background)); + } + if (Color.alpha(foreground) < 255) { + // If the foreground is translucent, composite the foreground over the background + foreground = compositeColors(foreground, background); + } + + final double luminance1 = calculateLuminance(foreground) + 0.05; + final double luminance2 = calculateLuminance(background) + 0.05; + + // Now return the lighter luminance divided by the darker luminance + return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); + } + + /** + * Convert the ARGB color to its CIE Lab representative components. + * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outLab 3-element array which holds the resulting LAB components + */ + public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { + RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); + } + + /** + * Convert RGB components to its CIE Lab representative components. + * + *

    + *
  • outLab[0] is L [0 ...100)
  • + *
  • outLab[1] is a [-128...127)
  • + *
  • outLab[2] is b [-128...127)
  • + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outLab 3-element array which holds the resulting LAB components + */ + public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull double[] outLab) { + // First we convert RGB to XYZ + RGBToXYZ(r, g, b, outLab); + // outLab now contains XYZ + XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); + // outLab now contains LAB representation + } + + /** + * Convert the ARGB color to it's CIE XYZ representative components. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE + * 2° Standard Observer (1931).

+ * + *
    + *
  • outXyz[0] is X [0 ...95.047)
  • + *
  • outXyz[1] is Y [0...100)
  • + *
  • outXyz[2] is Z [0...108.883)
  • + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outXyz 3-element array which holds the resulting LAB components + */ + public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { + RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); + } + + /** + * Convert RGB components to it's CIE XYZ representative components. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE + * 2° Standard Observer (1931).

+ * + *
    + *
  • outXyz[0] is X [0 ...95.047)
  • + *
  • outXyz[1] is Y [0...100)
  • + *
  • outXyz[2] is Z [0...108.883)
  • + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outXyz 3-element array which holds the resulting XYZ components + */ + public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull double[] outXyz) { + if (outXyz.length != 3) { + throw new IllegalArgumentException("outXyz must have a length of 3."); + } + + double sr = r / 255.0; + sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); + double sg = g / 255.0; + sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); + double sb = b / 255.0; + sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); + + outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); + outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); + outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); + } + + /** + * Converts a color from CIE XYZ to CIE Lab representation. + * + *

This method expects the XYZ representation to use the D65 illuminant and the CIE + * 2° Standard Observer (1931).

+ * + *
    + *
  • outLab[0] is L [0 ...100)
  • + *
  • outLab[1] is a [-128...127)
  • + *
  • outLab[2] is b [-128...127)
  • + *
+ * + * @param x X component value [0...95.047) + * @param y Y component value [0...100) + * @param z Z component value [0...108.883) + * @param outLab 3-element array which holds the resulting Lab components + */ + public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, + @NonNull double[] outLab) { + if (outLab.length != 3) { + throw new IllegalArgumentException("outLab must have a length of 3."); + } + x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); + y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); + z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); + outLab[0] = Math.max(0, 116 * y - 16); + outLab[1] = 500 * (x - y); + outLab[2] = 200 * (y - z); + } + + /** + * Converts a color from CIE Lab to CIE XYZ representation. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE + * 2° Standard Observer (1931).

+ * + *
    + *
  • outXyz[0] is X [0 ...95.047)
  • + *
  • outXyz[1] is Y [0...100)
  • + *
  • outXyz[2] is Z [0...108.883)
  • + *
+ * + * @param l L component value [0...100) + * @param a A component value [-128...127) + * @param b B component value [-128...127) + * @param outXyz 3-element array which holds the resulting XYZ components + */ + public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, + @FloatRange(from = -128, to = 127) final double a, + @FloatRange(from = -128, to = 127) final double b, + @NonNull double[] outXyz) { + final double fy = (l + 16) / 116; + final double fx = a / 500 + fy; + final double fz = fy - b / 200; + + double tmp = Math.pow(fx, 3); + final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; + final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; + + tmp = Math.pow(fz, 3); + final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; + + outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; + outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; + outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; + } + + /** + * Converts a color from CIE XYZ to its RGB representation. + * + *

This method expects the XYZ representation to use the D65 illuminant and the CIE + * 2° Standard Observer (1931).

+ * + * @param x X component value [0...95.047) + * @param y Y component value [0...100) + * @param z Z component value [0...108.883) + * @return int containing the RGB representation + */ + @ColorInt + public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { + double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; + double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; + double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; + + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; + b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; + + return Color.rgb( + constrain((int) Math.round(r * 255), 0, 255), + constrain((int) Math.round(g * 255), 0, 255), + constrain((int) Math.round(b * 255), 0, 255)); + } + + /** + * Converts a color from CIE Lab to its RGB representation. + * + * @param l L component value [0...100] + * @param a A component value [-128...127] + * @param b B component value [-128...127] + * @return int containing the RGB representation + */ + @ColorInt + public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, + @FloatRange(from = -128, to = 127) final double a, + @FloatRange(from = -128, to = 127) final double b) { + final double[] result = getTempDouble3Array(); + LABToXYZ(l, a, b, result); + return XYZToColor(result[0], result[1], result[2]); + } + + private static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static double pivotXyzComponent(double component) { + return component > XYZ_EPSILON + ? Math.pow(component, 1 / 3.0) + : (XYZ_KAPPA * component + 16) / 116; + } + + public static double[] getTempDouble3Array() { + double[] result = TEMP_ARRAY.get(); + if (result == null) { + result = new double[3]; + TEMP_ARRAY.set(result); + } + return result; + } + + /** + * Convert HSL (hue-saturation-lightness) components to a RGB color. + *
    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * If hsv values are out of range, they are pinned. + * + * @param hsl 3-element array which holds the input HSL components + * @return the resulting RGB color + */ + @ColorInt + public static int HSLToColor(@NonNull float[] hsl) { + final float h = hsl[0]; + final float s = hsl[1]; + final float l = hsl[2]; + + final float c = (1f - Math.abs(2 * l - 1f)) * s; + final float m = l - 0.5f * c; + final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); + + final int hueSegment = (int) h / 60; + + int r = 0, g = 0, b = 0; + + switch (hueSegment) { + case 0: + r = Math.round(255 * (c + m)); + g = Math.round(255 * (x + m)); + b = Math.round(255 * m); + break; + case 1: + r = Math.round(255 * (x + m)); + g = Math.round(255 * (c + m)); + b = Math.round(255 * m); + break; + case 2: + r = Math.round(255 * m); + g = Math.round(255 * (c + m)); + b = Math.round(255 * (x + m)); + break; + case 3: + r = Math.round(255 * m); + g = Math.round(255 * (x + m)); + b = Math.round(255 * (c + m)); + break; + case 4: + r = Math.round(255 * (x + m)); + g = Math.round(255 * m); + b = Math.round(255 * (c + m)); + break; + case 5: + case 6: + r = Math.round(255 * (c + m)); + g = Math.round(255 * m); + b = Math.round(255 * (x + m)); + break; + } + + r = constrain(r, 0, 255); + g = constrain(g, 0, 255); + b = constrain(b, 0, 255); + + return Color.rgb(r, g, b); + } + + /** + * Convert the ARGB color to its HSL (hue-saturation-lightness) components. + *
    + *
  • outHsl[0] is Hue [0 .. 360)
  • + *
  • outHsl[1] is Saturation [0...1]
  • + *
  • outHsl[2] is Lightness [0...1]
  • + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outHsl 3-element array which holds the resulting HSL components + */ + public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { + RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); + } + + /** + * Convert RGB components to HSL (hue-saturation-lightness). + *
    + *
  • outHsl[0] is Hue [0 .. 360)
  • + *
  • outHsl[1] is Saturation [0...1]
  • + *
  • outHsl[2] is Lightness [0...1]
  • + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outHsl 3-element array which holds the resulting HSL components + */ + public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull float[] outHsl) { + final float rf = r / 255f; + final float gf = g / 255f; + final float bf = b / 255f; + + final float max = Math.max(rf, Math.max(gf, bf)); + final float min = Math.min(rf, Math.min(gf, bf)); + final float deltaMaxMin = max - min; + + float h, s; + float l = (max + min) / 2f; + + if (max == min) { + // Monochromatic + h = s = 0f; + } else { + if (max == rf) { + h = ((gf - bf) / deltaMaxMin) % 6f; + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2f; + } else { + h = ((rf - gf) / deltaMaxMin) + 4f; + } + + s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); + } + + h = (h * 60f) % 360f; + if (h < 0) { + h += 360f; + } + + outHsl[0] = constrain(h, 0f, 360f); + outHsl[1] = constrain(s, 0f, 1f); + outHsl[2] = constrain(l, 0f, 1f); + } + + } +} \ No newline at end of file diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index f8e66c85..2ee1b60a 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -147,7 +147,7 @@ Retro Müzik\'in ses ayarlarını değiştirmesine izin verin Zil sesini ayarla Kara listeyi temizlemek istiyor musunuz? - %1$s'i kaldırmak istiyor musunuz?]]> + %1$s\'i kaldırmak istiyor musunuz?]]> Bağış yapın Çalışmalarımın karşılığı olarak para hakettiğimi düşünüyorsanız bana biraz bahşiş bırakabilirsiniz Bağışlayacağınız tutar: