From 570a235836907893546b9e098e860236f5995aae Mon Sep 17 00:00:00 2001 From: h4h13 Date: Wed, 31 Jul 2019 22:12:19 +0530 Subject: [PATCH 1/2] SAF is fixed --- app/build.gradle | 11 +- app/src/main/AndroidManifest.xml | 1 + .../activities/saf/SAFGuideActivity.java | 70 ++++ .../tageditor/AbsTagEditorActivity.kt | 78 ++++- .../tageditor/SongTagEditorActivity.kt | 1 + .../tageditor/WriteTagsAsyncTask.java | 67 ++-- .../dialogs/DeleteSongsAsyncTask.java | 138 ++++++++ .../retromusic/dialogs/DeleteSongsDialog.kt | 72 ++++- .../fragments/base/AbsPlayerFragment.kt | 47 ++- .../retromusic/helper/MusicPlayerRemote.kt | 17 +- .../name/monkey/retromusic/util/FileUtil.java | 3 +- .../monkey/retromusic/util/MusicUtil.java | 109 ++++--- .../retromusic/util/PreferenceUtil.java | 16 +- .../name/monkey/retromusic/util/SAFUtil.java | 303 ++++++++++++++++++ app/src/main/res/drawable-v21/saf_guide_1.png | Bin 0 -> 5365 bytes app/src/main/res/drawable-v21/saf_guide_2.png | Bin 0 -> 6655 bytes app/src/main/res/drawable-v21/saf_guide_3.png | Bin 0 -> 11759 bytes app/src/main/res/drawable-v26/saf_guide_1.png | Bin 0 -> 7391 bytes app/src/main/res/drawable-v26/saf_guide_2.png | Bin 0 -> 8328 bytes app/src/main/res/drawable-v26/saf_guide_3.png | Bin 0 -> 6786 bytes .../fragment_simple_slide_large_image.xml | 56 ++++ app/src/main/res/layout/loading.xml | 28 ++ app/src/main/res/values-zh-rCN/strings.xml | 3 +- app/src/main/res/values/strings.xml | 17 + .../res/values/colors_material_design.xml | 12 + 25 files changed, 907 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java create mode 100644 app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsAsyncTask.java create mode 100644 app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java create mode 100644 app/src/main/res/drawable-v21/saf_guide_1.png create mode 100644 app/src/main/res/drawable-v21/saf_guide_2.png create mode 100644 app/src/main/res/drawable-v21/saf_guide_3.png create mode 100644 app/src/main/res/drawable-v26/saf_guide_1.png create mode 100644 app/src/main/res/drawable-v26/saf_guide_2.png create mode 100644 app/src/main/res/drawable-v26/saf_guide_3.png create mode 100644 app/src/main/res/layout/fragment_simple_slide_large_image.xml create mode 100644 app/src/main/res/layout/loading.xml 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 0000000000000000000000000000000000000000..131ddc5e5ad7e7ccc9bcae9a5331184e46e2b119 GIT binary patch literal 5365 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 E5BqWkHvj+t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be7fdb78d3bf9f8b0708a012c669609b8ae227d5 GIT binary patch literal 6655 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 Xv3Ot3%)ehe002}~=Si(HJp7*kz)zzJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..18fcccfbda4a8cc14233132103daea1cd2428ce9 GIT binary patch literal 11759 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1ad76c6817592feaf97632bd394140c519334557 GIT binary patch literal 7391 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 literal 0 HcmV?d00001 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 From 70dda1e85fa708212bb593a101dd78fc78a69638 Mon Sep 17 00:00:00 2001 From: h4h13 Date: Thu, 1 Aug 2019 00:55:38 +0530 Subject: [PATCH 2/2] Removed unsed libs --- app/build.gradle | 5 ++--- app/src/main/assets/index.html | 4 ++-- app/src/main/assets/retro-changelog.html | 2 +- .../java/code/name/monkey/retromusic/App.kt | 6 ------ .../activities/base/AbsThemeActivity.kt | 3 +++ .../dialogs/OptionsSheetDialogFragment.kt | 10 +++++----- .../mainactivity/folders/FoldersFragment.java | 18 ++++++++---------- .../retromusic/views/OptionMenuItemView.java | 3 +++ .../main/res/drawable/menu_item_background.xml | 2 +- .../main/res/layout/fragment_main_options.xml | 15 ++++++--------- 10 files changed, 31 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d3959726..b4ff5fc6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 346 - versionName '3.2.240' + versionCode 347 + versionName '3.3.00' multiDexEnabled true @@ -139,7 +139,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'org.eclipse.mylyn.github:org.eclipse.egit.github.core:3.4.0.201406110918-r' - implementation 'com.github.takahirom.downloadable.calligraphy:downloadable-calligraphy:0.1.3' implementation 'com.github.ksoichiro:android-observablescrollview:1.6.0' implementation 'com.github.kabouzeid:RecyclerView-FastScroll:1.0.16-kmod' implementation 'com.github.kabouzeid:AndroidSlidingUpPanel:3.3.0-kmod3' diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 7ad6c3c0..1bea4ec6 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -28,9 +28,9 @@

Phonograph by Karim Abou Zeid

-

RxAndroid by RxAndroid authors

+

VinylMusicPlayer by Adrien Poupa

RxJava by RxJava authors

-

Material Dialogs by Aidan Michael Follestad

+

Material Dialogs and Cab by Aidan Michael Follestad

Calligraphy by RxJava authors

Android-Snowfall by JetRadar

Android Sliding Up Panelby The Umano Team

diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html index 5a48d106..cf09b953 100644 --- a/app/src/main/assets/retro-changelog.html +++ b/app/src/main/assets/retro-changelog.html @@ -1 +1 @@ -

v3.2.240

  • Improved options dialog with selected color and title icon
  • Fix dialog color on dark theme

v3.2.220

  • Fix Notification SeekBar position (Need Android Q test)
  • Rolled back settings tint icons
  • Changed preference category title style according Android Q
  • Fix tabs options not working

v3.2.203

  • Carousel effect improved
  • Settings icons are now tint accent color
  • Settings title, back arrow and icons are now tint accent color
  • Equalizer is removed
  • Fix keyboard not popping on Search
  • Curated the main options
  • Folder toolbar outlined, folder icon background fix and separate line removed
  • Edit text input handles are colored

v3.2.135

  • Fix crashing on sharing song

v3.2.125

  • Fix crashing on selecting profile picture
  • Semi transparent color fix(s) on now playing themes

v3.2.120

  • Fix crashing on choosing a theme
  • Fix color theme selection without pro enabled
  • Fix icon tint some places

v3.2.105

  • Fix color notifications

v3.2.100

  • Fix crashing on Sleep timer
  • Toolbar elevation removed added stroke instead
  • Show empty indication for home

v3.2.000

  • Implemented Artist image loading all thanks to VinylMusicPlayer
  • Fixed buttons in Genre details
  • Fixed color buttons in Album and Artist details screen

v3.1.900

  • Added Play and Shuffle buttons on Songs list, Album Details, Artist Details, Playlist Details etc.
  • Home toolbar is semi transparent in Banner mode
  • Added new Buy Retro Music pro in settings
  • Improved dark color in Dark theme

v3.1.850

  • Toolbar will be clickable for Search

v3.1.800

  • Search bar CardView background
  • Improve volume zero
  • Now playing and Album theme picker rollback to dialog
  • Fix sharing app link
  • Fix scanning dialog
  • Added settings icons for options

v3.1.700

  • Cleaned internal code
  • Removed full screen option
  • Added Toolbar elevation
  • To access menu either tap on Toolbar or Hamburger icon
  • Fix back button not working on playing queue
  • Fix crashing on What's New screen
  • Fix lyrics dialog
  • Changed toggles to line icons
  • Custom UserImageView for loading user profile image
  • Fix crashing on artist list for number format error
  • Fix blacklist dialog crashing
  • Rearranged icons and main menu access
  • Fix some crashes when device is locked or background
  • Folder screen have main options access
  • Dialogs are now using Material Dialogs v3(BottomSheet)
  • Fix Shuffle icon for Artist, Album, Genre and Playlist details

v3.1.400

  • Removed sync lyrics for Android 5
  • Fix Seek-bar color in settings
  • Added keyboard to popup on search
  • Added keyboard to popup on search
  • Improved lock-screen behavior and UI
  • Improved text appearance
  • Fix bio text not showing in settings
  • Fix not showing slider(blur, filter song) amount in settings
  • Fix setting ringtone
  • Fix file sharing crash
  • Fix some crashes
  • Fix playlist icon on small devices
  • Fix empty lyrics text color
  • Fix album cover background purple color in color theme

v3.1.300

  • Fix rename playlist text color
  • Fix same album showing in details page
  • Fix lyrics text alignment on sync and lyrics reading improved
  • Improved home sections loading
  • Removed library options which are duplicated (it's available from profile menu)
  • Replaced collapsing Fab with Android Floating Extended Fab
  • Replaced home with for you
  • Fixed profile image not loading in about
  • Improved selecting user profile image
  • Added bio to enter custom message
  • Improved some UI screens

v3.1.240

  • Fix Search not showing from home screen
  • Fix Volume controls color issue
  • Fix Seek bar alignment
  • Added tiny theme
  • Improved full theme appearances
  • Now playing theme preview updated
  • Fix composer error
  • Bottom Options improved(internal)

v3.1.200

  • Added composer sort and editing
  • Fix Crash in Album tag editor while selecting options
  • Added Filter song length
  • Added Favourites playlist icon will be accent color
  • Added Colorful settings icons
  • Added Corners for dialog

v3.0.570

  • Fix Album/Artist square image
  • Fix Delete dialog text format
  • Fix Profile picture not showing after coming back from folders
  • Fix Play button color i Simple and Plain themes
  • Fix Sleep timer dialog crashing
  • Fix Share song dialog title and text

If you see entire app white or dark or black select same theme in settings to fix

FAQ's

*If you face any UI related issues you clear app data and cache, if its not working try to uninstall and install again.

\ No newline at end of file +

v3.3.000

  • SD card support for deleting and editing

v3.2.240

  • Improved options dialog with selected color and title icon
  • Fix dialog color on dark theme

v3.2.220

  • Fix Notification SeekBar position (Need Android Q test)
  • Rolled back settings tint icons
  • Changed preference category title style according Android Q
  • Fix tabs options not working

v3.2.203

  • Carousel effect improved
  • Settings icons are now tint accent color
  • Settings title, back arrow and icons are now tint accent color
  • Equalizer is removed
  • Fix keyboard not popping on Search
  • Curated the main options
  • Folder toolbar outlined, folder icon background fix and separate line removed
  • Edit text input handles are colored

v3.2.135

  • Fix crashing on sharing song

v3.2.125

  • Fix crashing on selecting profile picture
  • Semi transparent color fix(s) on now playing themes

v3.2.120

  • Fix crashing on choosing a theme
  • Fix color theme selection without pro enabled
  • Fix icon tint some places

v3.2.105

  • Fix color notifications

v3.2.100

  • Fix crashing on Sleep timer
  • Toolbar elevation removed added stroke instead
  • Show empty indication for home

v3.2.000

  • Implemented Artist image loading all thanks to VinylMusicPlayer
  • Fixed buttons in Genre details
  • Fixed color buttons in Album and Artist details screen

v3.1.900

  • Added Play and Shuffle buttons on Songs list, Album Details, Artist Details, Playlist Details etc.
  • Home toolbar is semi transparent in Banner mode
  • Added new Buy Retro Music pro in settings
  • Improved dark color in Dark theme

v3.1.850

  • Toolbar will be clickable for Search

v3.1.800

  • Search bar CardView background
  • Improve volume zero
  • Now playing and Album theme picker rollback to dialog
  • Fix sharing app link
  • Fix scanning dialog
  • Added settings icons for options

v3.1.700

  • Cleaned internal code
  • Removed full screen option
  • Added Toolbar elevation
  • To access menu either tap on Toolbar or Hamburger icon
  • Fix back button not working on playing queue
  • Fix crashing on What's New screen
  • Fix lyrics dialog
  • Changed toggles to line icons
  • Custom UserImageView for loading user profile image
  • Fix crashing on artist list for number format error
  • Fix blacklist dialog crashing
  • Rearranged icons and main menu access
  • Fix some crashes when device is locked or background
  • Folder screen have main options access
  • Dialogs are now using Material Dialogs v3(BottomSheet)
  • Fix Shuffle icon for Artist, Album, Genre and Playlist details

v3.1.400

  • Removed sync lyrics for Android 5
  • Fix Seek-bar color in settings
  • Added keyboard to popup on search
  • Added keyboard to popup on search
  • Improved lock-screen behavior and UI
  • Improved text appearance
  • Fix bio text not showing in settings
  • Fix not showing slider(blur, filter song) amount in settings
  • Fix setting ringtone
  • Fix file sharing crash
  • Fix some crashes
  • Fix playlist icon on small devices
  • Fix empty lyrics text color
  • Fix album cover background purple color in color theme

v3.1.300

  • Fix rename playlist text color
  • Fix same album showing in details page
  • Fix lyrics text alignment on sync and lyrics reading improved
  • Improved home sections loading
  • Removed library options which are duplicated (it's available from profile menu)
  • Replaced collapsing Fab with Android Floating Extended Fab
  • Replaced home with for you
  • Fixed profile image not loading in about
  • Improved selecting user profile image
  • Added bio to enter custom message
  • Improved some UI screens

v3.1.240

  • Fix Search not showing from home screen
  • Fix Volume controls color issue
  • Fix Seek bar alignment
  • Added tiny theme
  • Improved full theme appearances
  • Now playing theme preview updated
  • Fix composer error
  • Bottom Options improved(internal)

v3.1.200

  • Added composer sort and editing
  • Fix Crash in Album tag editor while selecting options
  • Added Filter song length
  • Added Favourites playlist icon will be accent color
  • Added Colorful settings icons
  • Added Corners for dialog

v3.0.570

  • Fix Album/Artist square image
  • Fix Delete dialog text format
  • Fix Profile picture not showing after coming back from folders
  • Fix Play button color i Simple and Plain themes
  • Fix Sleep timer dialog crashing
  • Fix Share song dialog title and text

If you see entire app white or dark or black select same theme in settings to fix

FAQ's

*If you face any UI related issues you clear app data and cache, if its not working try to uninstall and install again.

\ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/App.kt b/app/src/main/java/code/name/monkey/retromusic/App.kt index 2f578b2d..d164a848 100644 --- a/app/src/main/java/code/name/monkey/retromusic/App.kt +++ b/app/src/main/java/code/name/monkey/retromusic/App.kt @@ -44,12 +44,6 @@ class App : MultiDexApplication() { if (VersionUtils.hasNougatMR()) DynamicShortcutManager(this).initDynamicShortcuts() - - CalligraphyConfig.initDefault(CalligraphyConfig.Builder() - .setDefaultFont(R.font.circular_std_book) - .build() - ) - // automatically restores purchases billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, object : BillingProcessor.IBillingHandler { diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt index fde4d776..a8e1b0a9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt @@ -1,5 +1,6 @@ package code.name.monkey.retromusic.activities.base +import android.content.Context import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle @@ -16,6 +17,8 @@ import code.name.monkey.appthemehelper.util.* import code.name.monkey.retromusic.R import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil +import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper +import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan abstract class AbsThemeActivity : ATHActivity(), Runnable { diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/OptionsSheetDialogFragment.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/OptionsSheetDialogFragment.kt index 2f288b57..ad5acafa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/OptionsSheetDialogFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/OptionsSheetDialogFragment.kt @@ -15,12 +15,10 @@ package code.name.monkey.retromusic.dialogs import android.app.Dialog -import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import androidx.fragment.app.DialogFragment -import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.util.NavigationUtil @@ -38,6 +36,7 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener { R.id.actionFolders -> mainActivity.selectedFragment(R.id.action_folder) R.id.actionLibrary -> mainActivity.selectedFragment(PreferenceUtil.getInstance().lastPage) R.id.actionSettings -> NavigationUtil.goToSettings(mainActivity) + R.id.actionRate -> NavigationUtil.goToPlayStore(mainActivity) } materialDialog.dismiss() } @@ -45,11 +44,13 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener { private lateinit var actionSettings: OptionMenuItemView private lateinit var actionLibrary: OptionMenuItemView private lateinit var actionFolders: OptionMenuItemView + private lateinit var actionRate: OptionMenuItemView private lateinit var materialDialog: MaterialDialog override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val layout = LayoutInflater.from(context).inflate(R.layout.fragment_main_options, null) actionSettings = layout.findViewById(R.id.actionSettings) + actionRate = layout.findViewById(R.id.actionRate) actionLibrary = layout.findViewById(R.id.actionLibrary) actionFolders = layout.findViewById(R.id.actionFolders) @@ -60,9 +61,11 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener { } actionSettings.setOnClickListener(this) + actionRate.setOnClickListener(this) actionLibrary.setOnClickListener(this) actionFolders.setOnClickListener(this) + materialDialog = MaterialDialog(activity!!, BottomSheet()) .show { icon(R.mipmap.ic_launcher_round) @@ -72,11 +75,8 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener { return materialDialog } - companion object { - private const val TAG: String = "MainOptionsBottomSheetD" - private const val WHICH_ONE = "which_one" @JvmField var LIBRARY: Int = 0 diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java index 1923f230..bdb03c39 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java @@ -1,7 +1,6 @@ package code.name.monkey.retromusic.fragments.mainactivity.folders; import android.app.Dialog; -import android.app.ProgressDialog; import android.content.Context; import android.media.MediaScannerConnection; import android.os.Bundle; @@ -27,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.afollestad.materialcab.MaterialCab; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView; @@ -64,7 +64,6 @@ import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.util.FileUtil; import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroColorUtil; -import code.name.monkey.retromusic.util.RetroUtil; import code.name.monkey.retromusic.util.ViewUtil; import code.name.monkey.retromusic.views.BreadCrumbLayout; @@ -729,14 +728,13 @@ public class FoldersFragment extends AbsMainActivityFragment implements @Override protected Dialog createDialog(@NonNull Context context) { - ProgressDialog dialog = new ProgressDialog(context); - dialog.setIndeterminate(true); - dialog.setTitle(R.string.listing_files); - dialog.setCancelable(false); - dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - dialog.setOnCancelListener(dialog1 -> cancel(false)); - dialog.setOnDismissListener(dialog1 -> cancel(false)); - return dialog; + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.listing_files) + .setCancelable(false) + .setView(R.layout.loading) + .setOnCancelListener(dialog -> cancel(false)) + .setOnDismissListener(dialog -> cancel(false)) + .create(); } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/OptionMenuItemView.java b/app/src/main/java/code/name/monkey/retromusic/views/OptionMenuItemView.java index 349d86bd..ca0a60f4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/OptionMenuItemView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/OptionMenuItemView.java @@ -56,6 +56,9 @@ public class OptionMenuItemView extends FrameLayout { int accentColor = ThemeStore.Companion.accentColor(context); setBackground(ContextCompat.getDrawable(context, R.drawable.menu_item_background)); + setClickable(true); + setFocusable(true); + inflate(context, R.layout.item_option_menu, this); setBackgroundTintList(ColorStateList.valueOf(ColorUtil.INSTANCE.adjustAlpha(accentColor, 0.22f))); diff --git a/app/src/main/res/drawable/menu_item_background.xml b/app/src/main/res/drawable/menu_item_background.xml index e0eb4c3f..eba09688 100644 --- a/app/src/main/res/drawable/menu_item_background.xml +++ b/app/src/main/res/drawable/menu_item_background.xml @@ -12,7 +12,7 @@ ~ See the GNU General Public License for more details. --> - + diff --git a/app/src/main/res/layout/fragment_main_options.xml b/app/src/main/res/layout/fragment_main_options.xml index 8eb58e6b..18bbd6e9 100644 --- a/app/src/main/res/layout/fragment_main_options.xml +++ b/app/src/main/res/layout/fragment_main_options.xml @@ -13,9 +13,6 @@ android:id="@+id/actionLibrary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?rectSelector" - android:clickable="true" - android:focusable="true" app:optionIcon="@drawable/ic_library_music_white_24dp" app:optionTitle="@string/library" /> @@ -23,9 +20,6 @@ android:id="@+id/actionFolders" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?rectSelector" - android:clickable="true" - android:focusable="true" app:optionIcon="@drawable/ic_folder_white_24dp" app:optionTitle="@string/folders" /> @@ -33,10 +27,13 @@ android:id="@+id/actionSettings" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?rectSelector" - android:clickable="true" - android:focusable="true" app:optionIcon="@drawable/ic_settings_white_24dp" app:optionTitle="@string/action_settings" /> + \ No newline at end of file