Merge pull request #1169 from prathameshmm02/dev
Support for scoped storage and better compatibility with Android 12main
commit
b2970a6185
|
@ -9,14 +9,14 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 31
|
||||
|
||||
renderscriptTargetApi 29//must match target sdk and build tools
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
applicationId "code.name.monkey.retromusic"
|
||||
versionCode 10519
|
||||
versionName '5.0.0' + "_" + getDate()
|
||||
versionCode 10537
|
||||
versionName '5.2.1 ' + "_" + getDate()
|
||||
|
||||
buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"")
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
tools:targetApi="m">
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
|
@ -144,6 +145,7 @@
|
|||
|
||||
<activity
|
||||
android:name=".cast.ExpandedControlsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
|
@ -176,13 +178,17 @@
|
|||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".service.MediaButtonIntentReceiver">
|
||||
<receiver
|
||||
android:name=".service.MediaButtonIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".appwidgets.BootReceiver">
|
||||
<receiver
|
||||
android:name=".appwidgets.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
|
@ -191,7 +197,7 @@
|
|||
|
||||
<receiver
|
||||
android:name=".appwidgets.AppWidgetBig"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_widget_big_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
@ -204,7 +210,7 @@
|
|||
|
||||
<receiver
|
||||
android:name=".appwidgets.AppWidgetClassic"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_widget_classic_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
@ -216,7 +222,7 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".appwidgets.AppWidgetSmall"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_widget_small_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
@ -228,7 +234,7 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".appwidgets.AppWidgetText"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_widget_text_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
@ -240,7 +246,7 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".appwidgets.AppWidgetCard"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_widget_card_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
@ -274,17 +280,6 @@
|
|||
android:name="com.android.vending.splits.required"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Android Auto -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application.theme"
|
||||
android:resource="@style/CarTheme" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@drawable/ic_notification" />
|
||||
|
||||
<!-- ChromeCast -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -74,7 +74,7 @@ class AppWidgetBig : BaseAppWidget() {
|
|||
)
|
||||
)
|
||||
appWidgetView.setImageViewBitmap(
|
||||
R.id.button_toggle_play_pause, BaseAppWidget.Companion.createBitmap(
|
||||
R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(
|
||||
RetroUtil.getTintedVectorDrawable(
|
||||
context,
|
||||
R.drawable.ic_play_arrow_white_32dp,
|
||||
|
@ -202,13 +202,13 @@ class AppWidgetBig : BaseAppWidget() {
|
|||
MainActivity.EXPAND_PANEL,
|
||||
PreferenceUtil.isExpandPanel
|
||||
)
|
||||
var pendingIntent: PendingIntent
|
||||
|
||||
val serviceName = ComponentName(context, MusicService::class.java)
|
||||
|
||||
// Home
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
pendingIntent = PendingIntent.getActivity(context, 0, action, 0)
|
||||
var pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
views.setOnClickPendingIntent(R.id.clickable_area, pendingIntent)
|
||||
|
||||
// Previous track
|
||||
|
|
|
@ -141,11 +141,11 @@ class AppWidgetCard : BaseAppWidget() {
|
|||
|
||||
if (imageSize == 0) {
|
||||
imageSize =
|
||||
service.resources.getDimensionPixelSize(code.name.monkey.retromusic.R.dimen.app_widget_card_image_size)
|
||||
service.resources.getDimensionPixelSize(R.dimen.app_widget_card_image_size)
|
||||
}
|
||||
if (cardRadius == 0f) {
|
||||
cardRadius =
|
||||
service.resources.getDimension(code.name.monkey.retromusic.R.dimen.app_widget_card_radius)
|
||||
service.resources.getDimension(R.dimen.app_widget_card_radius)
|
||||
}
|
||||
|
||||
// Load the album cover async and push the update on completion
|
||||
|
@ -225,13 +225,13 @@ class AppWidgetCard : BaseAppWidget() {
|
|||
MainActivity.EXPAND_PANEL,
|
||||
PreferenceUtil.isExpandPanel
|
||||
)
|
||||
var pendingIntent: PendingIntent
|
||||
|
||||
val serviceName = ComponentName(context, MusicService::class.java)
|
||||
|
||||
// Home
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
pendingIntent = PendingIntent.getActivity(context, 0, action, 0)
|
||||
var pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
views.setOnClickPendingIntent(R.id.image, pendingIntent)
|
||||
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
|
||||
|
||||
|
|
|
@ -216,13 +216,12 @@ class AppWidgetClassic : BaseAppWidget() {
|
|||
MainActivity.EXPAND_PANEL,
|
||||
PreferenceUtil.isExpandPanel
|
||||
)
|
||||
var pendingIntent: PendingIntent
|
||||
|
||||
val serviceName = ComponentName(context, MusicService::class.java)
|
||||
|
||||
// Home
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
pendingIntent = PendingIntent.getActivity(context, 0, action, 0)
|
||||
var pendingIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
views.setOnClickPendingIntent(R.id.image, pendingIntent)
|
||||
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
|
||||
|
||||
|
|
|
@ -201,13 +201,13 @@ class AppWidgetSmall : BaseAppWidget() {
|
|||
MainActivity.EXPAND_PANEL,
|
||||
PreferenceUtil.isExpandPanel
|
||||
)
|
||||
var pendingIntent: PendingIntent
|
||||
|
||||
val serviceName = ComponentName(context, MusicService::class.java)
|
||||
|
||||
// Home
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
pendingIntent = PendingIntent.getActivity(context, 0, action, 0)
|
||||
var pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
views.setOnClickPendingIntent(R.id.image, pendingIntent)
|
||||
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import android.app.PendingIntent
|
|||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.MediaStore
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
|
@ -83,13 +84,12 @@ class AppWidgetText : BaseAppWidget() {
|
|||
MainActivity.EXPAND_PANEL,
|
||||
PreferenceUtil.isExpandPanel
|
||||
)
|
||||
var pendingIntent: PendingIntent
|
||||
|
||||
val serviceName = ComponentName(context, MusicService::class.java)
|
||||
|
||||
// Home
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
pendingIntent = PendingIntent.getActivity(context, 0, action, 0)
|
||||
var pendingIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
views.setOnClickPendingIntent(R.id.image, pendingIntent)
|
||||
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
|
||||
|
||||
|
|
|
@ -97,9 +97,9 @@ abstract class BaseAppWidget : AppWidgetProvider() {
|
|||
val intent = Intent(action)
|
||||
intent.component = serviceName
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PendingIntent.getForegroundService(context, 0, intent, 0)
|
||||
PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, 0, intent, 0)
|
||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Bundle
|
|||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||
import code.name.monkey.retromusic.EXTRA_SONG
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
|
||||
|
@ -89,7 +90,11 @@ class DeleteSongsDialog : DialogFragment() {
|
|||
if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
|
||||
MusicPlayerRemote.playNextSong()
|
||||
}
|
||||
if (!SAFUtil.isSAFRequiredForSongs(songs)) {
|
||||
if (VersionUtils.hasQ()) {
|
||||
dismiss()
|
||||
MusicUtil.deleteTracksQ(requireActivity(), songs)
|
||||
reloadTabs()
|
||||
} else if (!SAFUtil.isSAFRequiredForSongs(songs)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
dismiss()
|
||||
MusicUtil.deleteTracks(requireContext(), songs)
|
||||
|
|
|
@ -21,13 +21,16 @@ import android.widget.Toast
|
|||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||
import code.name.monkey.retromusic.App
|
||||
import code.name.monkey.retromusic.EXTRA_PLAYLIST
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.db.PlaylistWithSongs
|
||||
import code.name.monkey.retromusic.extensions.colorButtons
|
||||
import code.name.monkey.retromusic.extensions.createNewFile
|
||||
import code.name.monkey.retromusic.extensions.extraNotNull
|
||||
import code.name.monkey.retromusic.extensions.materialDialog
|
||||
import code.name.monkey.retromusic.helper.M3UWriter
|
||||
import code.name.monkey.retromusic.util.PlaylistsUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -46,22 +49,59 @@ class SavePlaylistDialog : DialogFragment() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val playlistWithSongs = extraNotNull<PlaylistWithSongs>(EXTRA_PLAYLIST).value
|
||||
val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs)
|
||||
MediaScannerConnection.scanFile(
|
||||
requireActivity(),
|
||||
arrayOf<String>(file.path),
|
||||
null
|
||||
) { _, _ ->
|
||||
val playlistWithSongs = extraNotNull<PlaylistWithSongs>(EXTRA_PLAYLIST).value
|
||||
|
||||
if (VersionUtils.hasR()) {
|
||||
createNewFile(
|
||||
"audio/mpegurl",
|
||||
playlistWithSongs.playlistEntity.playlistName
|
||||
) { outputStream, data ->
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
M3UWriter.writeIO(
|
||||
outputStream,
|
||||
playlistWithSongs
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
String.format(
|
||||
requireContext().getString(R.string.saved_playlist_to),
|
||||
data?.lastPathSegment
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Something went wrong : " + e.message,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
String.format(App.getContext().getString(R.string.saved_playlist_to), file),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
dismiss()
|
||||
} else {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs)
|
||||
MediaScannerConnection.scanFile(
|
||||
requireActivity(),
|
||||
arrayOf<String>(file.path),
|
||||
null
|
||||
) { _, _ ->
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
String.format(App.getContext().getString(R.string.saved_playlist_to), file),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package code.name.monkey.retromusic.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
fun Fragment.createNewFile(
|
||||
mimeType: String,
|
||||
fileName: String,
|
||||
write: (outputStream: OutputStream?, data: Uri?) -> Unit
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = mimeType
|
||||
intent.putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
val startForResult =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult())
|
||||
{ result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val outputStream: OutputStream? =
|
||||
context?.contentResolver?.openOutputStream(result.data?.data!!)
|
||||
write(outputStream, result.data?.data)
|
||||
}
|
||||
|
||||
}
|
||||
startForResult.launch(intent)
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -18,10 +18,7 @@ import code.name.monkey.retromusic.db.PlaylistWithSongs
|
|||
import code.name.monkey.retromusic.db.toSongs
|
||||
import code.name.monkey.retromusic.model.Playlist
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.*
|
||||
|
||||
object M3UWriter : M3UConstants {
|
||||
@JvmStatic
|
||||
|
@ -69,4 +66,23 @@ object M3UWriter : M3UConstants {
|
|||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun writeIO(outputStream: OutputStream, playlistWithSongs: PlaylistWithSongs) {
|
||||
val songs: List<Song> = playlistWithSongs.songs.sortedBy {
|
||||
it.songPrimaryKey
|
||||
}.toSongs()
|
||||
if (songs.isNotEmpty()) {
|
||||
val bufferedWriter = outputStream.bufferedWriter()
|
||||
bufferedWriter.write(M3UConstants.HEADER)
|
||||
songs.forEach {
|
||||
bufferedWriter.newLine()
|
||||
bufferedWriter.write(M3UConstants.ENTRY + it.duration + M3UConstants.DURATION_SEPARATOR + it.artistName + " - " + it.title)
|
||||
bufferedWriter.newLine()
|
||||
bufferedWriter.write(it.data)
|
||||
}
|
||||
bufferedWriter.close()
|
||||
}
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
|
@ -189,13 +189,13 @@ public class MusicService extends MediaBrowserServiceCompat
|
|||
|
||||
private final AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance();
|
||||
|
||||
private final AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance();
|
||||
private final AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance();
|
||||
|
||||
private final AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance();
|
||||
private final AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance();
|
||||
|
||||
private final AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance();
|
||||
private final AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance();
|
||||
|
||||
private final AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance();
|
||||
private final AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance();
|
||||
|
||||
private final BroadcastReceiver widgetIntentReceiver =
|
||||
new BroadcastReceiver() {
|
||||
|
@ -229,15 +229,15 @@ public class MusicService extends MediaBrowserServiceCompat
|
|||
}
|
||||
}
|
||||
};
|
||||
private AudioManager audioManager;
|
||||
private final IntentFilter becomingNoisyReceiverIntentFilter =
|
||||
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
private boolean becomingNoisyReceiverRegistered;
|
||||
private final IntentFilter bluetoothConnectedIntentFilter =
|
||||
new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||
private boolean bluetoothConnectedRegistered = false;
|
||||
private final IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
|
||||
private boolean headsetReceiverRegistered = false;
|
||||
private AudioManager audioManager;
|
||||
private final IntentFilter becomingNoisyReceiverIntentFilter =
|
||||
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
private boolean becomingNoisyReceiverRegistered;
|
||||
private final IntentFilter bluetoothConnectedIntentFilter =
|
||||
new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||
private boolean bluetoothConnectedRegistered = false;
|
||||
private final IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
|
||||
private boolean headsetReceiverRegistered = false;
|
||||
private MediaSessionCompat mediaSession;
|
||||
private ContentObserver mediaStoreObserver;
|
||||
private HandlerThread musicPlayerHandlerThread;
|
||||
|
@ -291,59 +291,59 @@ public class MusicService extends MediaBrowserServiceCompat
|
|||
private HandlerThread queueSaveHandlerThread;
|
||||
private boolean queuesRestored;
|
||||
private int repeatMode;
|
||||
private int shuffleMode;
|
||||
private final SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper();
|
||||
private final BroadcastReceiver bluetoothReceiver =
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)
|
||||
&& PreferenceUtil.INSTANCE.isBluetoothSpeaker()) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) {
|
||||
private int shuffleMode;
|
||||
private final SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper();
|
||||
private final BroadcastReceiver bluetoothReceiver =
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)
|
||||
&& PreferenceUtil.INSTANCE.isBluetoothSpeaker()) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) {
|
||||
play();
|
||||
}
|
||||
} else {
|
||||
if (getAudioManager().isBluetoothA2dpOn()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (getAudioManager().isBluetoothA2dpOn()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
private final PhoneStateListener phoneStateListener =
|
||||
new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// Not in call: Play music
|
||||
play();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
// A call is dialing, active or on hold
|
||||
pause();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
}
|
||||
};
|
||||
private final BroadcastReceiver headsetReceiver =
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
|
||||
int state = intent.getIntExtra("state", -1);
|
||||
switch (state) {
|
||||
case 0:
|
||||
pause();
|
||||
};
|
||||
private final PhoneStateListener phoneStateListener =
|
||||
new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
// Not in call: Play music
|
||||
play();
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
// A call is dialing, active or on hold
|
||||
pause();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
super.onCallStateChanged(state, incomingNumber);
|
||||
}
|
||||
};
|
||||
private final BroadcastReceiver headsetReceiver =
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
|
||||
int state = intent.getIntExtra("state", -1);
|
||||
switch (state) {
|
||||
case 0:
|
||||
pause();
|
||||
break;
|
||||
case 1:
|
||||
play();
|
||||
|
@ -1123,7 +1123,7 @@ public class MusicService extends MediaBrowserServiceCompat
|
|||
if (playback instanceof CrossFadePlayer) {
|
||||
((CrossFadePlayer) playback).sourceChangedByUser();
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
trackEndedByCrossfade = false;
|
||||
}
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
|
@ -1594,15 +1594,18 @@ public class MusicService extends MediaBrowserServiceCompat
|
|||
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
mediaButtonIntent.setComponent(mediaButtonReceiverComponentName);
|
||||
|
||||
PendingIntent mediaButtonReceiverPendingIntent =
|
||||
PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0);
|
||||
PendingIntent mediaButtonReceiverPendingIntent;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, PendingIntent.FLAG_MUTABLE);
|
||||
} else {
|
||||
mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0);
|
||||
}
|
||||
|
||||
mediaSession =
|
||||
new MediaSessionCompat(
|
||||
this,
|
||||
"RetroMusicPlayer",
|
||||
mediaButtonReceiverComponentName,
|
||||
mediaButtonReceiverPendingIntent);
|
||||
mediaSession = new MediaSessionCompat(
|
||||
this,
|
||||
"RetroMusicPlayer",
|
||||
mediaButtonReceiverComponentName,
|
||||
mediaButtonReceiverPendingIntent);
|
||||
MediaSessionCallback mediasessionCallback =
|
||||
new MediaSessionCallback(getApplicationContext(), this);
|
||||
mediaSession.setFlags(
|
||||
|
|
|
@ -69,13 +69,21 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
|
|||
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
|
||||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
val clickIntent =
|
||||
PendingIntent.getActivity(service, 0, action, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getActivity(service, 0, action, PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getActivity(service, 0, action, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
val serviceName = ComponentName(service, MusicService::class.java)
|
||||
val intent = Intent(ACTION_QUIT)
|
||||
intent.component = serviceName
|
||||
val deleteIntent = PendingIntent.getService(service, 0, intent, 0)
|
||||
|
||||
val deleteIntent = PendingIntent.getService(
|
||||
service,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val bigNotificationImageSize = service.resources
|
||||
.getDimensionPixelSize(R.dimen.notification_big_image_size)
|
||||
service.runOnUiThread {
|
||||
|
@ -191,6 +199,6 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
|
|||
val serviceName = ComponentName(service, MusicService::class.java)
|
||||
val intent = Intent(action)
|
||||
intent.component = serviceName
|
||||
return PendingIntent.getService(service, 0, intent, 0)
|
||||
return PendingIntent.getService(service, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import androidx.core.app.NotificationCompat
|
|||
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
|
||||
import code.name.monkey.appthemehelper.util.ColorUtil
|
||||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.activities.MainActivity
|
||||
import code.name.monkey.retromusic.glide.GlideApp
|
||||
|
@ -78,7 +79,12 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
|
||||
val clickIntent = PendingIntent
|
||||
.getActivity(service, 0, action, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
.getActivity(
|
||||
service,
|
||||
0,
|
||||
action,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val deleteIntent = buildPendingIntent(service, ACTION_QUIT, null)
|
||||
|
||||
val builder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
|
||||
|
@ -143,10 +149,16 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
)
|
||||
}
|
||||
|
||||
if (!PreferenceUtil.isColoredNotification) {
|
||||
bgColorFinal = resolveColor(service, R.attr.colorPrimary, Color.WHITE)
|
||||
// Android 12 applies a standard Notification template to every notification
|
||||
// which will in turn have a default background so setting a different background
|
||||
// than that, looks weird
|
||||
if (!VersionUtils.hasS()) {
|
||||
if (!PreferenceUtil.isColoredNotification) {
|
||||
bgColorFinal =
|
||||
resolveColor(service, R.attr.colorPrimary, Color.WHITE)
|
||||
}
|
||||
setBackgroundColor(bgColorFinal)
|
||||
}
|
||||
setBackgroundColor(bgColorFinal)
|
||||
setNotificationContent(ColorUtil.isColorLight(bgColorFinal))
|
||||
|
||||
if (stopped) {
|
||||
|
@ -250,7 +262,7 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
): PendingIntent {
|
||||
val intent = Intent(action)
|
||||
intent.component = serviceName
|
||||
return PendingIntent.getService(context, 0, intent, 0)
|
||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.BaseColumns
|
||||
import android.provider.MediaStore
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
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
|
||||
|
@ -72,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 {
|
||||
|
@ -517,6 +524,14 @@ object MusicUtil : KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun deleteTracksQ(activity: Activity, songs: List<Song>) {
|
||||
val pendingIntent = MediaStore.createDeleteRequest(activity.contentResolver, songs.map {
|
||||
getSongFileUri(it.id)
|
||||
})
|
||||
activity.startIntentSenderForResult(pendingIntent.intentSender, 45, null, 0, 0, 0, null);
|
||||
}
|
||||
|
||||
fun songByGenre(genreId: Long): Song {
|
||||
return repository.getSongByGenre(genreId)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import java.util.List;
|
|||
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.db.PlaylistWithSongs;
|
||||
import code.name.monkey.retromusic.helper.M3UConstants;
|
||||
import code.name.monkey.retromusic.helper.M3UWriter;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.PlaylistSong;
|
||||
|
@ -319,9 +320,9 @@ public class PlaylistsUtil {
|
|||
private static boolean doesPlaylistExist(
|
||||
@NonNull Context context, @NonNull final String selection, @NonNull final String[] values) {
|
||||
Cursor cursor =
|
||||
context
|
||||
.getContentResolver()
|
||||
.query(EXTERNAL_CONTENT_URI, new String[] {}, selection, values, null);
|
||||
context
|
||||
.getContentResolver()
|
||||
.query(EXTERNAL_CONTENT_URI, new String[]{}, selection, values, null);
|
||||
|
||||
boolean exists = false;
|
||||
if (cursor != null) {
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@color/md_red_500"
|
||||
tools:ignore="ContentDescription">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@+id/content"
|
||||
android:layout_alignParentEnd="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/largeIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@id/actions"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Theme.RetroMusic.Notification"
|
||||
android:textStyle="bold"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Theme.RetroMusic.Notification.Title"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/actions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/content"
|
||||
android:layout_alignBottom="@id/content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_prev"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_skip_previous_round_white_32dp"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_play_pause"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_pause"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_next"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_skip_next_round_white_32dp"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@color/md_red_500">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/content"
|
||||
android:layout_alignParentEnd="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/largeIcon"
|
||||
android:layout_width="@dimen/notification_big_image_size"
|
||||
android:layout_height="@dimen/notification_big_image_size"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="144dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Theme.RetroMusic.Notification"
|
||||
android:textStyle="bold"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/Theme.RetroMusic.Notification.Title"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="0dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_prev"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
tools:src="@drawable/ic_skip_previous_round_white_32dp"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_play_pause"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
tools:src="@drawable/ic_pause_white_48dp"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_next"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
tools:src="@drawable/ic_skip_next_round_white_32dp"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_quit"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@drawable/notification_selector"
|
||||
android:scaleType="centerInside"
|
||||
tools:src="@drawable/ic_close"
|
||||
tools:tint="?colorOnPrimary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
|
@ -2,4 +2,6 @@
|
|||
<resources>
|
||||
<bool name="md3_available">true</bool>
|
||||
<bool name="md3_enabled">true</bool>
|
||||
|
||||
<bool name="colored_notification_available">false</bool>
|
||||
</resources>
|
|
@ -26,4 +26,6 @@
|
|||
<string name="pref_title_md3">Material You</string>
|
||||
<bool name="md3_available">false</bool>
|
||||
<bool name="md3_enabled">false</bool>
|
||||
|
||||
<bool name="colored_notification_available">true</bool>
|
||||
</resources>
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<automotiveApp>
|
||||
<uses name="media" />
|
||||
</automotiveApp>
|
|
@ -13,6 +13,7 @@
|
|||
android:defaultValue="true"
|
||||
android:key="colored_notification"
|
||||
android:layout="@layout/list_item_view_switch"
|
||||
app:isPreferenceVisible="@bool/colored_notification_available"
|
||||
android:summary="@string/pref_summary_colored_notification"
|
||||
android:title="@string/pref_title_colored_notification" />
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ android {
|
|||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 31
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
|
|
@ -12,14 +12,14 @@ object VersionUtils {
|
|||
* @return true if device is running API >= 21
|
||||
*/
|
||||
fun hasLollipop(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= 21
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 23
|
||||
*/
|
||||
fun hasMarshmallow(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= 23
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ object VersionUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 24
|
||||
* @return true if device is running API >= 25
|
||||
*/
|
||||
fun hasNougatMR(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1
|
||||
|
@ -44,20 +44,31 @@ object VersionUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 27
|
||||
* @return true if device is running API >= 28
|
||||
*/
|
||||
fun hasP(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 28
|
||||
* @return true if device is running API >= 29
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasQ(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 30
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasR(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if device is running API >= 31
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasS(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
|
Loading…
Reference in New Issue