diff --git a/app/build.gradle b/app/build.gradle index 7322c109..d3959726 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,6 +107,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'androidx.palette:palette-ktx:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation 'com.google.android.material:material:1.1.0-alpha09' @@ -130,11 +131,10 @@ dependencies { implementation('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.11.0@aar') { transitive = true } - - implementation 'com.anjlab.android.iab.v3:library:1.0.44' + /*UI Library*/ implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' - implementation 'com.r0adkll:slidableactivity:2.0.6' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'org.eclipse.mylyn.github:org.eclipse.egit.github.core:3.4.0.201406110918-r' @@ -145,7 +145,10 @@ dependencies { implementation 'com.github.kabouzeid:AndroidSlidingUpPanel:3.3.0-kmod3' implementation 'com.github.AdrienPoupa:jaudiotagger:2.2.3' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' + implementation 'com.anjlab.android.iab.v3:library:1.0.44' + implementation 'com.r0adkll:slidableactivity:2.0.6' + implementation 'com.heinrichreimersoftware:material-intro:1.6' + implementation project(':appthemehelper') } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2be1391c..6a9647b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -121,6 +121,7 @@ android:name=".appshortcuts.AppShortcutLauncherActivity" android:launchMode="singleInstance" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + ? = null lateinit var saveFab: ExtendedFloatingActionButton + private var savedSongPaths: List? = null + private val currentSongPath: String? = null + private var savedTags: Map? = null + private var savedArtworkInfo: ArtworkInfo? = null + protected val show: MaterialDialog get() = MaterialDialog(this@AbsTagEditorActivity).show { - title(R.string.update_image) + title(code.name.monkey.retromusic.R.string.update_image) listItems(items = items) { _, position, _ -> when (position) { 0 -> getImageFromLastFM() @@ -174,7 +183,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { super.onCreate(savedInstanceState) setContentView(contentViewLayout) - saveFab = findViewById(R.id.saveTags) + saveFab = findViewById(code.name.monkey.retromusic.R.id.saveTags) getIntentExtras() songPaths = getSongPaths() @@ -204,14 +213,14 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { private fun setUpImageView() { loadCurrentImage() - items = listOf(getString(R.string.download_from_last_fm), getString(R.string.pick_from_local_storage), getString(R.string.web_search), getString(R.string.remove_cover)) + items = listOf(getString(code.name.monkey.retromusic.R.string.download_from_last_fm), getString(code.name.monkey.retromusic.R.string.pick_from_local_storage), getString(code.name.monkey.retromusic.R.string.web_search), getString(code.name.monkey.retromusic.R.string.remove_cover)) editorImage.setOnClickListener { show } } private fun startImagePicker() { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.type = "image/*" - startActivityForResult(Intent.createChooser(intent, getString(R.string.pick_from_local_storage)), REQUEST_CODE_SELECT_IMAGE) + startActivityForResult(Intent.createChooser(intent, getString(code.name.monkey.retromusic.R.string.pick_from_local_storage)), REQUEST_CODE_SELECT_IMAGE) } protected abstract fun loadCurrentImage() @@ -295,9 +304,19 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { saveFab.isEnabled = true } + private fun hideFab() { + saveFab.animate() + .setDuration(500) + .setInterpolator(OvershootInterpolator()) + .scaleX(0.0f) + .scaleY(0.0f) + .start() + saveFab.isEnabled = false + } + protected fun setImageBitmap(bitmap: Bitmap?, bgColor: Int) { if (bitmap == null) { - editorImage.setImageResource(R.drawable.default_album_art) + editorImage.setImageResource(code.name.monkey.retromusic.R.drawable.default_album_art) } else { editorImage.setImageBitmap(bitmap) } @@ -312,16 +331,50 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { artworkInfo: ArtworkInfo?) { RetroUtil.hideSoftKeyboard(this) - WriteTagsAsyncTask(this) - .execute(WriteTagsAsyncTask.LoadingInfo(getSongPaths(), fieldKeyValueMap, artworkInfo)) + hideFab() + + savedSongPaths = getSongPaths() + savedTags = fieldKeyValueMap + savedArtworkInfo = artworkInfo + + if (!SAFUtil.isSAFRequired(savedSongPaths)) { + writeTags(savedSongPaths) + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (SAFUtil.isSDCardAccessGranted(this)) { + writeTags(savedSongPaths) + } else { + startActivityForResult(Intent(this, SAFGuideActivity::class.java), SAFGuideActivity.REQUEST_CODE_SAF_GUIDE) + } + } + } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) + private fun writeTags(paths: List?) { + WriteTagsAsyncTask(this).execute(WriteTagsAsyncTask.LoadingInfo(paths, savedTags, savedArtworkInfo)) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) when (requestCode) { REQUEST_CODE_SELECT_IMAGE -> if (resultCode == Activity.RESULT_OK) { - val selectedImage = data!!.data - loadImageFromFile(selectedImage) + intent?.data?.let { + loadImageFromFile(it) + } + } + SAFGuideActivity.REQUEST_CODE_SAF_GUIDE -> { + SAFUtil.openTreePicker(this) + } + SAFUtil.REQUEST_SAF_PICK_TREE -> { + if (resultCode == Activity.RESULT_OK) { + SAFUtil.saveTreeUri(this, intent) + writeTags(savedSongPaths) + } + } + SAFUtil.REQUEST_SAF_PICK_FILE -> { + if (resultCode == Activity.RESULT_OK) { + writeTags(Collections.singletonList(currentSongPath + SAFUtil.SEPARATOR + intent!!.dataString)) + } } } } @@ -335,7 +388,6 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { Log.e(TAG, "Could not read audio file $path", e) AudioFile() } - } class ArtworkInfo constructor(val albumId: Int, val artwork: Bitmap?) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt index b43ffc2e..f8346265 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt @@ -56,6 +56,7 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { songText.appHandleColor().addTextChangedListener(this) albumText.appHandleColor().addTextChangedListener(this) + albumArtistText.appHandleColor().addTextChangedListener(this) artistText.appHandleColor().addTextChangedListener(this) genreText.appHandleColor().addTextChangedListener(this) yearText.appHandleColor().addTextChangedListener(this) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java index 010a28f8..d9bfb86a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java @@ -5,43 +5,43 @@ import android.app.Dialog; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; -import com.afollestad.materialdialogs.MaterialDialog; -import com.afollestad.materialdialogs.bottomsheets.BottomSheet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.Collection; import java.util.Map; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; 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.util.MusicUtil; +import code.name.monkey.retromusic.util.SAFUtil; public class WriteTagsAsyncTask extends DialogAsyncTask { - private Context applicationContext; + private WeakReference activity; - public WriteTagsAsyncTask(Context context) { - super(context); - applicationContext = context; + public WriteTagsAsyncTask(@NonNull Activity activity) { + super(activity); + this.activity = new WeakReference<>(activity); } @Override @@ -68,6 +68,13 @@ public class WriteTagsAsyncTask extends for (String filePath : info.filePaths) { publishProgress(++counter, info.filePaths.size()); try { + Uri safUri = null; + if (filePath.contains(SAFUtil.SEPARATOR)) { + String[] fragments = filePath.split(SAFUtil.SEPARATOR); + filePath = fragments[0]; + safUri = Uri.parse(fragments[1]); + } + AudioFile audioFile = AudioFileIO.read(new File(filePath)); Tag tag = audioFile.getTagOrCreateAndSetDefault(); @@ -92,8 +99,10 @@ public class WriteTagsAsyncTask extends } } - audioFile.commit(); - } catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { + Activity activity = this.activity.get(); + SAFUtil.write(activity, audioFile, safUri); + + } catch (@NonNull Exception e) { e.printStackTrace(); } } @@ -107,7 +116,17 @@ public class WriteTagsAsyncTask extends } } - return info.filePaths.toArray(new String[info.filePaths.size()]); + Collection paths = info.filePaths; + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + paths = new ArrayList<>(info.filePaths.size()); + for (String path : info.filePaths) { + if (path.contains(SAFUtil.SEPARATOR)) + path = path.split(SAFUtil.SEPARATOR)[0]; + paths.add(path); + } + } + + return paths.toArray(new String[paths.size()]); } catch (Exception e) { e.printStackTrace(); return null; @@ -127,18 +146,20 @@ public class WriteTagsAsyncTask extends } private void scan(String[] toBeScanned) { - Context context = getContext(); - MediaScannerConnection.scanFile(applicationContext, toBeScanned, null, - context instanceof Activity ? new UpdateToastMediaScannerCompletionListener( - (Activity) context, toBeScanned) : null); + Activity activity = this.activity.get(); + if (activity != null) { + MediaScannerConnection.scanFile(activity, toBeScanned, null, new UpdateToastMediaScannerCompletionListener(activity, toBeScanned)); + } } @NonNull @Override protected Dialog createDialog(@NonNull Context context) { - return new MaterialDialog(context, new BottomSheet()) - .title(R.string.saving_changes, "") - .cancelable(false); + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.saving_changes) + .setCancelable(false) + .setView(R.layout.loading) + .create(); } @Override diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsAsyncTask.java b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsAsyncTask.java new file mode 100644 index 00000000..27b307d7 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsAsyncTask.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.dialogs; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.List; + +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.activities.saf.SAFGuideActivity; +import code.name.monkey.retromusic.misc.DialogAsyncTask; +import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.util.SAFUtil; + +/** + * Created by hemanths on 2019-07-31. + */ +public class DeleteSongsAsyncTask extends DialogAsyncTask { + private WeakReference dialogReference; + private WeakReference activityWeakReference; + + + public DeleteSongsAsyncTask(@NonNull DeleteSongsDialog dialog) { + super(dialog.getActivity()); + this.dialogReference = new WeakReference<>(dialog); + this.activityWeakReference = new WeakReference<>(dialog.getActivity()); + } + + @NonNull + @Override + protected Dialog createDialog(@NonNull Context context) { + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.deleting_songs) + .setView(R.layout.loading) + .setCancelable(false) + .create(); + } + + @Nullable + @Override + protected Void doInBackground(@NonNull LoadingInfo... loadingInfos) { + try { + LoadingInfo info = loadingInfos[0]; + DeleteSongsDialog dialog = this.dialogReference.get(); + FragmentActivity fragmentActivity = this.activityWeakReference.get(); + + if (dialog == null || fragmentActivity == null) { + return null; + } + + if (!info.isIntent) { + if (!SAFUtil.isSAFRequiredForSongs(info.songs)) { + dialog.deleteSongs(info.songs, null); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (SAFUtil.isSDCardAccessGranted(fragmentActivity)) { + dialog.deleteSongs(info.songs, null); + } else { + dialog.startActivityForResult(new Intent(fragmentActivity, SAFGuideActivity.class), SAFGuideActivity.REQUEST_CODE_SAF_GUIDE); + } + } else { + Log.i("Hmm", "doInBackground: kitkat delete songs"); + } + } + } else { + switch (info.requestCode) { + case SAFUtil.REQUEST_SAF_PICK_TREE: + if (info.resultCode == Activity.RESULT_OK) { + SAFUtil.saveTreeUri(fragmentActivity, info.intent); + if (dialog.songsToRemove != null) { + dialog.deleteSongs(dialog.songsToRemove, null); + } + } + break; + case SAFUtil.REQUEST_SAF_PICK_FILE: + if (info.resultCode == Activity.RESULT_OK) { + dialog.deleteSongs(Collections.singletonList(dialog.currentSong), Collections.singletonList(info.intent.getData())); + } + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public static class LoadingInfo { + public boolean isIntent; + + public List songs; + public List safUris; + + public int requestCode; + public int resultCode; + public Intent intent; + + public LoadingInfo(List songs, List safUris) { + this.isIntent = false; + this.songs = songs; + this.safUris = safUris; + } + + public LoadingInfo(int requestCode, int resultCode, Intent intent) { + this.isIntent = true; + this.requestCode = requestCode; + this.resultCode = resultCode; + this.intent = intent; + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt index 344d43a9..da6e3f02 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt @@ -15,41 +15,85 @@ package code.name.monkey.retromusic.dialogs import android.app.Dialog +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Html import androidx.fragment.app.DialogFragment import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.saf.SAFGuideActivity +import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil +import code.name.monkey.retromusic.util.SAFUtil import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.bottomsheets.BottomSheet class DeleteSongsDialog : DialogFragment() { + @JvmField + var currentSong: Song? = null + @JvmField + var songsToRemove: List? = null + + private var deleteSongsAsyncTask: DeleteSongsAsyncTask? = null + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val songs = arguments!!.getParcelableArrayList("songs") - val title: Int - val content: CharSequence - if (songs.size > 1) { - title = R.string.delete_songs_title - content = Html.fromHtml(getString(R.string.delete_x_songs, songs.size)) - } else { - title = R.string.delete_song_title - content = Html.fromHtml(getString(R.string.delete_song_x, songs.get(0).title)) + val songs: ArrayList? = arguments?.getParcelableArrayList("songs") + var title = 0 + var content: CharSequence = "" + if (songs != null) { + if (songs.size > 1) { + title = R.string.delete_songs_title + content = Html.fromHtml(getString(R.string.delete_x_songs, songs.size)) + } else { + title = R.string.delete_song_title + content = Html.fromHtml(getString(R.string.delete_song_x, songs[0].title)) + } } - return MaterialDialog(activity!!, BottomSheet()).show { + + return MaterialDialog(requireActivity(), BottomSheet()).show { title(title) message(text = content) - negativeButton(android.R.string.cancel) + negativeButton(android.R.string.cancel) { + dismiss() + } + noAutoDismiss() positiveButton(R.string.action_delete) { - if (activity == null) - return@positiveButton - MusicUtil.deleteTracks(activity!!, songs); + if (songs != null) { + if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) { + MusicPlayerRemote.playNextSong() + } + } + + songsToRemove = songs + deleteSongsAsyncTask = DeleteSongsAsyncTask(this@DeleteSongsDialog) + deleteSongsAsyncTask?.execute(DeleteSongsAsyncTask.LoadingInfo(songs, null)) } } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + SAFGuideActivity.REQUEST_CODE_SAF_GUIDE -> { + SAFUtil.openTreePicker(this) + } + SAFUtil.REQUEST_SAF_PICK_TREE, + SAFUtil.REQUEST_SAF_PICK_FILE -> { + if (deleteSongsAsyncTask != null) { + deleteSongsAsyncTask?.cancel(true) + } + deleteSongsAsyncTask = DeleteSongsAsyncTask(this) + deleteSongsAsyncTask?.execute(DeleteSongsAsyncTask.LoadingInfo(requestCode, resultCode, data)) + } + } + } + + fun deleteSongs(songs: List, safUris: List?) { + MusicUtil.deleteTracks(activity!!, songs, safUris) { this.dismiss() } + } companion object { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt index 412d2475..aac03f73 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt @@ -15,14 +15,14 @@ import android.widget.Toast import androidx.appcompat.widget.Toolbar import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.tageditor.AbsTagEditorActivity +import code.name.monkey.retromusic.activities.tageditor.SongTagEditorActivity import code.name.monkey.retromusic.dialogs.* +import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.PaletteColorHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.lyrics.Lyrics -import code.name.monkey.retromusic.activities.tageditor.AbsTagEditorActivity -import code.name.monkey.retromusic.activities.tageditor.SongTagEditorActivity -import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.util.* import code.name.monkey.retromusic.views.FitSystemWindowsLayout import java.io.FileNotFoundException @@ -57,20 +57,15 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem return true } R.id.action_share -> { - if (fragmentManager != null) { - SongShareDialog.create(song).show(fragmentManager!!, "SHARE_SONG") - } + SongShareDialog.create(song).show(requireFragmentManager(), "SHARE_SONG") return true } R.id.action_delete_from_device -> { - DeleteSongsDialog.create(song) - .show(activity!!.supportFragmentManager, "DELETE_SONGS") + DeleteSongsDialog.create(song).show(requireFragmentManager(), "DELETE_SONGS") return true } R.id.action_add_to_playlist -> { - if (fragmentManager != null) { - AddToPlaylistDialog.create(song).show(fragmentManager!!, "ADD_PLAYLIST") - } + AddToPlaylistDialog.create(song).show(requireFragmentManager(), "ADD_PLAYLIST") return true } R.id.action_clear_playing_queue -> { @@ -79,7 +74,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem } R.id.action_save_playing_queue -> { CreatePlaylistDialog.create(MusicPlayerRemote.playingQueue) - .show(activity!!.supportFragmentManager, "ADD_TO_PLAYLIST") + .show(requireFragmentManager(), "ADD_TO_PLAYLIST") return true } R.id.action_tag_editor -> { @@ -89,45 +84,43 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem return true } R.id.action_details -> { - if (fragmentManager != null) { - SongDetailDialog.create(song).show(fragmentManager!!, "SONG_DETAIL") - } + SongDetailDialog.create(song).show(requireFragmentManager(), "SONG_DETAIL") return true } R.id.action_go_to_album -> { - NavigationUtil.goToAlbum(activity!!, song.albumId) + NavigationUtil.goToAlbum(requireActivity(), song.albumId) return true } R.id.action_go_to_artist -> { - NavigationUtil.goToArtist(activity!!, song.artistId) + NavigationUtil.goToArtist(requireActivity(), song.artistId) return true } R.id.now_playing -> { - NavigationUtil.goToPlayingQueue(activity!!) + NavigationUtil.goToPlayingQueue(requireActivity()) return true } R.id.action_show_lyrics -> { - NavigationUtil.goToLyrics(activity!!) + NavigationUtil.goToLyrics(requireActivity()) return true } R.id.action_equalizer -> { - NavigationUtil.openEqualizer(activity!!) + NavigationUtil.openEqualizer(requireActivity()) return true } R.id.action_sleep_timer -> { - SleepTimerDialog().show(fragmentManager!!, TAG) + SleepTimerDialog().show(requireFragmentManager(), TAG) return true } R.id.action_set_as_ringtone -> { - if (RingtoneManager.requiresDialog(activity!!)) { - RingtoneManager.getDialog(activity!!) + if (RingtoneManager.requiresDialog(requireActivity())) { + RingtoneManager.getDialog(requireActivity()) } - val ringtoneManager = RingtoneManager(activity!!) + val ringtoneManager = RingtoneManager(requireActivity()) ringtoneManager.setRingtone(song) return true } R.id.action_settings -> { - NavigationUtil.goToSettings(activity!!) + NavigationUtil.goToSettings(requireActivity()) return true } R.id.action_go_to_genre -> { @@ -146,7 +139,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem } protected open fun toggleFavorite(song: Song) { - MusicUtil.toggleFavorite(activity!!, song) + MusicUtil.toggleFavorite(requireActivity(), song) } abstract fun playerToolbar(): Toolbar @@ -252,7 +245,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.setBackgroundColor(ThemeStore.primaryColor(activity!!)) + view.setBackgroundColor(ThemeStore.primaryColor(requireActivity())) if (PreferenceUtil.getInstance().fullScreenMode && view.findViewById(R.id.status_bar) != null) { view.findViewById(R.id.status_bar).visibility = View.GONE } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt index 5f1ade9c..a707ac1c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt @@ -26,8 +26,6 @@ import android.provider.DocumentsContract import android.provider.MediaStore import android.util.Log import android.widget.Toast - -import code.name.monkey.retromusic.R import code.name.monkey.retromusic.loaders.SongLoader import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.service.MusicService @@ -45,6 +43,13 @@ object MusicPlayerRemote { val isPlaying: Boolean get() = musicService != null && musicService!!.isPlaying + fun isPlaying(song: Song): Boolean { + return if (!isPlaying) { + false + } else song.id == currentSong.id + } + + val currentSong: Song get() = if (musicService != null) { musicService!!.currentSong @@ -278,7 +283,7 @@ object MusicPlayerRemote { queue.add(song) openQueue(queue, 0, false) } - Toast.makeText(musicService, musicService!!.resources.getString(R.string.added_title_to_playing_queue), Toast.LENGTH_SHORT).show() + Toast.makeText(musicService, musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_title_to_playing_queue), Toast.LENGTH_SHORT).show() return true } return false @@ -291,7 +296,7 @@ object MusicPlayerRemote { } else { openQueue(songs, 0, false) } - val toast = if (songs.size == 1) musicService!!.resources.getString(R.string.added_title_to_playing_queue) else musicService!!.resources.getString(R.string.added_x_titles_to_playing_queue, songs.size) + val toast = if (songs.size == 1) musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_title_to_playing_queue) else musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_x_titles_to_playing_queue, songs.size) Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show() return true } @@ -307,7 +312,7 @@ object MusicPlayerRemote { queue.add(song) openQueue(queue, 0, false) } - Toast.makeText(musicService, musicService!!.resources.getString(R.string.added_title_to_playing_queue), Toast.LENGTH_SHORT).show() + Toast.makeText(musicService, musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_title_to_playing_queue), Toast.LENGTH_SHORT).show() return true } return false @@ -320,7 +325,7 @@ object MusicPlayerRemote { } else { openQueue(songs, 0, false) } - val toast = if (songs.size == 1) musicService!!.resources.getString(R.string.added_title_to_playing_queue) else musicService!!.resources.getString(R.string.added_x_titles_to_playing_queue, songs.size) + val toast = if (songs.size == 1) musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_title_to_playing_queue) else musicService!!.resources.getString(code.name.monkey.retromusic.R.string.added_x_titles_to_playing_queue, songs.size) Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show() return true } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java index 87c9f7f9..342e74d9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java @@ -57,7 +57,6 @@ public final class FileUtil { stream.close(); return baos.toByteArray(); } - @NonNull public static Observable> matchFilesWithMediaStore(@NonNull Context context, @Nullable List files) { @@ -263,4 +262,6 @@ public final class FileUtil { return file.getAbsoluteFile(); } } + + } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java index 4f75cd61..76bc6b92 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java @@ -26,7 +26,6 @@ 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.NonNull; @@ -263,64 +262,80 @@ public class MusicUtil { } public static void deleteTracks(@NonNull final Activity activity, - @NonNull final List songs) { + @NonNull final List songs, + @Nullable final List safUris, + @Nullable final Runnable callback) { final String[] projection = new String[]{ BaseColumns._ID, MediaStore.MediaColumns.DATA }; - final StringBuilder selection = new StringBuilder(); - selection.append(BaseColumns._ID + " IN ("); - for (int i = 0; i < songs.size(); i++) { - selection.append(songs.get(i).getId()); - if (i < songs.size() - 1) { + + // Split the query into multiple batches, and merge the resulting cursors + int batchStart = 0; + int batchEnd = 0; + final int batchSize = 1000000 / 10; // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID + final int songCount = songs.size(); + + while (batchEnd < songCount) { + batchStart = batchEnd; + + final StringBuilder selection = new StringBuilder(); + selection.append(BaseColumns._ID + " IN ("); + + for (int i = 0; (i < batchSize - 1) && (batchEnd < songCount - 1); i++, batchEnd++) { + selection.append(songs.get(batchEnd).getId()); selection.append(","); } - } - selection.append(")"); + // The last element of a batch + selection.append(songs.get(batchEnd).getId()); + batchEnd++; + selection.append(")"); - try { - final Cursor cursor = activity.getContentResolver().query( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(), - null, null); - if (cursor != null) { - // Step 1: Remove selected tracks from the current playlist, as well - // as from the album art cache - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - final int id = cursor.getInt(0); - Song song = SongLoader.INSTANCE.getSong(activity, id).blockingFirst(); - MusicPlayerRemote.INSTANCE.removeFromQueue(song); - cursor.moveToNext(); - } + try { + final Cursor cursor = activity.getContentResolver().query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(), + null, null); + // TODO: At this point, there is no guarantee that the size of the cursor is the same as the size of the selection string. + // Despite that, the Step 3 assumes that the safUris elements are tracking closely the content of the cursor. - // Step 2: Remove selected tracks from the database - activity.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - selection.toString(), null); - - // Step 3: Remove files from card - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - final String name = cursor.getString(1); - try { // File.delete can throw a security exception - final File f = new File(name); - if (!f.delete()) { - // I'm not sure if we'd ever get here (deletion would - // have to fail, but no exception thrown) - Log.e("MusicUtils", "Failed to delete file " + name); - } + if (cursor != null) { + // Step 1: Remove selected tracks from the current playlist, as well + // as from the album art cache + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + final int id = cursor.getInt(0); + final Song song = SongLoader.INSTANCE.getSong(activity, id).blockingFirst(); + MusicPlayerRemote.INSTANCE.removeFromQueue(song); cursor.moveToNext(); - } catch (@NonNull final SecurityException ex) { - cursor.moveToNext(); - } catch (NullPointerException e) { - Log.e("MusicUtils", "Failed to find file " + name); } + + // Step 2: Remove selected tracks from the database + activity.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + selection.toString(), null); + + // Step 3: Remove files from card + cursor.moveToFirst(); + int i = batchStart; + while (!cursor.isAfterLast()) { + final String name = cursor.getString(1); + final Uri safUri = safUris == null || safUris.size() <= i ? null : safUris.get(i); + SAFUtil.delete(activity, name, safUri); + i++; + cursor.moveToNext(); + } + cursor.close(); } - cursor.close(); + } catch (SecurityException ignored) { } - activity.getContentResolver().notifyChange(Uri.parse("content://media"), null); - Toast.makeText(activity, activity.getString(R.string.deleted_x_songs, songs.size()), - Toast.LENGTH_SHORT).show(); - } catch (SecurityException ignored) { } + + activity.getContentResolver().notifyChange(Uri.parse("content://media"), null); + + activity.runOnUiThread(() -> { + Toast.makeText(activity, activity.getString(R.string.deleted_x_songs, songCount), Toast.LENGTH_SHORT).show(); + if (callback != null) { + callback.run(); + } + }); } public static void deleteAlbumArt(@NonNull Context context, int albumId) { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.java index 56105406..761eabd8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.TypedArray; +import android.net.Uri; import android.preference.PreferenceManager; import androidx.annotation.LayoutRes; @@ -87,6 +88,7 @@ public final class PreferenceUtil { public static final String ALBUM_COVER_STYLE = "album_cover_style_id"; public static final String ALBUM_COVER_TRANSFORM = "album_cover_transform"; public static final String TAB_TEXT_MODE = "tab_text_mode"; + public static final String SAF_SDCARD_URI = "saf_sdcard_uri"; private static final String GENRE_SORT_ORDER = "genre_sort_order"; private static final String LAST_PAGE = "last_start_page"; private static final String LAST_MUSIC_CHOOSER = "last_music_chooser"; @@ -281,7 +283,6 @@ public final class PreferenceUtil { return Integer.parseInt(mPreferences.getString(DEFAULT_START_PAGE, "-1")); } - public final int getLastPage() { return mPreferences.getInt(LAST_PAGE, R.id.action_song); } @@ -292,7 +293,6 @@ public final class PreferenceUtil { editor.apply(); } - public void setLastLyricsType(int group) { final SharedPreferences.Editor editor = mPreferences.edit(); editor.putInt(LAST_KNOWN_LYRICS_TYPE, group); @@ -388,7 +388,6 @@ public final class PreferenceUtil { return mPreferences.getBoolean(IGNORE_MEDIA_STORE_ARTWORK, false); } - public int getLastSleepTimerValue() { return mPreferences.getInt(LAST_SLEEP_TIMER_VALUE, 30); } @@ -673,7 +672,6 @@ public final class PreferenceUtil { return mPreferences.getBoolean(TOGGLE_HEADSET, false); } - public boolean isDominantColor() { return mPreferences.getBoolean(DOMINANT_COLOR, false); } @@ -698,7 +696,6 @@ public final class PreferenceUtil { mPreferences.edit().putBoolean(CIRCULAR_ALBUM_ART, false).apply(); } - public String getAlbumDetailsStyle() { return mPreferences.getString(ALBUM_DETAIL_STYLE, "0"); } @@ -738,7 +735,6 @@ public final class PreferenceUtil { return mPreferences.getBoolean(PAUSE_ON_ZERO_VOLUME, false); } - public ViewPager.PageTransformer getAlbumCoverTransform() { int style = Integer.parseInt(Objects.requireNonNull(mPreferences.getString(ALBUM_COVER_TRANSFORM, "0"))); switch (style) { @@ -859,4 +855,12 @@ public final class PreferenceUtil { defaultCategoryInfos.add(new CategoryInfo(CategoryInfo.Category.GENRES, false)); return defaultCategoryInfos; } + + public final String getSAFSDCardUri() { + return mPreferences.getString(SAF_SDCARD_URI, ""); + } + + public final void setSAFSDCardUri(Uri uri) { + mPreferences.edit().putString(SAF_SDCARD_URI, uri.toString()).apply(); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java new file mode 100644 index 00000000..0fc91e19 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.util; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.UriPermission; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; +import androidx.fragment.app.Fragment; + +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.exceptions.CannotWriteException; +import org.jaudiotagger.audio.generic.Utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.model.Song; + +public class SAFUtil { + + public static final String TAG = SAFUtil.class.getSimpleName(); + public static final String SEPARATOR = "###/SAF/###"; + + public static final int REQUEST_SAF_PICK_FILE = 42; + public static final int REQUEST_SAF_PICK_TREE = 43; + + public static boolean isSAFRequired(File file) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !file.canWrite(); + } + + public static boolean isSAFRequired(String path) { + return isSAFRequired(new File(path)); + } + + public static boolean isSAFRequired(AudioFile audio) { + return isSAFRequired(audio.getFile()); + } + + public static boolean isSAFRequired(Song song) { + return isSAFRequired(song.getData()); + } + + public static boolean isSAFRequired(List paths) { + for (String path : paths) { + if (isSAFRequired(path)) return true; + } + return false; + } + + public static boolean isSAFRequiredForSongs(List songs) { + for (Song song : songs) { + if (isSAFRequired(song)) return true; + } + return false; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openFilePicker(Activity activity) { + Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("audio/*"); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openFilePicker(Fragment fragment) { + Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("audio/*"); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void openTreePicker(Activity activity) { + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void openTreePicker(Fragment fragment) { + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void saveTreeUri(Context context, Intent data) { + Uri uri = data.getData(); + context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + PreferenceUtil.getInstance().setSAFSDCardUri(uri); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean isTreeUriSaved(Context context) { + return !TextUtils.isEmpty(PreferenceUtil.getInstance().getSAFSDCardUri()); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean isSDCardAccessGranted(Context context) { + if (!isTreeUriSaved(context)) return false; + + String sdcardUri = PreferenceUtil.getInstance().getSAFSDCardUri(); + + List perms = context.getContentResolver().getPersistedUriPermissions(); + for (UriPermission perm : perms) { + if (perm.getUri().toString().equals(sdcardUri) && perm.isWritePermission()) return true; + } + + return false; + } + + /** + * https://github.com/vanilla-music/vanilla-music-tag-editor/commit/e00e87fef289f463b6682674aa54be834179ccf0#diff-d436417358d5dfbb06846746d43c47a5R359 + * Finds needed file through Document API for SAF. It's not optimized yet - you can still gain wrong URI on + * files such as "/a/b/c.mp3" and "/b/a/c.mp3", but I consider it complete enough to be usable. + * + * @param dir - document file representing current dir of search + * @param segments - path segments that are left to find + * @return URI for found file. Null if nothing found. + */ + @Nullable + public static Uri findDocument(DocumentFile dir, List segments) { + for (DocumentFile file : dir.listFiles()) { + int index = segments.indexOf(file.getName()); + if (index == -1) { + continue; + } + + if (file.isDirectory()) { + segments.remove(file.getName()); + return findDocument(file, segments); + } + + if (file.isFile() && index == segments.size() - 1) { + // got to the last part + return file.getUri(); + } + } + + return null; + } + + public static void write(Context context, AudioFile audio, Uri safUri) { + if (isSAFRequired(audio)) { + writeSAF(context, audio, safUri); + } else { + try { + writeFile(audio); + } catch (CannotWriteException e) { + e.printStackTrace(); + } + } + } + + public static void writeFile(AudioFile audio) throws CannotWriteException { + audio.commit(); + } + + public static void writeSAF(Context context, AudioFile audio, Uri safUri) { + Uri uri = null; + + if (context == null) { + Log.e(TAG, "writeSAF: context == null"); + return; + } + + if (isTreeUriSaved(context)) { + List pathSegments = new ArrayList<>(Arrays.asList(audio.getFile().getAbsolutePath().split("/"))); + Uri sdcard = Uri.parse(PreferenceUtil.getInstance().getSAFSDCardUri()); + uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); + } + + if (uri == null) { + uri = safUri; + } + + if (uri == null) { + Log.e(TAG, "writeSAF: Can't get SAF URI"); + toast(context, context.getString(R.string.saf_error_uri)); + return; + } + + try { + // copy file to app folder to use jaudiotagger + final File original = audio.getFile(); + File temp = File.createTempFile("tmp-media", '.' + Utils.getExtension(original)); + Utils.copy(original, temp); + temp.deleteOnExit(); + audio.setFile(temp); + writeFile(audio); + + ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "rw"); + if (pfd == null) { + Log.e(TAG, "writeSAF: SAF provided incorrect URI: " + uri); + return; + } + + // now read persisted data and write it to real FD provided by SAF + FileInputStream fis = new FileInputStream(temp); + byte[] audioContent = FileUtil.readBytes(fis); + FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor()); + fos.write(audioContent); + fos.close(); + + temp.delete(); + } catch (final Exception e) { + Log.e(TAG, "writeSAF: Failed to write to file descriptor provided by SAF", e); + + toast(context, String.format(context.getString(R.string.saf_write_failed), e.getLocalizedMessage())); + } + } + + public static void delete(Context context, String path, Uri safUri) { + if (isSAFRequired(path)) { + deleteSAF(context, path, safUri); + } else { + try { + deleteFile(path); + } catch (NullPointerException e) { + Log.e("MusicUtils", "Failed to find file " + path); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void deleteFile(String path) { + new File(path).delete(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void deleteSAF(Context context, String path, Uri safUri) { + Uri uri = null; + + if (context == null) { + Log.e(TAG, "deleteSAF: context == null"); + return; + } + + if (isTreeUriSaved(context)) { + List pathSegments = new ArrayList<>(Arrays.asList(path.split("/"))); + Uri sdcard = Uri.parse(PreferenceUtil.getInstance().getSAFSDCardUri()); + uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); + } + + if (uri == null) { + uri = safUri; + } + + if (uri == null) { + Log.e(TAG, "deleteSAF: Can't get SAF URI"); + toast(context, context.getString(R.string.saf_error_uri)); + return; + } + + try { + DocumentsContract.deleteDocument(context.getContentResolver(), uri); + } catch (final Exception e) { + Log.e(TAG, "deleteSAF: Failed to delete a file descriptor provided by SAF", e); + + toast(context, String.format(context.getString(R.string.saf_delete_failed), e.getLocalizedMessage())); + } + } + + private static void toast(final Context context, final String message) { + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); + } + } + +} diff --git a/app/src/main/res/drawable-v21/saf_guide_1.png b/app/src/main/res/drawable-v21/saf_guide_1.png new file mode 100644 index 00000000..131ddc5e Binary files /dev/null and b/app/src/main/res/drawable-v21/saf_guide_1.png differ diff --git a/app/src/main/res/drawable-v21/saf_guide_2.png b/app/src/main/res/drawable-v21/saf_guide_2.png new file mode 100644 index 00000000..be7fdb78 Binary files /dev/null and b/app/src/main/res/drawable-v21/saf_guide_2.png differ diff --git a/app/src/main/res/drawable-v21/saf_guide_3.png b/app/src/main/res/drawable-v21/saf_guide_3.png new file mode 100644 index 00000000..18fcccfb Binary files /dev/null and b/app/src/main/res/drawable-v21/saf_guide_3.png differ diff --git a/app/src/main/res/drawable-v26/saf_guide_1.png b/app/src/main/res/drawable-v26/saf_guide_1.png new file mode 100644 index 00000000..1ad76c68 Binary files /dev/null and b/app/src/main/res/drawable-v26/saf_guide_1.png differ diff --git a/app/src/main/res/drawable-v26/saf_guide_2.png b/app/src/main/res/drawable-v26/saf_guide_2.png new file mode 100644 index 00000000..32fee2ed Binary files /dev/null and b/app/src/main/res/drawable-v26/saf_guide_2.png differ diff --git a/app/src/main/res/drawable-v26/saf_guide_3.png b/app/src/main/res/drawable-v26/saf_guide_3.png new file mode 100644 index 00000000..d0e28be7 Binary files /dev/null and b/app/src/main/res/drawable-v26/saf_guide_3.png differ diff --git a/app/src/main/res/layout/fragment_simple_slide_large_image.xml b/app/src/main/res/layout/fragment_simple_slide_large_image.xml new file mode 100644 index 00000000..14c7d8eb --- /dev/null +++ b/app/src/main/res/layout/fragment_simple_slide_large_image.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/loading.xml b/app/src/main/res/layout/loading.xml new file mode 100644 index 00000000..54c6d3c1 --- /dev/null +++ b/app/src/main/res/layout/loading.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a69f7e8c..083e31c6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -420,7 +420,8 @@ 合页 选择一个预设 开启 - 随机播放 ` + 随机播放 + ` 加入带有时间轴的歌词 设置铃声 请允许 Retro Music 更改音频设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3129be7..275ab1e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -632,4 +632,21 @@ Configure visibility and order of library categories. Controls Album style + + + + Can\'t get SAF URI + File write failed: %s + File delete failed: %s + SD card access required. Please pick root directory of SD card + File access required. Pick %s + + %s needs SD card access + Enable \'Show SD card\' in overflow menu + Open navigation drawer + Select your SD card in navigation drawer + You need to select your SD card root directory + Tap \'select\' button at the bottom of the screen + Do not open any sub-folders + Deleting songs diff --git a/appthemehelper/src/main/res/values/colors_material_design.xml b/appthemehelper/src/main/res/values/colors_material_design.xml index 3fd119ce..50014332 100755 --- a/appthemehelper/src/main/res/values/colors_material_design.xml +++ b/appthemehelper/src/main/res/values/colors_material_design.xml @@ -19,13 +19,25 @@ + #651FFF + #9575CD + #7E57C2 + #5E35B1 + #512DA8 + #4527A0 #30673AB7 #673AB7 #6200EA #3F51B5 + #7986CB + #5C6BC0 + #3949AB + #303F9F + #283593 + #2196F3