Fixed tag editing for Android 11+ devices
This commit is contained in:
parent
4eb2f68da5
commit
8e64f117f9
8 changed files with 323 additions and 181 deletions
|
@ -21,28 +21,36 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.ImageView
|
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.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import code.name.monkey.appthemehelper.ThemeStore
|
import code.name.monkey.appthemehelper.ThemeStore
|
||||||
import code.name.monkey.appthemehelper.util.ATHUtil
|
import code.name.monkey.appthemehelper.util.ATHUtil
|
||||||
import code.name.monkey.appthemehelper.util.TintHelper
|
import code.name.monkey.appthemehelper.util.TintHelper
|
||||||
|
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.R.drawable
|
import code.name.monkey.retromusic.R.drawable
|
||||||
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
|
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
|
||||||
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
|
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
|
||||||
import code.name.monkey.retromusic.extensions.accentColor
|
import code.name.monkey.retromusic.extensions.accentColor
|
||||||
import code.name.monkey.retromusic.model.ArtworkInfo
|
import code.name.monkey.retromusic.model.ArtworkInfo
|
||||||
import code.name.monkey.retromusic.model.LoadingInfo
|
import code.name.monkey.retromusic.model.AudioTagInfo
|
||||||
import code.name.monkey.retromusic.repository.Repository
|
import code.name.monkey.retromusic.repository.Repository
|
||||||
import code.name.monkey.retromusic.util.RetroUtil
|
import code.name.monkey.retromusic.util.RetroUtil
|
||||||
import code.name.monkey.retromusic.util.SAFUtil
|
import code.name.monkey.retromusic.util.SAFUtil
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jaudiotagger.audio.AudioFile
|
import org.jaudiotagger.audio.AudioFile
|
||||||
import org.jaudiotagger.audio.AudioFileIO
|
import org.jaudiotagger.audio.AudioFileIO
|
||||||
import org.jaudiotagger.tag.FieldKey
|
import org.jaudiotagger.tag.FieldKey
|
||||||
|
@ -66,9 +74,12 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
private var savedArtworkInfo: ArtworkInfo? = null
|
private var savedArtworkInfo: ArtworkInfo? = null
|
||||||
private var _binding: VB? = null
|
private var _binding: VB? = null
|
||||||
protected val binding: VB get() = _binding!!
|
protected val binding: VB get() = _binding!!
|
||||||
|
private var cacheFiles = listOf<File>()
|
||||||
|
|
||||||
abstract val bindingInflater: (LayoutInflater) -> VB
|
abstract val bindingInflater: (LayoutInflater) -> VB
|
||||||
|
|
||||||
|
private lateinit var launcher: ActivityResultLauncher<IntentSenderRequest>
|
||||||
|
|
||||||
protected abstract fun loadImageFromFile(selectedFile: Uri?)
|
protected abstract fun loadImageFromFile(selectedFile: Uri?)
|
||||||
|
|
||||||
protected val show: AlertDialog
|
protected val show: AlertDialog
|
||||||
|
@ -195,7 +206,6 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
_binding = bindingInflater.invoke(layoutInflater)
|
_binding = bindingInflater.invoke(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setStatusbarColorAuto()
|
|
||||||
setTaskDescriptionColorAuto()
|
setTaskDescriptionColorAuto()
|
||||||
|
|
||||||
saveFab = findViewById(R.id.saveTags)
|
saveFab = findViewById(R.id.saveTags)
|
||||||
|
@ -207,6 +217,11 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
setUpViews()
|
setUpViews()
|
||||||
|
launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
writeToFiles(getSongUris(), cacheFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpViews() {
|
private fun setUpViews() {
|
||||||
|
@ -265,6 +280,8 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
|
|
||||||
protected abstract fun getSongPaths(): List<String>
|
protected abstract fun getSongPaths(): List<String>
|
||||||
|
|
||||||
|
protected abstract fun getSongUris(): List<Uri>
|
||||||
|
|
||||||
protected fun searchWebFor(vararg keys: String) {
|
protected fun searchWebFor(vararg keys: String) {
|
||||||
val stringBuilder = StringBuilder()
|
val stringBuilder = StringBuilder()
|
||||||
for (key in keys) {
|
for (key in keys) {
|
||||||
|
@ -336,23 +353,53 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
|
|
||||||
hideFab()
|
hideFab()
|
||||||
println(fieldKeyValueMap)
|
println(fieldKeyValueMap)
|
||||||
WriteTagsAsyncTask(this).execute(
|
GlobalScope.launch {
|
||||||
LoadingInfo(
|
if (VersionUtils.hasR()) {
|
||||||
songPaths,
|
cacheFiles = TagWriter.writeTagsToFilesR(
|
||||||
fieldKeyValueMap,
|
this@AbsTagEditorActivity, AudioTagInfo(
|
||||||
artworkInfo
|
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>?) {
|
private fun writeTags(paths: List<String>?) {
|
||||||
WriteTagsAsyncTask(this).execute(
|
GlobalScope.launch {
|
||||||
LoadingInfo(
|
if (VersionUtils.hasR()) {
|
||||||
paths,
|
cacheFiles = TagWriter.writeTagsToFilesR(
|
||||||
savedTags,
|
this@AbsTagEditorActivity, AudioTagInfo(
|
||||||
savedArtworkInfo
|
paths,
|
||||||
)
|
savedTags,
|
||||||
)
|
savedArtworkInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
|
||||||
|
|
||||||
|
launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
|
||||||
|
} else {
|
||||||
|
TagWriter.writeTagsToFiles(
|
||||||
|
this@AbsTagEditorActivity, AudioTagInfo(
|
||||||
|
paths,
|
||||||
|
savedTags,
|
||||||
|
savedArtworkInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -391,9 +438,30 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
companion object {
|
||||||
|
|
||||||
const val EXTRA_ID = "extra_id"
|
const val EXTRA_ID = "extra_id"
|
||||||
const val EXTRA_PALETTE = "extra_palette"
|
const val EXTRA_PALETTE = "extra_palette"
|
||||||
private val TAG = AbsTagEditorActivity::class.java.simpleName
|
private val TAG = AbsTagEditorActivity::class.java.simpleName
|
||||||
|
|
|
@ -38,6 +38,7 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
|
||||||
import code.name.monkey.retromusic.model.ArtworkInfo
|
import code.name.monkey.retromusic.model.ArtworkInfo
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.util.ImageUtil
|
import code.name.monkey.retromusic.util.ImageUtil
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil
|
||||||
import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
|
import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
|
||||||
import code.name.monkey.retromusic.util.RetroColorUtil.getColor
|
import code.name.monkey.retromusic.util.RetroColorUtil.getColor
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
@ -193,6 +194,11 @@ class AlbumTagEditorActivity : AbsTagEditorActivity<ActivityAlbumTagEditorBindin
|
||||||
.map(Song::data)
|
.map(Song::data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getSongUris(): List<Uri> = repository.albumById(id).songs.map {
|
||||||
|
MusicUtil.getSongFileUri(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import code.name.monkey.retromusic.databinding.ActivitySongTagEditorBinding
|
||||||
import code.name.monkey.retromusic.extensions.appHandleColor
|
import code.name.monkey.retromusic.extensions.appHandleColor
|
||||||
import code.name.monkey.retromusic.extensions.setTint
|
import code.name.monkey.retromusic.extensions.setTint
|
||||||
import code.name.monkey.retromusic.repository.SongRepository
|
import code.name.monkey.retromusic.repository.SongRepository
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil
|
||||||
import org.jaudiotagger.tag.FieldKey
|
import org.jaudiotagger.tag.FieldKey
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -111,6 +112,8 @@ class SongTagEditorActivity : AbsTagEditorActivity<ActivitySongTagEditorBinding>
|
||||||
|
|
||||||
override fun getSongPaths(): List<String> = listOf(songRepository.song(id).data)
|
override fun getSongPaths(): List<String> = listOf(songRepository.song(id).data)
|
||||||
|
|
||||||
|
override fun getSongUris(): List<Uri> = listOf(MusicUtil.getSongFileUri(id))
|
||||||
|
|
||||||
override fun loadImageFromFile(selectedFile: Uri?) {
|
override fun loadImageFromFile(selectedFile: Uri?) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,203 @@
|
||||||
|
package code.name.monkey.retromusic.activities.tageditor
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener
|
||||||
|
import code.name.monkey.retromusic.model.AudioTagInfo
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil.createAlbumArtFile
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil.deleteAlbumArt
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil.insertAlbumArt
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jaudiotagger.audio.AudioFileIO
|
||||||
|
import org.jaudiotagger.audio.exceptions.CannotReadException
|
||||||
|
import org.jaudiotagger.audio.exceptions.CannotWriteException
|
||||||
|
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException
|
||||||
|
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException
|
||||||
|
import org.jaudiotagger.tag.TagException
|
||||||
|
import org.jaudiotagger.tag.images.Artwork
|
||||||
|
import org.jaudiotagger.tag.images.ArtworkFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
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")
|
||||||
|
Toast.makeText(context, "Scan file from folder", Toast.LENGTH_SHORT).show()
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
var artwork: Artwork? = null
|
||||||
|
var albumArtFile: File? = null
|
||||||
|
if (info.artworkInfo?.artwork != null) {
|
||||||
|
try {
|
||||||
|
albumArtFile = createAlbumArtFile(context).canonicalFile
|
||||||
|
info.artworkInfo.artwork.compress(
|
||||||
|
Bitmap.CompressFormat.PNG,
|
||||||
|
0,
|
||||||
|
FileOutputStream(albumArtFile)
|
||||||
|
)
|
||||||
|
artwork = ArtworkFactory.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)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
scan(context, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
suspend fun writeTagsToFilesR(context: Context, info: AudioTagInfo) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val cacheFiles = mutableListOf<File>()
|
||||||
|
try {
|
||||||
|
var artwork: Artwork? = null
|
||||||
|
var albumArtFile: File? = null
|
||||||
|
if (info.artworkInfo?.artwork != null) {
|
||||||
|
try {
|
||||||
|
albumArtFile = createAlbumArtFile(context).canonicalFile
|
||||||
|
info.artworkInfo.artwork.compress(
|
||||||
|
Bitmap.CompressFormat.PNG,
|
||||||
|
0,
|
||||||
|
FileOutputStream(albumArtFile)
|
||||||
|
)
|
||||||
|
artwork = ArtworkFactory.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)
|
||||||
|
}
|
||||||
|
cacheFiles
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,152 +0,0 @@
|
||||||
package code.name.monkey.retromusic.activities.tageditor;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
import org.jaudiotagger.audio.AudioFile;
|
|
||||||
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.FieldKey;
|
|
||||||
import org.jaudiotagger.tag.Tag;
|
|
||||||
import org.jaudiotagger.tag.TagException;
|
|
||||||
import org.jaudiotagger.tag.images.Artwork;
|
|
||||||
import org.jaudiotagger.tag.images.ArtworkFactory;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import code.name.monkey.retromusic.R;
|
|
||||||
import code.name.monkey.retromusic.misc.DialogAsyncTask;
|
|
||||||
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener;
|
|
||||||
import code.name.monkey.retromusic.model.LoadingInfo;
|
|
||||||
import code.name.monkey.retromusic.util.MusicUtil;
|
|
||||||
|
|
||||||
public class WriteTagsAsyncTask extends DialogAsyncTask<LoadingInfo, Integer, List<String>> {
|
|
||||||
|
|
||||||
public WriteTagsAsyncTask(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> doInBackground(LoadingInfo... params) {
|
|
||||||
try {
|
|
||||||
LoadingInfo info = params[0];
|
|
||||||
|
|
||||||
Artwork artwork = null;
|
|
||||||
File albumArtFile = null;
|
|
||||||
if (info.getArtworkInfo() != null && info.getArtworkInfo().getArtwork() != null) {
|
|
||||||
try {
|
|
||||||
albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile();
|
|
||||||
info.getArtworkInfo().getArtwork().compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile));
|
|
||||||
artwork = ArtworkFactory.createArtworkFromFile(albumArtFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int counter = 0;
|
|
||||||
boolean wroteArtwork = false;
|
|
||||||
boolean deletedArtwork = false;
|
|
||||||
for (String filePath : info.getFilePaths()) {
|
|
||||||
publishProgress(++counter, info.getFilePaths().size());
|
|
||||||
try {
|
|
||||||
AudioFile audioFile = AudioFileIO.read(new File(filePath));
|
|
||||||
Tag tag = audioFile.getTagOrCreateAndSetDefault();
|
|
||||||
|
|
||||||
if (info.getFieldKeyValueMap() != null) {
|
|
||||||
for (Map.Entry<FieldKey, String> entry : info.getFieldKeyValueMap().entrySet()) {
|
|
||||||
try {
|
|
||||||
tag.setField(entry.getKey(), entry.getValue());
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.getArtworkInfo() != null) {
|
|
||||||
if (info.getArtworkInfo().getArtwork() == null) {
|
|
||||||
tag.deleteArtworkField();
|
|
||||||
deletedArtwork = true;
|
|
||||||
} else if (artwork != null) {
|
|
||||||
tag.deleteArtworkField();
|
|
||||||
tag.setField(artwork);
|
|
||||||
wroteArtwork = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audioFile.commit();
|
|
||||||
} catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = getContext();
|
|
||||||
if (context != null) {
|
|
||||||
if (wroteArtwork) {
|
|
||||||
MusicUtil.INSTANCE.
|
|
||||||
insertAlbumArt(context, info.getArtworkInfo().getAlbumId(), albumArtFile.getPath());
|
|
||||||
} else if (deletedArtwork) {
|
|
||||||
MusicUtil.INSTANCE.deleteAlbumArt(context, info.getArtworkInfo().getAlbumId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return info.getFilePaths();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<String> toBeScanned) {
|
|
||||||
super.onPostExecute(toBeScanned);
|
|
||||||
scan(toBeScanned);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCancelled(List<String> toBeScanned) {
|
|
||||||
super.onCancelled(toBeScanned);
|
|
||||||
scan(toBeScanned);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scan(List<String> toBeScanned) {
|
|
||||||
Context context = getContext();
|
|
||||||
if (toBeScanned == null || toBeScanned.isEmpty()) {
|
|
||||||
Log.i("scan", "scan: Empty");
|
|
||||||
Toast.makeText(context, "Scan file from folder", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MediaScannerConnection.scanFile(context, toBeScanned.toArray(new String[0]), null, context instanceof Activity ? new UpdateToastMediaScannerCompletionListener((Activity) context, toBeScanned) : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Dialog createDialog(@NonNull Context context) {
|
|
||||||
return new MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle(R.string.saving_changes)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setView(R.layout.loading)
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) {
|
|
||||||
super.onProgressUpdate(dialog, values);
|
|
||||||
// ((MaterialDialog) dialog).setMaxProgress(values[1]);
|
|
||||||
// ((MaterialDialog) dialog).setProgress(values[0]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,19 +14,22 @@
|
||||||
*/
|
*/
|
||||||
package code.name.monkey.retromusic.fragments.other
|
package code.name.monkey.retromusic.fragments.other
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.transition.Fade
|
import androidx.transition.Fade
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
||||||
|
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.activities.MainActivity
|
import code.name.monkey.retromusic.activities.MainActivity
|
||||||
import code.name.monkey.retromusic.activities.tageditor.WriteTagsAsyncTask
|
import code.name.monkey.retromusic.activities.tageditor.TagWriter
|
||||||
import code.name.monkey.retromusic.databinding.FragmentLyricsBinding
|
import code.name.monkey.retromusic.databinding.FragmentLyricsBinding
|
||||||
import code.name.monkey.retromusic.databinding.FragmentNormalLyricsBinding
|
import code.name.monkey.retromusic.databinding.FragmentNormalLyricsBinding
|
||||||
import code.name.monkey.retromusic.databinding.FragmentSyncedLyricsBinding
|
import code.name.monkey.retromusic.databinding.FragmentSyncedLyricsBinding
|
||||||
|
@ -37,7 +40,7 @@ import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment
|
||||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||||
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
|
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
|
||||||
import code.name.monkey.retromusic.lyrics.LrcView
|
import code.name.monkey.retromusic.lyrics.LrcView
|
||||||
import code.name.monkey.retromusic.model.LoadingInfo
|
import code.name.monkey.retromusic.model.AudioTagInfo
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.util.LyricUtil
|
import code.name.monkey.retromusic.util.LyricUtil
|
||||||
import code.name.monkey.retromusic.util.RetroUtil
|
import code.name.monkey.retromusic.util.RetroUtil
|
||||||
|
@ -107,6 +110,9 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
|
||||||
setupViews()
|
setupViews()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
updateTitleSong()
|
updateTitleSong()
|
||||||
|
if (VersionUtils.hasR()) {
|
||||||
|
binding.editButton.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
|
@ -187,6 +193,7 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
private fun editNormalLyrics() {
|
private fun editNormalLyrics() {
|
||||||
var content = ""
|
var content = ""
|
||||||
val file = File(MusicPlayerRemote.currentSong.data)
|
val file = File(MusicPlayerRemote.currentSong.data)
|
||||||
|
@ -205,11 +212,13 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
|
||||||
) { _, input ->
|
) { _, input ->
|
||||||
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
|
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
|
||||||
fieldKeyValueMap[FieldKey.LYRICS] = input.toString()
|
fieldKeyValueMap[FieldKey.LYRICS] = input.toString()
|
||||||
WriteTagsAsyncTask(requireActivity()).execute(
|
GlobalScope.launch {
|
||||||
LoadingInfo(
|
TagWriter.writeTagsToFiles(
|
||||||
listOf(song.data), fieldKeyValueMap, null
|
requireContext(), AudioTagInfo(
|
||||||
|
listOf(song.data), fieldKeyValueMap, null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
positiveButton(res = R.string.save) {
|
positiveButton(res = R.string.save) {
|
||||||
(lyricsSectionsAdapter.fragments[1].first as NormalLyrics).loadNormalLyrics()
|
(lyricsSectionsAdapter.fragments[1].first as NormalLyrics).loadNormalLyrics()
|
||||||
|
@ -219,6 +228,7 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
private fun editSyncedLyrics() {
|
private fun editSyncedLyrics() {
|
||||||
var lrcFile: File? = null
|
var lrcFile: File? = null
|
||||||
if (LyricUtil.isLrcOriginalFileExist(song.data)) {
|
if (LyricUtil.isLrcOriginalFileExist(song.data)) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package code.name.monkey.retromusic.model
|
||||||
|
|
||||||
import org.jaudiotagger.tag.FieldKey
|
import org.jaudiotagger.tag.FieldKey
|
||||||
|
|
||||||
class LoadingInfo(
|
class AudioTagInfo(
|
||||||
val filePaths: List<String>?,
|
val filePaths: List<String>?,
|
||||||
val fieldKeyValueMap: Map<FieldKey, String>?,
|
val fieldKeyValueMap: Map<FieldKey, String>?,
|
||||||
val artworkInfo: ArtworkInfo?
|
val artworkInfo: ArtworkInfo?
|
|
@ -17,6 +17,7 @@ import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.db.PlaylistEntity
|
import code.name.monkey.retromusic.db.PlaylistEntity
|
||||||
import code.name.monkey.retromusic.db.SongEntity
|
import code.name.monkey.retromusic.db.SongEntity
|
||||||
|
@ -75,15 +76,18 @@ object MusicUtil : KoinComponent {
|
||||||
return if (string2.isNullOrEmpty()) if (string1.isNullOrEmpty()) "" else string1 else "$string1 • $string2"
|
return if (string2.isNullOrEmpty()) if (string1.isNullOrEmpty()) "" else string1 else "$string1 • $string2"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createAlbumArtFile(): File {
|
fun createAlbumArtFile(context: Context): File {
|
||||||
return File(
|
return File(
|
||||||
createAlbumArtDir(),
|
createAlbumArtDir(context),
|
||||||
System.currentTimeMillis().toString()
|
System.currentTimeMillis().toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAlbumArtDir(): File {
|
private fun createAlbumArtDir(context: Context): File {
|
||||||
val albumArtDir = File(Environment.getExternalStorageDirectory(), "/albumthumbs/")
|
val albumArtDir = File(
|
||||||
|
if (VersionUtils.hasR()) context.cacheDir else Environment.getExternalStorageDirectory(),
|
||||||
|
"/albumthumbs/"
|
||||||
|
)
|
||||||
if (!albumArtDir.exists()) {
|
if (!albumArtDir.exists()) {
|
||||||
albumArtDir.mkdirs()
|
albumArtDir.mkdirs()
|
||||||
try {
|
try {
|
||||||
|
@ -520,7 +524,7 @@ object MusicUtil : KoinComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
fun deleteTracksQ(activity: Activity, songs: List<Song>) {
|
fun deleteTracksQ(activity: Activity, songs: List<Song>) {
|
||||||
val pendingIntent = MediaStore.createDeleteRequest(activity.contentResolver, songs.map {
|
val pendingIntent = MediaStore.createDeleteRequest(activity.contentResolver, songs.map {
|
||||||
getSongFileUri(it.id)
|
getSongFileUri(it.id)
|
||||||
|
|
Loading…
Reference in a new issue