Merge pull request #1169 from prathameshmm02/dev

Support for scoped storage and better compatibility with Android 12
main
Daksh P. Jain 2021-11-22 18:12:53 +05:30 committed by GitHub
commit b2970a6185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 813 additions and 330 deletions

View File

@ -9,14 +9,14 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 31
renderscriptTargetApi 29//must match target sdk and build tools renderscriptTargetApi 29//must match target sdk and build tools
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
applicationId "code.name.monkey.retromusic" applicationId "code.name.monkey.retromusic"
versionCode 10519 versionCode 10537
versionName '5.0.0' + "_" + getDate() versionName '5.2.1 ' + "_" + getDate()
buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"") buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"")
} }

View File

@ -35,6 +35,7 @@
tools:targetApi="m"> tools:targetApi="m">
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme">
<intent-filter> <intent-filter>
@ -144,6 +145,7 @@
<activity <activity
android:name=".cast.ExpandedControlsActivity" android:name=".cast.ExpandedControlsActivity"
android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@ -176,13 +178,17 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<receiver android:name=".service.MediaButtonIntentReceiver"> <receiver
android:name=".service.MediaButtonIntentReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".appwidgets.BootReceiver"> <receiver
android:name=".appwidgets.BootReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
@ -191,7 +197,7 @@
<receiver <receiver
android:name=".appwidgets.AppWidgetBig" android:name=".appwidgets.AppWidgetBig"
android:exported="false" android:exported="true"
android:label="@string/app_widget_big_name"> android:label="@string/app_widget_big_name">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -204,7 +210,7 @@
<receiver <receiver
android:name=".appwidgets.AppWidgetClassic" android:name=".appwidgets.AppWidgetClassic"
android:exported="false" android:exported="true"
android:label="@string/app_widget_classic_name"> android:label="@string/app_widget_classic_name">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -216,7 +222,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".appwidgets.AppWidgetSmall" android:name=".appwidgets.AppWidgetSmall"
android:exported="false" android:exported="true"
android:label="@string/app_widget_small_name"> android:label="@string/app_widget_small_name">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -228,7 +234,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".appwidgets.AppWidgetText" android:name=".appwidgets.AppWidgetText"
android:exported="false" android:exported="true"
android:label="@string/app_widget_text_name"> android:label="@string/app_widget_text_name">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -240,7 +246,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".appwidgets.AppWidgetCard" android:name=".appwidgets.AppWidgetCard"
android:exported="false" android:exported="true"
android:label="@string/app_widget_card_name"> android:label="@string/app_widget_card_name">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -274,17 +280,6 @@
android:name="com.android.vending.splits.required" android:name="com.android.vending.splits.required"
android:value="true" /> 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 --> <!-- ChromeCast -->
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"

View File

@ -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

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.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) {
} }

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.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?) {
} }

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

@ -74,7 +74,7 @@ class AppWidgetBig : BaseAppWidget() {
) )
) )
appWidgetView.setImageViewBitmap( appWidgetView.setImageViewBitmap(
R.id.button_toggle_play_pause, BaseAppWidget.Companion.createBitmap( R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(
RetroUtil.getTintedVectorDrawable( RetroUtil.getTintedVectorDrawable(
context, context,
R.drawable.ic_play_arrow_white_32dp, R.drawable.ic_play_arrow_white_32dp,
@ -202,13 +202,13 @@ class AppWidgetBig : BaseAppWidget() {
MainActivity.EXPAND_PANEL, MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel PreferenceUtil.isExpandPanel
) )
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Home // Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 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) views.setOnClickPendingIntent(R.id.clickable_area, pendingIntent)
// Previous track // Previous track

View File

@ -141,11 +141,11 @@ class AppWidgetCard : BaseAppWidget() {
if (imageSize == 0) { if (imageSize == 0) {
imageSize = 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) { if (cardRadius == 0f) {
cardRadius = 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 // Load the album cover async and push the update on completion
@ -225,13 +225,13 @@ class AppWidgetCard : BaseAppWidget() {
MainActivity.EXPAND_PANEL, MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel PreferenceUtil.isExpandPanel
) )
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Home // Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 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.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)

View File

@ -216,13 +216,12 @@ class AppWidgetClassic : BaseAppWidget() {
MainActivity.EXPAND_PANEL, MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel PreferenceUtil.isExpandPanel
) )
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Home // Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 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.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)

View File

@ -201,13 +201,13 @@ class AppWidgetSmall : BaseAppWidget() {
MainActivity.EXPAND_PANEL, MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel PreferenceUtil.isExpandPanel
) )
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Home // Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 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.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)

View File

@ -18,6 +18,7 @@ import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
@ -83,13 +84,12 @@ class AppWidgetText : BaseAppWidget() {
MainActivity.EXPAND_PANEL, MainActivity.EXPAND_PANEL,
PreferenceUtil.isExpandPanel PreferenceUtil.isExpandPanel
) )
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Home // Home
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 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.image, pendingIntent)
views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)

View File

@ -97,9 +97,9 @@ abstract class BaseAppWidget : AppWidgetProvider() {
val intent = Intent(action) val intent = Intent(action)
intent.component = serviceName intent.component = serviceName
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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 { } else {
PendingIntent.getService(context, 0, intent, 0) PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} }
} }

View File

@ -21,6 +21,7 @@ import android.os.Bundle
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.EXTRA_SONG import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
@ -89,7 +90,11 @@ class DeleteSongsDialog : DialogFragment() {
if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) { if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
MusicPlayerRemote.playNextSong() MusicPlayerRemote.playNextSong()
} }
if (!SAFUtil.isSAFRequiredForSongs(songs)) { if (VersionUtils.hasQ()) {
dismiss()
MusicUtil.deleteTracksQ(requireActivity(), songs)
reloadTabs()
} else if (!SAFUtil.isSAFRequiredForSongs(songs)) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
dismiss() dismiss()
MusicUtil.deleteTracks(requireContext(), songs) MusicUtil.deleteTracks(requireContext(), songs)

View File

@ -21,13 +21,16 @@ import android.widget.Toast
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.App import code.name.monkey.retromusic.App
import code.name.monkey.retromusic.EXTRA_PLAYLIST import code.name.monkey.retromusic.EXTRA_PLAYLIST
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.extensions.colorButtons 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.extraNotNull
import code.name.monkey.retromusic.extensions.materialDialog import code.name.monkey.retromusic.extensions.materialDialog
import code.name.monkey.retromusic.helper.M3UWriter
import code.name.monkey.retromusic.util.PlaylistsUtil import code.name.monkey.retromusic.util.PlaylistsUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -46,22 +49,59 @@ class SavePlaylistDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.IO) { val playlistWithSongs = extraNotNull<PlaylistWithSongs>(EXTRA_PLAYLIST).value
val playlistWithSongs = extraNotNull<PlaylistWithSongs>(EXTRA_PLAYLIST).value
val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs) if (VersionUtils.hasR()) {
MediaScannerConnection.scanFile( createNewFile(
requireActivity(), "audio/mpegurl",
arrayOf<String>(file.path), playlistWithSongs.playlistEntity.playlistName
null ) { 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) { } else {
Toast.makeText( lifecycleScope.launch(Dispatchers.IO) {
requireContext(), val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs)
String.format(App.getContext().getString(R.string.saved_playlist_to), file), MediaScannerConnection.scanFile(
Toast.LENGTH_LONG requireActivity(),
).show() arrayOf<String>(file.path),
dismiss() null
) { _, _ ->
}
withContext(Dispatchers.Main) {
Toast.makeText(
requireContext(),
String.format(App.getContext().getString(R.string.saved_playlist_to), file),
Toast.LENGTH_LONG
).show()
dismiss()
}
} }
} }
} }

View File

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

View File

@ -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)) {

View File

@ -18,10 +18,7 @@ import code.name.monkey.retromusic.db.PlaylistWithSongs
import code.name.monkey.retromusic.db.toSongs import code.name.monkey.retromusic.db.toSongs
import code.name.monkey.retromusic.model.Playlist import code.name.monkey.retromusic.model.Playlist
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import java.io.BufferedWriter import java.io.*
import java.io.File
import java.io.FileWriter
import java.io.IOException
object M3UWriter : M3UConstants { object M3UWriter : M3UConstants {
@JvmStatic @JvmStatic
@ -69,4 +66,23 @@ object M3UWriter : M3UConstants {
} }
return file 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()
}
} }

View File

@ -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?

View File

@ -189,13 +189,13 @@ public class MusicService extends MediaBrowserServiceCompat
private final AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); 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 = private final BroadcastReceiver widgetIntentReceiver =
new BroadcastReceiver() { new BroadcastReceiver() {
@ -229,15 +229,15 @@ public class MusicService extends MediaBrowserServiceCompat
} }
} }
}; };
private AudioManager audioManager; private AudioManager audioManager;
private final IntentFilter becomingNoisyReceiverIntentFilter = private final IntentFilter becomingNoisyReceiverIntentFilter =
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private boolean becomingNoisyReceiverRegistered; private boolean becomingNoisyReceiverRegistered;
private final IntentFilter bluetoothConnectedIntentFilter = private final IntentFilter bluetoothConnectedIntentFilter =
new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED);
private boolean bluetoothConnectedRegistered = false; private boolean bluetoothConnectedRegistered = false;
private final IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); private final IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
private boolean headsetReceiverRegistered = false; private boolean headsetReceiverRegistered = false;
private MediaSessionCompat mediaSession; private MediaSessionCompat mediaSession;
private ContentObserver mediaStoreObserver; private ContentObserver mediaStoreObserver;
private HandlerThread musicPlayerHandlerThread; private HandlerThread musicPlayerHandlerThread;
@ -291,59 +291,59 @@ public class MusicService extends MediaBrowserServiceCompat
private HandlerThread queueSaveHandlerThread; private HandlerThread queueSaveHandlerThread;
private boolean queuesRestored; private boolean queuesRestored;
private int repeatMode; private int repeatMode;
private int shuffleMode; private int shuffleMode;
private final SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); private final SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper();
private final BroadcastReceiver bluetoothReceiver = private final BroadcastReceiver bluetoothReceiver =
new BroadcastReceiver() { new BroadcastReceiver() {
@Override @Override
public void onReceive(final Context context, final Intent intent) { public void onReceive(final Context context, final Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
if (action != null) { if (action != null) {
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action) if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)
&& PreferenceUtil.INSTANCE.isBluetoothSpeaker()) { && PreferenceUtil.INSTANCE.isBluetoothSpeaker()) {
if (VERSION.SDK_INT >= VERSION_CODES.M) { if (VERSION.SDK_INT >= VERSION_CODES.M) {
if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) { if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) {
play(); play();
}
} else {
if (getAudioManager().isBluetoothA2dpOn()) {
play();
}
}
}
} }
} else {
if (getAudioManager().isBluetoothA2dpOn()) {
play();
}
}
} }
} };
} private final PhoneStateListener phoneStateListener =
}; new PhoneStateListener() {
private final PhoneStateListener phoneStateListener = @Override
new PhoneStateListener() { public void onCallStateChanged(int state, String incomingNumber) {
@Override switch (state) {
public void onCallStateChanged(int state, String incomingNumber) { case TelephonyManager.CALL_STATE_IDLE:
switch (state) { // Not in call: Play music
case TelephonyManager.CALL_STATE_IDLE: play();
// Not in call: Play music break;
play(); case TelephonyManager.CALL_STATE_RINGING:
break; case TelephonyManager.CALL_STATE_OFFHOOK:
case TelephonyManager.CALL_STATE_RINGING: // A call is dialing, active or on hold
case TelephonyManager.CALL_STATE_OFFHOOK: pause();
// A call is dialing, active or on hold break;
pause(); default:
break; }
default: super.onCallStateChanged(state, incomingNumber);
} }
super.onCallStateChanged(state, incomingNumber); };
} private final BroadcastReceiver headsetReceiver =
}; new BroadcastReceiver() {
private final BroadcastReceiver headsetReceiver = @Override
new BroadcastReceiver() { public void onReceive(Context context, Intent intent) {
@Override String action = intent.getAction();
public void onReceive(Context context, Intent intent) { if (action != null) {
String action = intent.getAction(); if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
if (action != null) { int state = intent.getIntExtra("state", -1);
if (Intent.ACTION_HEADSET_PLUG.equals(action)) { switch (state) {
int state = intent.getIntExtra("state", -1); case 0:
switch (state) { pause();
case 0:
pause();
break; break;
case 1: case 1:
play(); play();
@ -1123,7 +1123,7 @@ public class MusicService extends MediaBrowserServiceCompat
if (playback instanceof CrossFadePlayer) { if (playback instanceof CrossFadePlayer) {
((CrossFadePlayer) playback).sourceChangedByUser(); ((CrossFadePlayer) playback).sourceChangedByUser();
} }
} else { } else {
trackEndedByCrossfade = false; trackEndedByCrossfade = false;
} }
if (openTrackAndPrepareNextAt(position)) { if (openTrackAndPrepareNextAt(position)) {
@ -1594,15 +1594,18 @@ public class MusicService extends MediaBrowserServiceCompat
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); mediaButtonIntent.setComponent(mediaButtonReceiverComponentName);
PendingIntent mediaButtonReceiverPendingIntent = PendingIntent mediaButtonReceiverPendingIntent;
PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); 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 = mediaSession = new MediaSessionCompat(
new MediaSessionCompat( this,
this, "RetroMusicPlayer",
"RetroMusicPlayer", mediaButtonReceiverComponentName,
mediaButtonReceiverComponentName, mediaButtonReceiverPendingIntent);
mediaButtonReceiverPendingIntent);
MediaSessionCallback mediasessionCallback = MediaSessionCallback mediasessionCallback =
new MediaSessionCallback(getApplicationContext(), this); new MediaSessionCallback(getApplicationContext(), this);
mediaSession.setFlags( mediaSession.setFlags(

View File

@ -69,13 +69,21 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel) action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent = 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 serviceName = ComponentName(service, MusicService::class.java)
val intent = Intent(ACTION_QUIT) val intent = Intent(ACTION_QUIT)
intent.component = serviceName 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 val bigNotificationImageSize = service.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size) .getDimensionPixelSize(R.dimen.notification_big_image_size)
service.runOnUiThread { service.runOnUiThread {
@ -191,6 +199,6 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
val serviceName = ComponentName(service, MusicService::class.java) val serviceName = ComponentName(service, MusicService::class.java)
val intent = Intent(action) val intent = Intent(action)
intent.component = serviceName intent.component = serviceName
return PendingIntent.getService(service, 0, intent, 0) return PendingIntent.getService(service, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

View File

@ -26,6 +26,7 @@ import androidx.core.app.NotificationCompat
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper 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.R
import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.glide.GlideApp 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 action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent = PendingIntent 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 deleteIntent = buildPendingIntent(service, ACTION_QUIT, null)
val builder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID) val builder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
@ -143,10 +149,16 @@ class PlayingNotificationOreo : PlayingNotification() {
) )
} }
if (!PreferenceUtil.isColoredNotification) { // Android 12 applies a standard Notification template to every notification
bgColorFinal = resolveColor(service, R.attr.colorPrimary, Color.WHITE) // 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)) setNotificationContent(ColorUtil.isColorLight(bgColorFinal))
if (stopped) { if (stopped) {
@ -250,7 +262,7 @@ class PlayingNotificationOreo : PlayingNotification() {
): PendingIntent { ): PendingIntent {
val intent = Intent(action) val intent = Intent(action)
intent.component = serviceName intent.component = serviceName
return PendingIntent.getService(context, 0, intent, 0) return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} }

View File

@ -1,19 +1,23 @@
package code.name.monkey.retromusic.util package code.name.monkey.retromusic.util
import android.app.Activity
import android.content.ContentUris import android.content.ContentUris
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.BaseColumns import android.provider.BaseColumns
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
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
@ -72,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 {
@ -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 { fun songByGenre(genreId: Long): Song {
return repository.getSongByGenre(genreId) return repository.getSongByGenre(genreId)
} }

View File

@ -36,6 +36,7 @@ import java.util.List;
import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.R;
import code.name.monkey.retromusic.db.PlaylistWithSongs; 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.helper.M3UWriter;
import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Playlist;
import code.name.monkey.retromusic.model.PlaylistSong; import code.name.monkey.retromusic.model.PlaylistSong;
@ -319,9 +320,9 @@ public class PlaylistsUtil {
private static boolean doesPlaylistExist( private static boolean doesPlaylistExist(
@NonNull Context context, @NonNull final String selection, @NonNull final String[] values) { @NonNull Context context, @NonNull final String selection, @NonNull final String[] values) {
Cursor cursor = Cursor cursor =
context context
.getContentResolver() .getContentResolver()
.query(EXTERNAL_CONTENT_URI, new String[] {}, selection, values, null); .query(EXTERNAL_CONTENT_URI, new String[]{}, selection, values, null);
boolean exists = false; boolean exists = false;
if (cursor != null) { if (cursor != null) {

View File

@ -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>

View File

@ -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>

View File

@ -2,4 +2,6 @@
<resources> <resources>
<bool name="md3_available">true</bool> <bool name="md3_available">true</bool>
<bool name="md3_enabled">true</bool> <bool name="md3_enabled">true</bool>
<bool name="colored_notification_available">false</bool>
</resources> </resources>

View File

@ -26,4 +26,6 @@
<string name="pref_title_md3">Material You</string> <string name="pref_title_md3">Material You</string>
<bool name="md3_available">false</bool> <bool name="md3_available">false</bool>
<bool name="md3_enabled">false</bool> <bool name="md3_enabled">false</bool>
<bool name="colored_notification_available">true</bool>
</resources> </resources>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="media" />
</automotiveApp>

View File

@ -13,6 +13,7 @@
android:defaultValue="true" android:defaultValue="true"
android:key="colored_notification" android:key="colored_notification"
android:layout="@layout/list_item_view_switch" android:layout="@layout/list_item_view_switch"
app:isPreferenceVisible="@bool/colored_notification_available"
android:summary="@string/pref_summary_colored_notification" android:summary="@string/pref_summary_colored_notification"
android:title="@string/pref_title_colored_notification" /> android:title="@string/pref_title_colored_notification" />

View File

@ -4,7 +4,7 @@ android {
compileSdkVersion 31 compileSdkVersion 31
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@ -12,14 +12,14 @@ object VersionUtils {
* @return true if device is running API >= 21 * @return true if device is running API >= 21
*/ */
fun hasLollipop(): Boolean { 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 * @return true if device is running API >= 23
*/ */
fun hasMarshmallow(): Boolean { 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 { fun hasNougatMR(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 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 { fun hasP(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P 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 @JvmStatic
fun hasQ(): Boolean { fun hasQ(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q 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 @JvmStatic
fun hasS(): Boolean { fun hasS(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S