Fixed tag editing for Android 11+ devices

main
Prathamesh More 2021-11-22 15:31:33 +05:30
parent 4eb2f68da5
commit 8e64f117f9
8 changed files with 323 additions and 181 deletions

View File

@ -21,28 +21,36 @@ 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.ATHUtil
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.drawable
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
import code.name.monkey.retromusic.extensions.accentColor
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.util.RetroUtil
import code.name.monkey.retromusic.util.SAFUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jaudiotagger.audio.AudioFile
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
@ -66,9 +74,12 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
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
@ -195,7 +206,6 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
super.onCreate(savedInstanceState)
_binding = bindingInflater.invoke(layoutInflater)
setContentView(binding.root)
setStatusbarColorAuto()
setTaskDescriptionColorAuto()
saveFab = findViewById(R.id.saveTags)
@ -207,6 +217,11 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
finish()
}
setUpViews()
launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
writeToFiles(getSongUris(), cacheFiles)
}
}
}
private fun setUpViews() {
@ -265,6 +280,8 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
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) {
@ -336,23 +353,53 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
hideFab()
println(fieldKeyValueMap)
WriteTagsAsyncTask(this).execute(
LoadingInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
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>?) {
WriteTagsAsyncTask(this).execute(
LoadingInfo(
paths,
savedTags,
savedArtworkInfo
)
)
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
)
)
}
}
}
@ -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 {
const val EXTRA_ID = "extra_id"
const val EXTRA_PALETTE = "extra_palette"
private val TAG = AbsTagEditorActivity::class.java.simpleName

View File

@ -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.Song
import code.name.monkey.retromusic.util.ImageUtil
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
import code.name.monkey.retromusic.util.RetroColorUtil.getColor
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -193,6 +194,11 @@ class AlbumTagEditorActivity : AbsTagEditorActivity<ActivityAlbumTagEditorBindin
.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) {
}

View File

@ -27,6 +27,7 @@ import code.name.monkey.retromusic.databinding.ActivitySongTagEditorBinding
import code.name.monkey.retromusic.extensions.appHandleColor
import code.name.monkey.retromusic.extensions.setTint
import code.name.monkey.retromusic.repository.SongRepository
import code.name.monkey.retromusic.util.MusicUtil
import org.jaudiotagger.tag.FieldKey
import org.koin.android.ext.android.inject
import java.util.*
@ -111,6 +112,8 @@ class SongTagEditorActivity : AbsTagEditorActivity<ActivitySongTagEditorBinding>
override fun getSongPaths(): List<String> = listOf(songRepository.song(id).data)
override fun getSongUris(): List<Uri> = listOf(MusicUtil.getSongFileUri(id))
override fun loadImageFromFile(selectedFile: Uri?) {
}

View File

@ -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()
}
}
}
}

View File

@ -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]);
}
}

View File

@ -14,19 +14,22 @@
*/
package code.name.monkey.retromusic.fragments.other
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputType
import android.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.transition.Fade
import androidx.viewpager2.adapter.FragmentStateAdapter
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.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.FragmentNormalLyricsBinding
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.MusicProgressViewUpdateHelper
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.util.LyricUtil
import code.name.monkey.retromusic.util.RetroUtil
@ -107,6 +110,9 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
setupViews()
setupToolbar()
updateTitleSong()
if (VersionUtils.hasR()) {
binding.editButton.isVisible = false
}
}
private fun setupViews() {
@ -187,6 +193,7 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
}
@SuppressLint("CheckResult")
private fun editNormalLyrics() {
var content = ""
val file = File(MusicPlayerRemote.currentSong.data)
@ -205,11 +212,13 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
) { _, input ->
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
fieldKeyValueMap[FieldKey.LYRICS] = input.toString()
WriteTagsAsyncTask(requireActivity()).execute(
LoadingInfo(
listOf(song.data), fieldKeyValueMap, null
GlobalScope.launch {
TagWriter.writeTagsToFiles(
requireContext(), AudioTagInfo(
listOf(song.data), fieldKeyValueMap, null
)
)
)
}
}
positiveButton(res = R.string.save) {
(lyricsSectionsAdapter.fragments[1].first as NormalLyrics).loadNormalLyrics()
@ -219,6 +228,7 @@ class LyricsFragment : AbsMusicServiceFragment(R.layout.fragment_lyrics) {
}
@SuppressLint("CheckResult")
private fun editSyncedLyrics() {
var lrcFile: File? = null
if (LyricUtil.isLrcOriginalFileExist(song.data)) {

View File

@ -2,7 +2,7 @@ package code.name.monkey.retromusic.model
import org.jaudiotagger.tag.FieldKey
class LoadingInfo(
class AudioTagInfo(
val filePaths: List<String>?,
val fieldKeyValueMap: Map<FieldKey, String>?,
val artworkInfo: ArtworkInfo?

View File

@ -17,6 +17,7 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
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"
}
fun createAlbumArtFile(): File {
fun createAlbumArtFile(context: Context): File {
return File(
createAlbumArtDir(),
createAlbumArtDir(context),
System.currentTimeMillis().toString()
)
}
private fun createAlbumArtDir(): File {
val albumArtDir = File(Environment.getExternalStorageDirectory(), "/albumthumbs/")
private fun createAlbumArtDir(context: Context): File {
val albumArtDir = File(
if (VersionUtils.hasR()) context.cacheDir else Environment.getExternalStorageDirectory(),
"/albumthumbs/"
)
if (!albumArtDir.exists()) {
albumArtDir.mkdirs()
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>) {
val pendingIntent = MediaStore.createDeleteRequest(activity.contentResolver, songs.map {
getSongFileUri(it.id)