Fix up and repackage

This commit is contained in:
JFronny 2022-05-14 15:47:55 +02:00
parent 4df292bddf
commit cde7fd6565
No known key found for this signature in database
GPG key ID: E76429612C2929F4
510 changed files with 2660 additions and 3312 deletions

View file

@ -0,0 +1,262 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities
import android.animation.ObjectAnimator
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.animation.LinearInterpolator
import android.widget.SeekBar
import androidx.lifecycle.lifecycleScope
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsMusicServiceActivity
import io.github.muntashirakon.music.databinding.ActivityDriveModeBinding
import io.github.muntashirakon.music.db.toSongEntity
import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.drawAboveSystemBars
import io.github.muntashirakon.music.fragments.base.AbsPlayerControlsFragment
import io.github.muntashirakon.music.glide.BlurTransformation
import io.github.muntashirakon.music.glide.GlideApp
import io.github.muntashirakon.music.glide.RetroGlideExtension
import io.github.muntashirakon.music.helper.MusicPlayerRemote
import io.github.muntashirakon.music.helper.MusicProgressViewUpdateHelper
import io.github.muntashirakon.music.helper.MusicProgressViewUpdateHelper.Callback
import io.github.muntashirakon.music.helper.PlayPauseButtonOnClickHandler
import io.github.muntashirakon.music.misc.SimpleOnSeekbarChangeListener
import io.github.muntashirakon.music.model.Song
import io.github.muntashirakon.music.repository.RealRepository
import io.github.muntashirakon.music.service.MusicService
import io.github.muntashirakon.music.util.MusicUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
/**
* Created by hemanths on 2020-02-02.
*/
class DriveModeActivity : AbsMusicServiceActivity(), Callback {
private lateinit var binding: ActivityDriveModeBinding
private var lastPlaybackControlsColor: Int = Color.GRAY
private var lastDisabledPlaybackControlsColor: Int = Color.GRAY
private lateinit var progressViewUpdateHelper: MusicProgressViewUpdateHelper
private val repository: RealRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDriveModeBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpMusicControllers()
progressViewUpdateHelper = MusicProgressViewUpdateHelper(this)
lastPlaybackControlsColor = accentColor()
binding.close.setOnClickListener {
onBackPressed()
}
binding.repeatButton.drawAboveSystemBars()
}
private fun setUpMusicControllers() {
setUpPlayPauseFab()
setUpPrevNext()
setUpRepeatButton()
setUpShuffleButton()
setUpProgressSlider()
setupFavouriteToggle()
}
private fun setupFavouriteToggle() {
binding.songFavourite.setOnClickListener {
toggleFavorite(MusicPlayerRemote.currentSong)
}
}
private fun toggleFavorite(song: Song) {
lifecycleScope.launch(Dispatchers.IO) {
val playlist = repository.favoritePlaylist()
val songEntity = song.toSongEntity(playlist.playListId)
val isFavorite = repository.isSongFavorite(song.id)
if (isFavorite) {
repository.removeSongFromPlaylist(songEntity)
} else {
repository.insertSongs(listOf(song.toSongEntity(playlist.playListId)))
}
sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
}
}
private fun updateFavorite() {
lifecycleScope.launch(Dispatchers.IO) {
val isFavorite: Boolean =
repository.isSongFavorite(MusicPlayerRemote.currentSong.id)
withContext(Dispatchers.Main) {
binding.songFavourite.setImageResource(if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border)
}
}
}
private fun setUpProgressSlider() {
binding.progressSlider.setOnSeekBarChangeListener(object : SimpleOnSeekbarChangeListener() {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
MusicPlayerRemote.seekTo(progress)
onUpdateProgressViews(
MusicPlayerRemote.songProgressMillis,
MusicPlayerRemote.songDurationMillis
)
}
}
})
}
override fun onPause() {
super.onPause()
progressViewUpdateHelper.stop()
}
override fun onResume() {
super.onResume()
progressViewUpdateHelper.start()
}
private fun setUpPrevNext() {
binding.nextButton.setOnClickListener { MusicPlayerRemote.playNextSong() }
binding.previousButton.setOnClickListener { MusicPlayerRemote.back() }
}
private fun setUpShuffleButton() {
binding.shuffleButton.setOnClickListener { MusicPlayerRemote.toggleShuffleMode() }
}
private fun setUpRepeatButton() {
binding.repeatButton.setOnClickListener { MusicPlayerRemote.cycleRepeatMode() }
}
private fun setUpPlayPauseFab() {
binding.playPauseButton.setOnClickListener(PlayPauseButtonOnClickHandler())
}
override fun onRepeatModeChanged() {
super.onRepeatModeChanged()
updateRepeatState()
}
override fun onShuffleModeChanged() {
super.onShuffleModeChanged()
updateShuffleState()
}
override fun onPlayStateChanged() {
super.onPlayStateChanged()
updatePlayPauseDrawableState()
}
override fun onServiceConnected() {
super.onServiceConnected()
updatePlayPauseDrawableState()
updateSong()
updateRepeatState()
updateShuffleState()
updateFavorite()
}
private fun updatePlayPauseDrawableState() {
if (MusicPlayerRemote.isPlaying) {
binding.playPauseButton.setImageResource(R.drawable.ic_pause)
} else {
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow)
}
}
fun updateShuffleState() {
when (MusicPlayerRemote.shuffleMode) {
MusicService.SHUFFLE_MODE_SHUFFLE -> binding.shuffleButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
else -> binding.shuffleButton.setColorFilter(
lastDisabledPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
}
private fun updateRepeatState() {
when (MusicPlayerRemote.repeatMode) {
MusicService.REPEAT_MODE_NONE -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat)
binding.repeatButton.setColorFilter(
lastDisabledPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
MusicService.REPEAT_MODE_ALL -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat)
binding.repeatButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
MusicService.REPEAT_MODE_THIS -> {
binding.repeatButton.setImageResource(R.drawable.ic_repeat_one)
binding.repeatButton.setColorFilter(
lastPlaybackControlsColor,
PorterDuff.Mode.SRC_IN
)
}
}
}
override fun onPlayingMetaChanged() {
super.onPlayingMetaChanged()
updateSong()
updateFavorite()
}
override fun onFavoriteStateChanged() {
super.onFavoriteStateChanged()
updateFavorite()
}
private fun updateSong() {
val song = MusicPlayerRemote.currentSong
binding.songTitle.text = song.title
binding.songText.text = song.artistName
GlideApp.with(this)
.load(RetroGlideExtension.getSongModel(song))
.songCoverOptions(song)
.transform(BlurTransformation.Builder(this).build())
.into(binding.image)
}
override fun onUpdateProgressViews(progress: Int, total: Int) {
binding.progressSlider.max = total
val animator = ObjectAnimator.ofInt(binding.progressSlider, "progress", progress)
animator.duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME
animator.interpolator = LinearInterpolator()
animator.start()
binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong())
binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong())
}
}

View file

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

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.activities
import android.graphics.Color
import android.os.Bundle
import android.view.MenuItem
import code.name.monkey.appthemehelper.util.ATHUtil.isWindowBackgroundDark
import code.name.monkey.appthemehelper.util.ColorUtil.lightenColor
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import io.github.muntashirakon.music.activities.base.AbsThemeActivity
import io.github.muntashirakon.music.databinding.ActivityLicenseBinding
import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.drawAboveSystemBars
import io.github.muntashirakon.music.extensions.surfaceColor
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
/** Created by hemanths on 2019-09-27. */
class LicenseActivity : AbsThemeActivity() {
private lateinit var binding: ActivityLicenseBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLicenseBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar)
try {
val buf = StringBuilder()
val json = assets.open("license.html")
BufferedReader(InputStreamReader(json, StandardCharsets.UTF_8)).use { br ->
var str: String?
while (br.readLine().also { str = it } != null) {
buf.append(str)
}
}
// Inject color values for WebView body background and links
val isDark = isWindowBackgroundDark(this)
val backgroundColor = colorToCSS(
surfaceColor(Color.parseColor(if (isDark) "#424242" else "#ffffff"))
)
val contentColor = colorToCSS(Color.parseColor(if (isDark) "#ffffff" else "#000000"))
val changeLog = buf.toString()
.replace(
"{style-placeholder}", String.format(
"body { background-color: %s; color: %s; }", backgroundColor, contentColor
)
)
.replace("{link-color}", colorToCSS(accentColor()))
.replace(
"{link-color-active}",
colorToCSS(
lightenColor(accentColor())
)
)
binding.license.loadData(changeLog, "text/html", "UTF-8")
} catch (e: Throwable) {
binding.license.loadData(
"<h1>Unable to load</h1><p>" + e.localizedMessage + "</p>", "text/html", "UTF-8"
)
}
binding.license.drawAboveSystemBars()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
private fun colorToCSS(color: Int): String {
return String.format(
"rgb(%d, %d, %d)",
Color.red(color),
Color.green(color),
Color.blue(color)
) // on API 29, WebView doesn't load with hex colors
}
}

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities
import android.app.KeyguardManager
import android.os.Bundle
import android.view.WindowManager
import androidx.core.content.getSystemService
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsMusicServiceActivity
import io.github.muntashirakon.music.databinding.ActivityLockScreenBinding
import io.github.muntashirakon.music.extensions.hideStatusBar
import io.github.muntashirakon.music.extensions.setTaskDescriptionColorAuto
import io.github.muntashirakon.music.extensions.whichFragment
import io.github.muntashirakon.music.fragments.player.lockscreen.LockScreenControlsFragment
import io.github.muntashirakon.music.glide.GlideApp
import io.github.muntashirakon.music.glide.RetroGlideExtension
import io.github.muntashirakon.music.glide.RetroMusicColoredTarget
import io.github.muntashirakon.music.helper.MusicPlayerRemote
import io.github.muntashirakon.music.util.color.MediaNotificationProcessor
import com.r0adkll.slidr.Slidr
import com.r0adkll.slidr.model.SlidrConfig
import com.r0adkll.slidr.model.SlidrListener
import com.r0adkll.slidr.model.SlidrPosition
class LockScreenActivity : AbsMusicServiceActivity() {
private lateinit var binding: ActivityLockScreenBinding
private var fragment: LockScreenControlsFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lockScreenInit()
binding = ActivityLockScreenBinding.inflate(layoutInflater)
setContentView(binding.root)
hideStatusBar()
setTaskDescriptionColorAuto()
val config = SlidrConfig.Builder().listener(object : SlidrListener {
override fun onSlideStateChanged(state: Int) {
}
override fun onSlideChange(percent: Float) {
}
override fun onSlideOpened() {
}
override fun onSlideClosed(): Boolean {
if (VersionUtils.hasOreo()) {
val keyguardManager =
getSystemService<KeyguardManager>()
keyguardManager?.requestDismissKeyguard(this@LockScreenActivity, null)
}
finish()
return true
}
}).position(SlidrPosition.BOTTOM).build()
Slidr.attach(this, config)
fragment = whichFragment<LockScreenControlsFragment>(R.id.playback_controls_fragment)
binding.slide.apply {
translationY = 100f
alpha = 0f
animate().translationY(0f).alpha(1f).setDuration(1500).start()
}
}
@Suppress("Deprecation")
private fun lockScreenInit() {
if (VersionUtils.hasOreoMR1()) {
setShowWhenLocked(true)
val keyguardManager = getSystemService<KeyguardManager>()
keyguardManager?.requestDismissKeyguard(this, null)
} else {
this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
)
}
}
override fun onPlayingMetaChanged() {
super.onPlayingMetaChanged()
updateSongs()
}
override fun onServiceConnected() {
super.onServiceConnected()
updateSongs()
}
private fun updateSongs() {
val song = MusicPlayerRemote.currentSong
GlideApp.with(this)
.asBitmapPalette()
.songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.dontAnimate()
.into(object : RetroMusicColoredTarget(binding.image) {
override fun onColorReady(colors: MediaNotificationProcessor) {
fragment?.setColor(colors)
}
})
}
}

View file

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

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities
import android.Manifest
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsMusicServiceActivity
import io.github.muntashirakon.music.databinding.ActivityPermissionBinding
import io.github.muntashirakon.music.extensions.*
class PermissionActivity : AbsMusicServiceActivity() {
private lateinit var binding: ActivityPermissionBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPermissionBinding.inflate(layoutInflater)
setContentView(binding.root)
setStatusBarColorAuto()
setTaskDescriptionColorAuto()
setupTitle()
binding.storagePermission.setButtonClick {
requestPermissions()
}
if (VersionUtils.hasMarshmallow()) {
binding.audioPermission.show()
binding.audioPermission.setButtonClick {
if (!hasAudioPermission()) {
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.data = ("package:" + applicationContext.packageName).toUri()
startActivity(intent)
}
}
}
if (VersionUtils.hasS()) {
binding.bluetoothPermission.show()
binding.bluetoothPermission.setButtonClick {
ActivityCompat.requestPermissions(this,
arrayOf(BLUETOOTH_CONNECT),
PERMISSION_REQUEST)
}
}
binding.finish.accentBackgroundColor()
binding.finish.setOnClickListener {
if (hasPermissions()) {
startActivity(
Intent(this, MainActivity::class.java).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
)
finish()
}
}
}
private fun setupTitle() {
val appName =
getString(R.string.message_welcome,
"<b>Metro</b>")
.parseAsHtml()
binding.appNameText.text = appName
}
override fun onResume() {
super.onResume()
binding.finish.isEnabled = hasStoragePermission()
if (hasStoragePermission()) {
binding.storagePermission.checkImage.isVisible = true
binding.storagePermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
if (VersionUtils.hasMarshmallow()) {
if (hasAudioPermission()) {
binding.audioPermission.checkImage.isVisible = true
binding.audioPermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
}
if (VersionUtils.hasS()) {
if (hasBluetoothPermission()) {
binding.bluetoothPermission.checkImage.isVisible = true
binding.bluetoothPermission.checkImage.imageTintList =
ColorStateList.valueOf(accentColor())
}
}
}
private fun hasStoragePermission(): Boolean {
return ActivityCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
@RequiresApi(Build.VERSION_CODES.S)
private fun hasBluetoothPermission(): Boolean {
return ActivityCompat.checkSelfPermission(this,
BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
}
@RequiresApi(Build.VERSION_CODES.M)
private fun hasAudioPermission(): Boolean {
return Settings.System.canWrite(this)
}
override fun onBackPressed() {
super.onBackPressed()
finishAffinity()
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsBaseActivity
import io.github.muntashirakon.music.appshortcuts.DynamicShortcutManager
import io.github.muntashirakon.music.databinding.ActivitySettingsBinding
import io.github.muntashirakon.music.extensions.*
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.color.ColorCallback
class SettingsActivity : AbsBaseActivity(), ColorCallback, OnThemeChangedListener {
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
val mSavedInstanceState = extra<Bundle>(TAG).value ?: savedInstanceState
super.onCreate(mSavedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setupToolbar()
setPermissionDeniedMessage(getString(R.string.permission_bluetooth_denied))
}
override fun onResume() {
super.onResume()
setNavigationBarColorPreOreo(surfaceColor())
}
private fun setupToolbar() {
applyToolbar(binding.toolbar)
val navController: NavController = findNavController(R.id.contentFrame)
navController.addOnDestinationChangedListener { _, _, _ ->
binding.collapsingToolbarLayout.title =
navController.currentDestination?.let { getStringFromDestination(it) }
}
}
private fun getStringFromDestination(currentDestination: NavDestination): String {
val idRes = when (currentDestination.id) {
R.id.mainSettingsFragment -> R.string.action_settings
R.id.audioSettings -> R.string.pref_header_audio
R.id.imageSettingFragment -> R.string.pref_header_images
R.id.notificationSettingsFragment -> R.string.notification
R.id.nowPlayingSettingsFragment -> R.string.now_playing
R.id.otherSettingsFragment -> R.string.others
R.id.personalizeSettingsFragment -> R.string.personalize
R.id.themeSettingsFragment -> R.string.general_settings_title
R.id.aboutActivity -> R.string.action_about
R.id.backup_fragment -> R.string.backup_restore_title
else -> R.id.action_settings
}
return getString(idRes)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.contentFrame).navigateUp() || super.onSupportNavigateUp()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
}
return super.onOptionsItemSelected(item)
}
override fun getPermissionsToRequest(): Array<String> {
return if (VersionUtils.hasS()) {
arrayOf(BLUETOOTH_CONNECT)
} else {
arrayOf()
}
}
override fun invoke(dialog: MaterialDialog, color: Int) {
ThemeStore.editTheme(this).accentColor(color).commit()
if (VersionUtils.hasNougatMR())
DynamicShortcutManager(this).updateDynamicShortcuts()
restart()
}
override fun onThemeValuesChanged() {
restart()
}
private fun restart() {
val savedInstanceState = Bundle().apply {
onSaveInstanceState(this)
}
finish()
val intent = Intent(this, this::class.java).putExtra(TAG, savedInstanceState)
startActivity(intent)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
companion object {
val TAG: String = SettingsActivity::class.java.simpleName
}
}
interface OnThemeChangedListener {
fun onThemeValuesChanged()
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.provider.MediaStore.Images.Media
import android.view.MenuItem
import androidx.core.net.toUri
import androidx.core.view.drawToBitmap
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import io.github.muntashirakon.music.activities.base.AbsBaseActivity
import io.github.muntashirakon.music.databinding.ActivityShareInstagramBinding
import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.setLightStatusBar
import io.github.muntashirakon.music.extensions.setStatusBarColor
import io.github.muntashirakon.music.glide.GlideApp
import io.github.muntashirakon.music.glide.RetroGlideExtension
import io.github.muntashirakon.music.glide.RetroMusicColoredTarget
import io.github.muntashirakon.music.model.Song
import io.github.muntashirakon.music.util.Share
import io.github.muntashirakon.music.util.color.MediaNotificationProcessor
/**
* Created by hemanths on 2020-02-02.
*/
class ShareInstagramStory : AbsBaseActivity() {
private lateinit var binding: ActivityShareInstagramBinding
companion object {
const val EXTRA_SONG = "extra_song"
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityShareInstagramBinding.inflate(layoutInflater)
setContentView(binding.root)
setStatusBarColor(Color.TRANSPARENT)
binding.toolbar.setBackgroundColor(Color.TRANSPARENT)
setSupportActionBar(binding.toolbar)
val song = intent.extras?.getParcelable<Song>(EXTRA_SONG)
song?.let { songFinal ->
GlideApp.with(this)
.asBitmapPalette()
.songCoverOptions(songFinal)
.load(RetroGlideExtension.getSongModel(songFinal))
.into(object : RetroMusicColoredTarget(binding.image) {
override fun onColorReady(colors: MediaNotificationProcessor) {
val isColorLight = ColorUtil.isColorLight(colors.backgroundColor)
setColors(isColorLight, colors.backgroundColor)
}
})
binding.shareTitle.text = songFinal.title
binding.shareText.text = songFinal.artistName
binding.shareButton.setOnClickListener {
val path: String = Media.insertImage(
contentResolver,
binding.mainContent.drawToBitmap(Bitmap.Config.ARGB_8888),
"Design", null
)
Share.shareStoryToSocial(
this@ShareInstagramStory,
path.toUri()
)
}
}
binding.shareButton.setTextColor(
MaterialValueHelper.getPrimaryTextColor(
this,
ColorUtil.isColorLight(accentColor())
)
)
binding.shareButton.backgroundTintList =
ColorStateList.valueOf(accentColor())
}
private fun setColors(colorLight: Boolean, color: Int) {
setLightStatusBar(colorLight)
binding.toolbar.setTitleTextColor(
MaterialValueHelper.getPrimaryTextColor(
this@ShareInstagramStory,
colorLight
)
)
binding.toolbar.navigationIcon?.setTintList(
ColorStateList.valueOf(
MaterialValueHelper.getPrimaryTextColor(
this@ShareInstagramStory,
colorLight
)
)
)
binding.mainContent.background =
GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(color, Color.BLACK)
)
}
}

View file

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

View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities.base
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.app.ActivityCompat
import androidx.core.content.getSystemService
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.extensions.accentColor
import com.google.android.material.snackbar.Snackbar
abstract class AbsBaseActivity : AbsThemeActivity() {
private var hadPermissions: Boolean = false
private lateinit var permissions: Array<String>
private var permissionDeniedMessage: String? = null
open fun getPermissionsToRequest(): Array<String> {
return arrayOf()
}
protected fun setPermissionDeniedMessage(message: String) {
permissionDeniedMessage = message
}
fun getPermissionDeniedMessage(): String {
return if (permissionDeniedMessage == null) getString(R.string.permissions_denied) else permissionDeniedMessage!!
}
private val snackBarContainer: View
get() = window.decorView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
volumeControlStream = AudioManager.STREAM_MUSIC
permissions = getPermissionsToRequest()
hadPermissions = hasPermissions()
permissionDeniedMessage = null
}
override fun onResume() {
super.onResume()
val hasPermissions = hasPermissions()
if (hasPermissions != hadPermissions) {
hadPermissions = hasPermissions
if (VersionUtils.hasMarshmallow()) {
onHasPermissionsChanged(hasPermissions)
}
}
}
protected open fun onHasPermissionsChanged(hasPermissions: Boolean) {
// implemented by sub classes
println(hasPermissions)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_MENU && event.action == KeyEvent.ACTION_UP) {
showOverflowMenu()
return true
}
return super.dispatchKeyEvent(event)
}
private fun showOverflowMenu() {
}
protected open fun requestPermissions() {
ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST)
}
protected fun hasPermissions(): Boolean {
for (permission in permissions) {
if (ActivityCompat.checkSelfPermission(this,
permission) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST) {
for (grantResult in grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this@AbsBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
// User has deny from permission dialog
Snackbar.make(
snackBarContainer,
permissionDeniedMessage!!,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_grant) { requestPermissions() }
.setActionTextColor(accentColor()).show()
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this@AbsBaseActivity, Manifest.permission.BLUETOOTH_CONNECT
)
) {
// User has deny from permission dialog
Snackbar.make(
snackBarContainer,
R.string.permission_bluetooth_denied,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_grant) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
PERMISSION_REQUEST)
}
.setActionTextColor(accentColor()).show()
} else {
// User has deny permission and checked never show permission dialog so you can redirect to Application settings page
Snackbar.make(
snackBarContainer,
permissionDeniedMessage!!,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.action_settings) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts(
"package",
this@AbsBaseActivity.packageName,
null
)
intent.data = uri
startActivity(intent)
}.setActionTextColor(accentColor()).show()
}
return
}
}
hadPermissions = true
onHasPermissionsChanged(true)
}
}
companion object {
const val PERMISSION_REQUEST = 100
}
// this lets keyboard close when clicked in background
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val v = currentFocus
if (v is EditText) {
val outRect = Rect()
v.getGlobalVisibleRect(outRect)
if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
v.clearFocus()
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
v.windowToken,
0
)
}
}
}
return super.dispatchTouchEvent(event)
}
}

View file

@ -0,0 +1,223 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities.base
import android.Manifest
import android.content.*
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.db.toPlayCount
import io.github.muntashirakon.music.helper.MusicPlayerRemote
import io.github.muntashirakon.music.interfaces.IMusicServiceEventListener
import io.github.muntashirakon.music.repository.RealRepository
import io.github.muntashirakon.music.service.MusicService.Companion.FAVORITE_STATE_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.MEDIA_STORE_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.META_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.PLAY_STATE_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.QUEUE_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.REPEAT_MODE_CHANGED
import io.github.muntashirakon.music.service.MusicService.Companion.SHUFFLE_MODE_CHANGED
import io.github.muntashirakon.music.util.PreferenceUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.lang.ref.WeakReference
abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventListener {
private val mMusicServiceEventListeners = ArrayList<IMusicServiceEventListener>()
private val repository: RealRepository by inject()
private var serviceToken: MusicPlayerRemote.ServiceToken? = null
private var musicStateReceiver: MusicStateReceiver? = null
private var receiverRegistered: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
serviceToken = MusicPlayerRemote.bindToService(this, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
this@AbsMusicServiceActivity.onServiceConnected()
}
override fun onServiceDisconnected(name: ComponentName) {
this@AbsMusicServiceActivity.onServiceDisconnected()
}
})
setPermissionDeniedMessage(getString(R.string.permission_external_storage_denied))
}
override fun onDestroy() {
super.onDestroy()
MusicPlayerRemote.unbindFromService(serviceToken)
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver)
receiverRegistered = false
}
}
fun addMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
if (listenerI != null) {
mMusicServiceEventListeners.add(listenerI)
}
}
fun removeMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
if (listenerI != null) {
mMusicServiceEventListeners.remove(listenerI)
}
}
override fun onServiceConnected() {
if (!receiverRegistered) {
musicStateReceiver = MusicStateReceiver(this)
val filter = IntentFilter()
filter.addAction(PLAY_STATE_CHANGED)
filter.addAction(SHUFFLE_MODE_CHANGED)
filter.addAction(REPEAT_MODE_CHANGED)
filter.addAction(META_CHANGED)
filter.addAction(QUEUE_CHANGED)
filter.addAction(MEDIA_STORE_CHANGED)
filter.addAction(FAVORITE_STATE_CHANGED)
registerReceiver(musicStateReceiver, filter)
receiverRegistered = true
}
for (listener in mMusicServiceEventListeners) {
listener.onServiceConnected()
}
}
override fun onServiceDisconnected() {
if (receiverRegistered) {
unregisterReceiver(musicStateReceiver)
receiverRegistered = false
}
for (listener in mMusicServiceEventListeners) {
listener.onServiceDisconnected()
}
}
override fun onPlayingMetaChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onPlayingMetaChanged()
}
lifecycleScope.launch(Dispatchers.IO) {
val entity = repository.songPresentInHistory(MusicPlayerRemote.currentSong)
if (entity != null) {
repository.updateHistorySong(MusicPlayerRemote.currentSong)
} else {
// Check whether pause history option is ON or OFF
if (!PreferenceUtil.pauseHistory) {
repository.addSongToHistory(MusicPlayerRemote.currentSong)
}
}
val songs = repository.checkSongExistInPlayCount(MusicPlayerRemote.currentSong.id)
if (songs.isNotEmpty()) {
repository.updateSongInPlayCount(songs.first().apply {
playCount += 1
})
} else {
repository.insertSongInPlayCount(MusicPlayerRemote.currentSong.toPlayCount())
}
}
}
override fun onQueueChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onQueueChanged()
}
}
override fun onPlayStateChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onPlayStateChanged()
}
}
override fun onMediaStoreChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onMediaStoreChanged()
}
}
override fun onRepeatModeChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onRepeatModeChanged()
}
}
override fun onShuffleModeChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onShuffleModeChanged()
}
}
override fun onFavoriteStateChanged() {
for (listener in mMusicServiceEventListeners) {
listener.onFavoriteStateChanged()
}
}
override fun onHasPermissionsChanged(hasPermissions: Boolean) {
super.onHasPermissionsChanged(hasPermissions)
val intent = Intent(MEDIA_STORE_CHANGED)
intent.putExtra(
"from_permissions_changed",
true
) // just in case we need to know this at some point
sendBroadcast(intent)
println("sendBroadcast $hasPermissions")
}
override fun getPermissionsToRequest(): Array<String> {
return mutableListOf(Manifest.permission.READ_EXTERNAL_STORAGE).apply {
if (!VersionUtils.hasQ()) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
private class MusicStateReceiver(activity: AbsMusicServiceActivity) : BroadcastReceiver() {
private val reference: WeakReference<AbsMusicServiceActivity> = WeakReference(activity)
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
val activity = reference.get()
if (activity != null && action != null) {
when (action) {
FAVORITE_STATE_CHANGED -> activity.onFavoriteStateChanged()
META_CHANGED -> activity.onPlayingMetaChanged()
QUEUE_CHANGED -> activity.onQueueChanged()
PLAY_STATE_CHANGED -> activity.onPlayStateChanged()
REPEAT_MODE_CHANGED -> activity.onRepeatModeChanged()
SHUFFLE_MODE_CHANGED -> activity.onShuffleModeChanged()
MEDIA_STORE_CHANGED -> activity.onMediaStoreChanged()
}
}
}
}
companion object {
val TAG: String = AbsMusicServiceActivity::class.java.simpleName
}
}

View file

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

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities.base
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.View
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.core.os.ConfigurationCompat
import code.name.monkey.appthemehelper.common.ATHToolbarActivity
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.LanguageContextWrapper
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.extensions.*
import io.github.muntashirakon.music.util.PreferenceUtil
import io.github.muntashirakon.music.util.theme.getNightMode
import io.github.muntashirakon.music.util.theme.getThemeResValue
import java.util.*
abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
updateTheme()
hideStatusBar()
super.onCreate(savedInstanceState)
setEdgeToEdgeOrImmersive()
registerSystemUiVisibility()
toggleScreenOn()
setLightNavigationBarAuto()
setLightStatusBarAuto(surfaceColor())
if (VersionUtils.hasQ()) {
window.decorView.isForceDarkAllowed = false
}
}
private fun updateTheme() {
setTheme(getThemeResValue())
if (PreferenceUtil.materialYou) {
setDefaultNightMode(getNightMode())
}
if (PreferenceUtil.isCustomFont) {
setTheme(R.style.FontThemeOverlay)
}
if (PreferenceUtil.circlePlayButton) {
setTheme(R.style.CircleFABOverlay)
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideStatusBar()
handler.removeCallbacks(this)
handler.postDelayed(this, 300)
} else {
handler.removeCallbacks(this)
}
}
private fun registerSystemUiVisibility() {
val decorView = window.decorView
decorView.setOnSystemUiVisibilityChangeListener { visibility ->
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
setImmersiveFullscreen()
}
}
}
private fun unregisterSystemUiVisibility() {
val decorView = window.decorView
decorView.setOnSystemUiVisibilityChangeListener(null)
}
override fun run() {
setImmersiveFullscreen()
}
override fun onStop() {
handler.removeCallbacks(this)
super.onStop()
}
public override fun onDestroy() {
super.onDestroy()
unregisterSystemUiVisibility()
exitFullscreen()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
handler.removeCallbacks(this)
handler.postDelayed(this, 500)
}
return super.onKeyDown(keyCode, event)
}
override fun attachBaseContext(newBase: Context?) {
val code = PreferenceUtil.languageCode
val locale = if (code == "auto") {
// Get the device default locale
ConfigurationCompat.getLocales(Resources.getSystem().configuration)[0]
} else {
Locale.forLanguageTag(code)
}
super.attachBaseContext(LanguageContextWrapper.wrap(newBase, locale))
}
}

View file

@ -0,0 +1,316 @@
/*
* Copyright (c) 2020 Hemanth Savarla.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.activities.bugreport
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringDef
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.MaterialUtil
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsThemeActivity
import io.github.muntashirakon.music.activities.bugreport.model.DeviceInfo
import io.github.muntashirakon.music.activities.bugreport.model.Report
import io.github.muntashirakon.music.activities.bugreport.model.github.ExtraInfo
import io.github.muntashirakon.music.activities.bugreport.model.github.GithubLogin
import io.github.muntashirakon.music.activities.bugreport.model.github.GithubTarget
import io.github.muntashirakon.music.databinding.ActivityBugReportBinding
import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.setTaskDescriptionColorAuto
import io.github.muntashirakon.music.extensions.showToast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.egit.github.core.Issue
import org.eclipse.egit.github.core.client.GitHubClient
import org.eclipse.egit.github.core.client.RequestException
import org.eclipse.egit.github.core.service.IssueService
import java.io.IOException
private const val RESULT_SUCCESS = "RESULT_OK"
private const val RESULT_BAD_CREDENTIALS = "RESULT_BAD_CREDENTIALS"
private const val RESULT_INVALID_TOKEN = "RESULT_INVALID_TOKEN"
private const val RESULT_ISSUES_NOT_ENABLED = "RESULT_ISSUES_NOT_ENABLED"
private const val RESULT_UNKNOWN = "RESULT_UNKNOWN"
@StringDef(
RESULT_SUCCESS,
RESULT_BAD_CREDENTIALS,
RESULT_INVALID_TOKEN,
RESULT_ISSUES_NOT_ENABLED,
RESULT_UNKNOWN
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Result
open class BugReportActivity : AbsThemeActivity() {
private lateinit var binding: ActivityBugReportBinding
private var deviceInfo: DeviceInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBugReportBinding.inflate(layoutInflater)
setContentView(binding.root)
setTaskDescriptionColorAuto()
initViews()
if (title.isNullOrEmpty()) setTitle(R.string.report_an_issue)
deviceInfo = DeviceInfo(this)
binding.cardDeviceInfo.airTextDeviceInfo.text = deviceInfo.toString()
}
private fun initViews() {
val accentColor = accentColor()
setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
TintHelper.setTintAuto(binding.cardReport.optionUseAccount, accentColor, false)
binding.cardReport.optionUseAccount.setOnClickListener {
binding.cardReport.inputTitle.isEnabled = true
binding.cardReport.inputDescription.isEnabled = true
binding.cardReport.inputUsername.isEnabled = true
binding.cardReport.inputPassword.isEnabled = true
binding.cardReport.optionAnonymous.isChecked = false
binding.sendFab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
binding.sendFab.setImageResource(R.drawable.ic_send)
binding.sendFab.show()
}
})
}
TintHelper.setTintAuto(binding.cardReport.optionAnonymous, accentColor, false)
binding.cardReport.optionAnonymous.setOnClickListener {
binding.cardReport.inputTitle.isEnabled = false
binding.cardReport.inputDescription.isEnabled = false
binding.cardReport.inputUsername.isEnabled = false
binding.cardReport.inputPassword.isEnabled = false
binding.cardReport.optionUseAccount.isChecked = false
binding.sendFab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
binding.sendFab.setImageResource(R.drawable.ic_open_in_browser)
binding.sendFab.show()
}
})
}
binding.cardReport.inputPassword.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
reportIssue()
return@setOnEditorActionListener true
}
false
}
binding.cardDeviceInfo.airTextDeviceInfo.setOnClickListener { copyDeviceInfoToClipBoard() }
TintHelper.setTintAuto(binding.sendFab, accentColor, true)
binding.sendFab.setOnClickListener { reportIssue() }
MaterialUtil.setTint(binding.cardReport.inputLayoutTitle, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutDescription, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutUsername, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutPassword, false)
}
private fun reportIssue() {
if (binding.cardReport.optionUseAccount.isChecked) {
if (!validateInput()) return
val username = binding.cardReport.inputUsername.text.toString()
val password = binding.cardReport.inputPassword.text.toString()
sendBugReport(GithubLogin(username, password))
} else {
copyDeviceInfoToClipBoard()
val i = Intent(Intent.ACTION_VIEW)
i.data = ISSUE_TRACKER_LINK.toUri()
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i)
}
}
private fun copyDeviceInfoToClipBoard() {
val clipboard = getSystemService<ClipboardManager>()
val clip = ClipData.newPlainText(getString(R.string.device_info), deviceInfo?.toMarkdown())
clipboard?.setPrimaryClip(clip)
showToast(R.string.copied_device_info_to_clipboard)
}
private fun validateInput(): Boolean {
var hasErrors = false
if (binding.cardReport.optionUseAccount.isChecked) {
if (binding.cardReport.inputUsername.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutUsername, R.string.bug_report_no_username)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutUsername)
}
if (binding.cardReport.inputPassword.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutPassword, R.string.bug_report_no_password)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutPassword)
}
}
if (binding.cardReport.inputTitle.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutTitle, R.string.bug_report_no_title)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutTitle)
}
if (binding.cardReport.inputDescription.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutDescription, R.string.bug_report_no_description)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutDescription)
}
return !hasErrors
}
private fun setError(editTextLayout: TextInputLayout, @StringRes errorRes: Int) {
editTextLayout.error = getString(errorRes)
}
private fun removeError(editTextLayout: TextInputLayout) {
editTextLayout.error = null
}
private fun sendBugReport(login: GithubLogin) {
if (!validateInput()) return
val bugTitle = binding.cardReport.inputTitle.text.toString()
val bugDescription = binding.cardReport.inputDescription.text.toString()
val extraInfo = ExtraInfo()
onSaveExtraInfo()
val report = Report(bugTitle, bugDescription, deviceInfo, extraInfo)
val target = GithubTarget("RetroMusicPlayer", "RetroMusicPlayer")
reportIssue(report, target, login)
}
private fun onSaveExtraInfo() {}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
}
return super.onOptionsItemSelected(item)
}
private fun reportIssue(
report: Report,
target: GithubTarget,
login: GithubLogin
) {
val client: GitHubClient = if (login.shouldUseApiToken()) {
GitHubClient().setOAuth2Token(login.apiToken)
} else {
GitHubClient().setCredentials(login.username, login.password)
}
val issue = Issue().setTitle(report.title).setBody(report.getDescription())
lifecycleScope.launch(Dispatchers.IO) {
val result = try {
IssueService(client).createIssue(target.username, target.repository, issue)
RESULT_SUCCESS
} catch (e: RequestException) {
when (e.status) {
STATUS_BAD_CREDENTIALS -> {
if (login.shouldUseApiToken()) RESULT_INVALID_TOKEN else RESULT_BAD_CREDENTIALS
}
STATUS_ISSUES_NOT_ENABLED -> RESULT_ISSUES_NOT_ENABLED
else -> {
RESULT_UNKNOWN
throw e
}
}
} catch (e: IOException) {
e.printStackTrace()
RESULT_UNKNOWN
}
withContext(Dispatchers.Main) {
val activity = this@BugReportActivity
when (result) {
RESULT_SUCCESS -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_success)
.setPositiveButton(android.R.string.ok) { _, _ -> tryToFinishActivity() }
.show()
RESULT_BAD_CREDENTIALS -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_wrong_credentials)
.setPositiveButton(android.R.string.ok, null)
.show()
RESULT_INVALID_TOKEN -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_invalid_token)
.setPositiveButton(android.R.string.ok, null)
.show()
RESULT_ISSUES_NOT_ENABLED -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_issues_not_available)
.setPositiveButton(android.R.string.ok, null)
.show()
else -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_unknown)
.setPositiveButton(android.R.string.ok) { _, _ -> tryToFinishActivity() }
.setNegativeButton(android.R.string.cancel) { _, _ -> tryToFinishActivity() }
.show()
}
}
}
}
private fun tryToFinishActivity() {
if (!isFinishing) {
finish()
}
}
companion object {
private const val STATUS_BAD_CREDENTIALS = 401
private const val STATUS_ISSUES_NOT_ENABLED = 410
private const val ISSUE_TRACKER_LINK =
"https://github.com/RetroMusicPlayer/RetroMusicPlayer"
}
}

View file

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

View file

@ -0,0 +1,22 @@
package io.github.muntashirakon.music.activities.bugreport.model
import io.github.muntashirakon.music.activities.bugreport.model.github.ExtraInfo
class Report(
val title: String,
private val description: String,
private val deviceInfo: DeviceInfo?,
private val extraInfo: ExtraInfo
) {
fun getDescription(): String {
return """
$description
-
${deviceInfo?.toMarkdown()}
${extraInfo.toMarkdown()}
""".trimIndent()
}
}

View file

@ -0,0 +1,61 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
class ExtraInfo {
private val extraInfo: MutableMap<String, String> = LinkedHashMap()
fun put(key: String, value: String) {
extraInfo[key] = value
}
fun put(key: String, value: Boolean) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Double) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Float) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Long) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Int) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Any) {
extraInfo[key] = value.toString()
}
fun remove(key: String) {
extraInfo.remove(key)
}
fun toMarkdown(): String {
if (extraInfo.isEmpty()) {
return ""
}
val output = StringBuilder()
output.append(
"""
Extra info:
---
<table>
""".trimIndent()
)
for (key in extraInfo.keys) {
output
.append("<tr><td>")
.append(key)
.append("</td><td>")
.append(extraInfo[key])
.append("</td></tr>\n")
}
output.append("</table>\n")
return output.toString()
}
}

View file

@ -0,0 +1,25 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
import android.text.TextUtils
class GithubLogin {
val apiToken: String?
val password: String?
val username: String?
constructor(username: String?, password: String?) {
this.username = username
this.password = password
apiToken = null
}
constructor(apiToken: String?) {
username = null
password = null
this.apiToken = apiToken
}
fun shouldUseApiToken(): Boolean {
return TextUtils.isEmpty(username) || TextUtils.isEmpty(password)
}
}

View file

@ -0,0 +1,3 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
class GithubTarget(val username: String, val repository: String)

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.activities.saf;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.heinrichreimersoftware.materialintro.app.IntroActivity;
import com.heinrichreimersoftware.materialintro.slide.SimpleSlide;
import io.github.muntashirakon.music.R;
/** Created by hemanths on 2019-07-31. */
public class SAFGuideActivity extends IntroActivity {
public static final int REQUEST_CODE_SAF_GUIDE = 98;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setButtonCtaVisible(false);
setButtonNextVisible(false);
setButtonBackVisible(false);
setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT);
String title =
String.format(getString(R.string.saf_guide_slide1_title), getString(R.string.app_name));
addSlide(
new SimpleSlide.Builder()
.title(title)
.description(
Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1
? R.string.saf_guide_slide1_description_before_o
: R.string.saf_guide_slide1_description)
.image(R.drawable.saf_guide_1)
.background(R.color.md_deep_purple_300)
.backgroundDark(R.color.md_deep_purple_400)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
addSlide(
new SimpleSlide.Builder()
.title(R.string.saf_guide_slide2_title)
.description(R.string.saf_guide_slide2_description)
.image(R.drawable.saf_guide_2)
.background(R.color.md_deep_purple_500)
.backgroundDark(R.color.md_deep_purple_600)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
addSlide(
new SimpleSlide.Builder()
.title(R.string.saf_guide_slide3_title)
.description(R.string.saf_guide_slide3_description)
.image(R.drawable.saf_guide_3)
.background(R.color.md_deep_purple_700)
.backgroundDark(R.color.md_deep_purple_800)
.layout(R.layout.fragment_simple_slide_large_image)
.build());
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021 Bartlomiej Uliasz.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.activities.saf
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import io.github.muntashirakon.music.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE
import io.github.muntashirakon.music.util.SAFUtil
/** Created by buliasz on 2021-02-07. */
class SAFRequestActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, SAFGuideActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_SAF_GUIDE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
when (requestCode) {
REQUEST_CODE_SAF_GUIDE -> {
SAFUtil.openTreePicker(this)
}
SAFUtil.REQUEST_SAF_PICK_TREE -> {
if (resultCode == RESULT_OK) {
SAFUtil.saveTreeUri(this, intent)
}
finish()
}
}
}
}

View file

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

View file

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

View file

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

View file

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