diff --git a/app/build.gradle b/app/build.gradle index 7322c109..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 @@ -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,22 +131,23 @@ 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' - 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' 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" /> +

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

v3.2.220

v3.2.203

v3.2.135

v3.2.125

v3.2.120

v3.2.105

v3.2.100

v3.2.000

v3.1.900

v3.1.850

v3.1.800

v3.1.700

v3.1.400

v3.1.300

v3.1.240

v3.1.200

v3.0.570

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

v3.2.240

v3.2.220

v3.2.203

v3.2.135

v3.2.125

v3.2.120

v3.2.105

v3.2.100

v3.2.000

v3.1.900

v3.1.850

v3.1.800

v3.1.700

v3.1.400

v3.1.300

v3.1.240

v3.1.200

v3.0.570

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/activities/saf/SAFGuideActivity.java b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java new file mode 100644 index 00000000..4ba6c990 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java @@ -0,0 +1,70 @@ +/* + * 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.activities.saf; + +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.heinrichreimersoftware.materialintro.app.IntroActivity; +import com.heinrichreimersoftware.materialintro.slide.SimpleSlide; + +import code.name.monkey.retromusic.R; + +/** + * Created by hemanths on 2019-07-31. + */ +public class SAFGuideActivity extends IntroActivity { + public static final int REQUEST_CODE_SAF_GUIDE = 98; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setButtonCtaVisible(false); + setButtonNextVisible(false); + setButtonBackVisible(false); + + setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT); + + String title = String.format(getString(R.string.saf_guide_slide1_title), getString(R.string.app_name)); + + addSlide(new SimpleSlide.Builder() + .title(title) + .description(Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 ? R.string.saf_guide_slide1_description_before_o : R.string.saf_guide_slide1_description) + .image(R.drawable.saf_guide_1) + .background(R.color.md_deep_purple_300) + .backgroundDark(R.color.md_deep_purple_400) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + addSlide(new SimpleSlide.Builder() + .title(R.string.saf_guide_slide2_title) + .description(R.string.saf_guide_slide2_description) + .image(R.drawable.saf_guide_2) + .background(R.color.md_deep_purple_500) + .backgroundDark(R.color.md_deep_purple_600) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + addSlide(new SimpleSlide.Builder() + .title(R.string.saf_guide_slide3_title) + .description(R.string.saf_guide_slide3_description) + .image(R.drawable.saf_guide_3) + .background(R.color.md_deep_purple_700) + .backgroundDark(R.color.md_deep_purple_800) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt index 5d3134cb..f122b7d0 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt @@ -7,6 +7,7 @@ import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.Log import android.view.MenuItem @@ -16,9 +17,10 @@ import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.MaterialValueHelper import code.name.monkey.appthemehelper.util.TintHelper -import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsBaseActivity +import code.name.monkey.retromusic.activities.saf.SAFGuideActivity import code.name.monkey.retromusic.util.RetroUtil +import code.name.monkey.retromusic.util.SAFUtil import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItems import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton @@ -27,6 +29,8 @@ import org.jaudiotagger.audio.AudioFile import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.tag.FieldKey import java.io.File +import java.util.* + abstract class AbsTagEditorActivity : AbsBaseActivity() { @@ -38,9 +42,14 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { private var songPaths: List? = 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/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/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/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/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/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-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/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 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