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 {
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')}\"")
}

View File

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

View File

@ -21,28 +21,36 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.animation.OvershootInterpolator
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.appthemehelper.util.ATHUtil
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.R.drawable
import code.name.monkey.retromusic.activities.base.AbsBaseActivity
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.model.ArtworkInfo
import code.name.monkey.retromusic.model.LoadingInfo
import code.name.monkey.retromusic.model.AudioTagInfo
import code.name.monkey.retromusic.repository.Repository
import code.name.monkey.retromusic.util.RetroUtil
import code.name.monkey.retromusic.util.SAFUtil
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jaudiotagger.audio.AudioFile
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
@ -66,9 +74,12 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
private var savedArtworkInfo: ArtworkInfo? = null
private var _binding: VB? = null
protected val binding: VB get() = _binding!!
private var cacheFiles = listOf<File>()
abstract val bindingInflater: (LayoutInflater) -> VB
private lateinit var launcher: ActivityResultLauncher<IntentSenderRequest>
protected abstract fun loadImageFromFile(selectedFile: Uri?)
protected val show: AlertDialog
@ -195,7 +206,6 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
super.onCreate(savedInstanceState)
_binding = bindingInflater.invoke(layoutInflater)
setContentView(binding.root)
setStatusbarColorAuto()
setTaskDescriptionColorAuto()
saveFab = findViewById(R.id.saveTags)
@ -207,6 +217,11 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
finish()
}
setUpViews()
launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
writeToFiles(getSongUris(), cacheFiles)
}
}
}
private fun setUpViews() {
@ -265,6 +280,8 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
protected abstract fun getSongPaths(): List<String>
protected abstract fun getSongUris(): List<Uri>
protected fun searchWebFor(vararg keys: String) {
val stringBuilder = StringBuilder()
for (key in keys) {
@ -336,23 +353,53 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
hideFab()
println(fieldKeyValueMap)
WriteTagsAsyncTask(this).execute(
LoadingInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
GlobalScope.launch {
if (VersionUtils.hasR()) {
cacheFiles = TagWriter.writeTagsToFilesR(
this@AbsTagEditorActivity, AudioTagInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
} else {
TagWriter.writeTagsToFiles(
this@AbsTagEditorActivity, AudioTagInfo(
songPaths,
fieldKeyValueMap,
artworkInfo
)
)
}
}
}
private fun writeTags(paths: List<String>?) {
WriteTagsAsyncTask(this).execute(
LoadingInfo(
paths,
savedTags,
savedArtworkInfo
)
)
GlobalScope.launch {
if (VersionUtils.hasR()) {
cacheFiles = TagWriter.writeTagsToFilesR(
this@AbsTagEditorActivity, AudioTagInfo(
paths,
savedTags,
savedArtworkInfo
)
)
val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
} else {
TagWriter.writeTagsToFiles(
this@AbsTagEditorActivity, AudioTagInfo(
paths,
savedTags,
savedArtworkInfo
)
)
}
}
}
@ -391,9 +438,30 @@ abstract class AbsTagEditorActivity<VB : ViewBinding> : AbsBaseActivity() {
}
}
private fun writeToFiles(songUris: List<Uri>, cacheFiles: List<File>) {
if (cacheFiles.size == songUris.size) {
for (i in cacheFiles.indices) {
contentResolver.openOutputStream(songUris[i])?.use { output ->
cacheFiles[i].inputStream().use { input ->
input.copyTo(output)
}
}
}
}
lifecycleScope.launch {
TagWriter.scan(this@AbsTagEditorActivity, getSongPaths())
}
}
override fun onDestroy() {
super.onDestroy()
// Delete Cache Files
cacheFiles.forEach { file ->
file.delete()
}
}
companion object {
const val EXTRA_ID = "extra_id"
const val EXTRA_PALETTE = "extra_palette"
private val TAG = AbsTagEditorActivity::class.java.simpleName

View File

@ -38,6 +38,7 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.model.ArtworkInfo
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.ImageUtil
import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
import code.name.monkey.retromusic.util.RetroColorUtil.getColor
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -193,6 +194,11 @@ class AlbumTagEditorActivity : AbsTagEditorActivity<ActivityAlbumTagEditorBindin
.map(Song::data)
}
override fun getSongUris(): List<Uri> = repository.albumById(id).songs.map {
MusicUtil.getSongFileUri(it.id)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}

View File

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

View File

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

View File

@ -1,152 +0,0 @@
package code.name.monkey.retromusic.activities.tageditor;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaScannerConnection;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.CannotWriteException;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagException;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.images.ArtworkFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import code.name.monkey.retromusic.R;
import code.name.monkey.retromusic.misc.DialogAsyncTask;
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener;
import code.name.monkey.retromusic.model.LoadingInfo;
import code.name.monkey.retromusic.util.MusicUtil;
public class WriteTagsAsyncTask extends DialogAsyncTask<LoadingInfo, Integer, List<String>> {
public WriteTagsAsyncTask(Context context) {
super(context);
}
@Override
protected List<String> doInBackground(LoadingInfo... params) {
try {
LoadingInfo info = params[0];
Artwork artwork = null;
File albumArtFile = null;
if (info.getArtworkInfo() != null && info.getArtworkInfo().getArtwork() != null) {
try {
albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile();
info.getArtworkInfo().getArtwork().compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile));
artwork = ArtworkFactory.createArtworkFromFile(albumArtFile);
} catch (IOException e) {
e.printStackTrace();
}
}
int counter = 0;
boolean wroteArtwork = false;
boolean deletedArtwork = false;
for (String filePath : info.getFilePaths()) {
publishProgress(++counter, info.getFilePaths().size());
try {
AudioFile audioFile = AudioFileIO.read(new File(filePath));
Tag tag = audioFile.getTagOrCreateAndSetDefault();
if (info.getFieldKeyValueMap() != null) {
for (Map.Entry<FieldKey, String> entry : info.getFieldKeyValueMap().entrySet()) {
try {
tag.setField(entry.getKey(), entry.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
}
if (info.getArtworkInfo() != null) {
if (info.getArtworkInfo().getArtwork() == null) {
tag.deleteArtworkField();
deletedArtwork = true;
} else if (artwork != null) {
tag.deleteArtworkField();
tag.setField(artwork);
wroteArtwork = true;
}
}
audioFile.commit();
} catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) {
e.printStackTrace();
}
}
Context context = getContext();
if (context != null) {
if (wroteArtwork) {
MusicUtil.INSTANCE.
insertAlbumArt(context, info.getArtworkInfo().getAlbumId(), albumArtFile.getPath());
} else if (deletedArtwork) {
MusicUtil.INSTANCE.deleteAlbumArt(context, info.getArtworkInfo().getAlbumId());
}
}
return info.getFilePaths();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(List<String> toBeScanned) {
super.onPostExecute(toBeScanned);
scan(toBeScanned);
}
@Override
protected void onCancelled(List<String> toBeScanned) {
super.onCancelled(toBeScanned);
scan(toBeScanned);
}
private void scan(List<String> toBeScanned) {
Context context = getContext();
if (toBeScanned == null || toBeScanned.isEmpty()) {
Log.i("scan", "scan: Empty");
Toast.makeText(context, "Scan file from folder", Toast.LENGTH_SHORT).show();
return;
}
MediaScannerConnection.scanFile(context, toBeScanned.toArray(new String[0]), null, context instanceof Activity ? new UpdateToastMediaScannerCompletionListener((Activity) context, toBeScanned) : null);
}
@Override
protected Dialog createDialog(@NonNull Context context) {
return new MaterialAlertDialogBuilder(context)
.setTitle(R.string.saving_changes)
.setCancelable(false)
.setView(R.layout.loading)
.create();
}
@Override
protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) {
super.onProgressUpdate(dialog, values);
// ((MaterialDialog) dialog).setMaxProgress(values[1]);
// ((MaterialDialog) dialog).setProgress(values[0]);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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>
<bool name="md3_available">true</bool>
<bool name="md3_enabled">true</bool>
<bool name="colored_notification_available">false</bool>
</resources>

View File

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

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: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" />

View File

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

View File

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