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