Merge pull request #454 from h4h13/SAF

Saf
This commit is contained in:
Hemanth S 2019-08-01 00:56:22 +05:30 committed by GitHub
commit 28478b8755
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 938 additions and 179 deletions

View file

@ -13,8 +13,8 @@ android {
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
applicationId "code.name.monkey.retromusic" applicationId "code.name.monkey.retromusic"
versionCode 346 versionCode 347
versionName '3.2.240' versionName '3.3.00'
multiDexEnabled true multiDexEnabled true
@ -107,6 +107,7 @@ dependencies {
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'androidx.preference:preference:1.1.0-rc01'
implementation 'androidx.palette:palette-ktx:1.0.0' 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' implementation 'com.google.android.material:material:1.1.0-alpha09'
@ -131,21 +132,22 @@ dependencies {
transitive = true transitive = true
} }
implementation 'com.anjlab.android.iab.v3:library:1.0.44'
/*UI Library*/ /*UI Library*/
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' 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.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.eclipse.mylyn.github:org.eclipse.egit.github.core:3.4.0.201406110918-r' 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.ksoichiro:android-observablescrollview:1.6.0'
implementation 'com.github.kabouzeid:RecyclerView-FastScroll:1.0.16-kmod' implementation 'com.github.kabouzeid:RecyclerView-FastScroll:1.0.16-kmod'
implementation 'com.github.kabouzeid:AndroidSlidingUpPanel:3.3.0-kmod3' implementation 'com.github.kabouzeid:AndroidSlidingUpPanel:3.3.0-kmod3'
implementation 'com.github.AdrienPoupa:jaudiotagger:2.2.3' 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') implementation project(':appthemehelper')
} }

View file

@ -121,6 +121,7 @@
android:name=".appshortcuts.AppShortcutLauncherActivity" android:name=".appshortcuts.AppShortcutLauncherActivity"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name=".activities.saf.SAFGuideActivity" />
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"

View file

@ -28,9 +28,9 @@
<body style="padding: 1.0rem 0.5rem 1.0rem 0.5rem;"> <body style="padding: 1.0rem 0.5rem 1.0rem 0.5rem;">
<p><b><a href="https://github.com/kabouzeid/Phonograph" title="Phonograph"> Phonograph</a></b> by Karim Abou Zeid</p> <p><b><a href="https://github.com/kabouzeid/Phonograph" title="Phonograph"> Phonograph</a></b> by Karim Abou Zeid</p>
<p><b><a href="https://github.com/ReactiveX/RxAndroid" title="RxAndroid"> RxAndroid</a></b> by RxAndroid authors</p> <p><b><a href="https://github.com/AdrienPoupa/VinylMusicPlayer" title="VinylMusicPlayer"> VinylMusicPlayer</a></b> by Adrien Poupa</p>
<p><b><a href="https://github.com/ReactiveX/RxAndroid" title="RxJava"> RxJava</a></b> by RxJava authors</p> <p><b><a href="https://github.com/ReactiveX/RxAndroid" title="RxJava"> RxJava</a></b> by RxJava authors</p>
<p> <b><a href="https://github.com/afollestad/material-cab" title="Material Dialogs"> Material Dialogs</a></b> by Aidan Michael Follestad</p> <p> <b><a href="https://github.com/afollestad" title="Material Dialogs"> Material Dialogs and Cab</a></b> by Aidan Michael Follestad</p>
<p><b><a href="https://github.com/chrisjenx/Calligraphy" title="Calligraphy"> Calligraphy</a></b> by RxJava authors</p> <p><b><a href="https://github.com/chrisjenx/Calligraphy" title="Calligraphy"> Calligraphy</a></b> by RxJava authors</p>
<p><b><a href="https://github.com/JetradarMobile/android-snowfall" title="Android-Snowfall"> Android-Snowfall</a></b> by JetRadar</p> <p><b><a href="https://github.com/JetradarMobile/android-snowfall" title="Android-Snowfall"> Android-Snowfall</a></b> by JetRadar</p>
<p><b><a href="https://github.com/umano/AndroidSlidingUpPanel" title="Android Sliding Up Panel"> Android Sliding Up Panel</a></b>by The Umano Team</p> <p><b><a href="https://github.com/umano/AndroidSlidingUpPanel" title="Android Sliding Up Panel"> Android Sliding Up Panel</a></b>by The Umano Team</p>

File diff suppressed because one or more lines are too long

View file

@ -44,12 +44,6 @@ class App : MultiDexApplication() {
if (VersionUtils.hasNougatMR()) if (VersionUtils.hasNougatMR())
DynamicShortcutManager(this).initDynamicShortcuts() DynamicShortcutManager(this).initDynamicShortcuts()
CalligraphyConfig.initDefault(CalligraphyConfig.Builder()
.setDefaultFont(R.font.circular_std_book)
.build()
)
// automatically restores purchases // automatically restores purchases
billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY,
object : BillingProcessor.IBillingHandler { object : BillingProcessor.IBillingHandler {

View file

@ -1,5 +1,6 @@
package code.name.monkey.retromusic.activities.base package code.name.monkey.retromusic.activities.base
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle 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.R
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.RetroUtil
import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan
abstract class AbsThemeActivity : ATHActivity(), Runnable { abstract class AbsThemeActivity : ATHActivity(), Runnable {

View file

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

View file

@ -7,6 +7,7 @@ import android.content.res.ColorStateList
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.MenuItem 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.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.TintHelper 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.base.AbsBaseActivity
import code.name.monkey.retromusic.activities.saf.SAFGuideActivity
import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.RetroUtil
import code.name.monkey.retromusic.util.SAFUtil
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -27,6 +29,8 @@ import org.jaudiotagger.audio.AudioFile
import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey import org.jaudiotagger.tag.FieldKey
import java.io.File import java.io.File
import java.util.*
abstract class AbsTagEditorActivity : AbsBaseActivity() { abstract class AbsTagEditorActivity : AbsBaseActivity() {
@ -38,9 +42,14 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() {
private var songPaths: List<String>? = null private var songPaths: List<String>? = null
lateinit var saveFab: ExtendedFloatingActionButton lateinit var saveFab: ExtendedFloatingActionButton
private var savedSongPaths: List<String>? = null
private val currentSongPath: String? = null
private var savedTags: Map<FieldKey, String>? = null
private var savedArtworkInfo: ArtworkInfo? = null
protected val show: MaterialDialog protected val show: MaterialDialog
get() = MaterialDialog(this@AbsTagEditorActivity).show { get() = MaterialDialog(this@AbsTagEditorActivity).show {
title(R.string.update_image) title(code.name.monkey.retromusic.R.string.update_image)
listItems(items = items) { _, position, _ -> listItems(items = items) { _, position, _ ->
when (position) { when (position) {
0 -> getImageFromLastFM() 0 -> getImageFromLastFM()
@ -174,7 +183,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(contentViewLayout) setContentView(contentViewLayout)
saveFab = findViewById(R.id.saveTags) saveFab = findViewById(code.name.monkey.retromusic.R.id.saveTags)
getIntentExtras() getIntentExtras()
songPaths = getSongPaths() songPaths = getSongPaths()
@ -204,14 +213,14 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() {
private fun setUpImageView() { private fun setUpImageView() {
loadCurrentImage() 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 } editorImage.setOnClickListener { show }
} }
private fun startImagePicker() { private fun startImagePicker() {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*" 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() protected abstract fun loadCurrentImage()
@ -295,9 +304,19 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() {
saveFab.isEnabled = true 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) { protected fun setImageBitmap(bitmap: Bitmap?, bgColor: Int) {
if (bitmap == null) { if (bitmap == null) {
editorImage.setImageResource(R.drawable.default_album_art) editorImage.setImageResource(code.name.monkey.retromusic.R.drawable.default_album_art)
} else { } else {
editorImage.setImageBitmap(bitmap) editorImage.setImageBitmap(bitmap)
} }
@ -312,16 +331,50 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() {
artworkInfo: ArtworkInfo?) { artworkInfo: ArtworkInfo?) {
RetroUtil.hideSoftKeyboard(this) RetroUtil.hideSoftKeyboard(this)
WriteTagsAsyncTask(this) hideFab()
.execute(WriteTagsAsyncTask.LoadingInfo(getSongPaths(), fieldKeyValueMap, artworkInfo))
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?) { private fun writeTags(paths: List<String>?) {
super.onActivityResult(requestCode, resultCode, data) WriteTagsAsyncTask(this).execute(WriteTagsAsyncTask.LoadingInfo(paths, savedTags, savedArtworkInfo))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
when (requestCode) { when (requestCode) {
REQUEST_CODE_SELECT_IMAGE -> if (resultCode == Activity.RESULT_OK) { REQUEST_CODE_SELECT_IMAGE -> if (resultCode == Activity.RESULT_OK) {
val selectedImage = data!!.data intent?.data?.let {
loadImageFromFile(selectedImage) 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) Log.e(TAG, "Could not read audio file $path", e)
AudioFile() AudioFile()
} }
} }
class ArtworkInfo constructor(val albumId: Int, val artwork: Bitmap?) class ArtworkInfo constructor(val albumId: Int, val artwork: Bitmap?)

View file

@ -56,6 +56,7 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher {
songText.appHandleColor().addTextChangedListener(this) songText.appHandleColor().addTextChangedListener(this)
albumText.appHandleColor().addTextChangedListener(this) albumText.appHandleColor().addTextChangedListener(this)
albumArtistText.appHandleColor().addTextChangedListener(this)
artistText.appHandleColor().addTextChangedListener(this) artistText.appHandleColor().addTextChangedListener(this)
genreText.appHandleColor().addTextChangedListener(this) genreText.appHandleColor().addTextChangedListener(this)
yearText.appHandleColor().addTextChangedListener(this) yearText.appHandleColor().addTextChangedListener(this)

View file

@ -5,43 +5,43 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import com.afollestad.materialdialogs.MaterialDialog; import androidx.annotation.NonNull;
import com.afollestad.materialdialogs.bottomsheets.BottomSheet; import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO; 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.FieldKey;
import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagException;
import org.jaudiotagger.tag.images.Artwork; import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.images.ArtworkFactory; import org.jaudiotagger.tag.images.ArtworkFactory;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.R;
import code.name.monkey.retromusic.misc.DialogAsyncTask; import code.name.monkey.retromusic.misc.DialogAsyncTask;
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener;
import code.name.monkey.retromusic.util.MusicUtil; import code.name.monkey.retromusic.util.MusicUtil;
import code.name.monkey.retromusic.util.SAFUtil;
public class WriteTagsAsyncTask extends public class WriteTagsAsyncTask extends
DialogAsyncTask<WriteTagsAsyncTask.LoadingInfo, Integer, String[]> { DialogAsyncTask<WriteTagsAsyncTask.LoadingInfo, Integer, String[]> {
private Context applicationContext; private WeakReference<Activity> activity;
public WriteTagsAsyncTask(Context context) { public WriteTagsAsyncTask(@NonNull Activity activity) {
super(context); super(activity);
applicationContext = context; this.activity = new WeakReference<>(activity);
} }
@Override @Override
@ -68,6 +68,13 @@ public class WriteTagsAsyncTask extends
for (String filePath : info.filePaths) { for (String filePath : info.filePaths) {
publishProgress(++counter, info.filePaths.size()); publishProgress(++counter, info.filePaths.size());
try { 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)); AudioFile audioFile = AudioFileIO.read(new File(filePath));
Tag tag = audioFile.getTagOrCreateAndSetDefault(); Tag tag = audioFile.getTagOrCreateAndSetDefault();
@ -92,8 +99,10 @@ public class WriteTagsAsyncTask extends
} }
} }
audioFile.commit(); Activity activity = this.activity.get();
} catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { SAFUtil.write(activity, audioFile, safUri);
} catch (@NonNull Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
@ -107,7 +116,17 @@ public class WriteTagsAsyncTask extends
} }
} }
return info.filePaths.toArray(new String[info.filePaths.size()]); Collection<String> 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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
@ -127,18 +146,20 @@ public class WriteTagsAsyncTask extends
} }
private void scan(String[] toBeScanned) { private void scan(String[] toBeScanned) {
Context context = getContext(); Activity activity = this.activity.get();
MediaScannerConnection.scanFile(applicationContext, toBeScanned, null, if (activity != null) {
context instanceof Activity ? new UpdateToastMediaScannerCompletionListener( MediaScannerConnection.scanFile(activity, toBeScanned, null, new UpdateToastMediaScannerCompletionListener(activity, toBeScanned));
(Activity) context, toBeScanned) : null); }
} }
@NonNull @NonNull
@Override @Override
protected Dialog createDialog(@NonNull Context context) { protected Dialog createDialog(@NonNull Context context) {
return new MaterialDialog(context, new BottomSheet()) return new MaterialAlertDialogBuilder(context)
.title(R.string.saving_changes, "") .setTitle(R.string.saving_changes)
.cancelable(false); .setCancelable(false)
.setView(R.layout.loading)
.create();
} }
@Override @Override

View file

@ -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<DeleteSongsAsyncTask.LoadingInfo, Integer, Void> {
private WeakReference<DeleteSongsDialog> dialogReference;
private WeakReference<FragmentActivity> 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<Song> songs;
public List<Uri> safUris;
public int requestCode;
public int resultCode;
public Intent intent;
public LoadingInfo(List<Song> songs, List<Uri> 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;
}
}
}

View file

@ -15,41 +15,85 @@
package code.name.monkey.retromusic.dialogs package code.name.monkey.retromusic.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import code.name.monkey.retromusic.R 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.model.Song
import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.SAFUtil
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.bottomsheets.BottomSheet
class DeleteSongsDialog : DialogFragment() { class DeleteSongsDialog : DialogFragment() {
@JvmField
var currentSong: Song? = null
@JvmField
var songsToRemove: List<Song>? = null
private var deleteSongsAsyncTask: DeleteSongsAsyncTask? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val songs = arguments!!.getParcelableArrayList<Song>("songs") val songs: ArrayList<Song>? = arguments?.getParcelableArrayList("songs")
val title: Int var title = 0
val content: CharSequence var content: CharSequence = ""
if (songs != null) {
if (songs.size > 1) { if (songs.size > 1) {
title = R.string.delete_songs_title title = R.string.delete_songs_title
content = Html.fromHtml(getString(R.string.delete_x_songs, songs.size)) content = Html.fromHtml(getString(R.string.delete_x_songs, songs.size))
} else { } else {
title = R.string.delete_song_title title = R.string.delete_song_title
content = Html.fromHtml(getString(R.string.delete_song_x, songs.get(0).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) title(title)
message(text = content) message(text = content)
negativeButton(android.R.string.cancel) negativeButton(android.R.string.cancel) {
dismiss()
}
noAutoDismiss()
positiveButton(R.string.action_delete) { positiveButton(R.string.action_delete) {
if (activity == null) if (songs != null) {
return@positiveButton if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
MusicUtil.deleteTracks(activity!!, songs); 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<Song>, safUris: List<Uri>?) {
MusicUtil.deleteTracks(activity!!, songs, safUris) { this.dismiss() }
}
companion object { companion object {

View file

@ -15,12 +15,10 @@
package code.name.monkey.retromusic.dialogs package code.name.monkey.retromusic.dialogs
import android.app.Dialog import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.util.NavigationUtil 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.actionFolders -> mainActivity.selectedFragment(R.id.action_folder)
R.id.actionLibrary -> mainActivity.selectedFragment(PreferenceUtil.getInstance().lastPage) R.id.actionLibrary -> mainActivity.selectedFragment(PreferenceUtil.getInstance().lastPage)
R.id.actionSettings -> NavigationUtil.goToSettings(mainActivity) R.id.actionSettings -> NavigationUtil.goToSettings(mainActivity)
R.id.actionRate -> NavigationUtil.goToPlayStore(mainActivity)
} }
materialDialog.dismiss() materialDialog.dismiss()
} }
@ -45,11 +44,13 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener {
private lateinit var actionSettings: OptionMenuItemView private lateinit var actionSettings: OptionMenuItemView
private lateinit var actionLibrary: OptionMenuItemView private lateinit var actionLibrary: OptionMenuItemView
private lateinit var actionFolders: OptionMenuItemView private lateinit var actionFolders: OptionMenuItemView
private lateinit var actionRate: OptionMenuItemView
private lateinit var materialDialog: MaterialDialog private lateinit var materialDialog: MaterialDialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val layout = LayoutInflater.from(context).inflate(R.layout.fragment_main_options, null) val layout = LayoutInflater.from(context).inflate(R.layout.fragment_main_options, null)
actionSettings = layout.findViewById(R.id.actionSettings) actionSettings = layout.findViewById(R.id.actionSettings)
actionRate = layout.findViewById(R.id.actionRate)
actionLibrary = layout.findViewById(R.id.actionLibrary) actionLibrary = layout.findViewById(R.id.actionLibrary)
actionFolders = layout.findViewById(R.id.actionFolders) actionFolders = layout.findViewById(R.id.actionFolders)
@ -60,9 +61,11 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener {
} }
actionSettings.setOnClickListener(this) actionSettings.setOnClickListener(this)
actionRate.setOnClickListener(this)
actionLibrary.setOnClickListener(this) actionLibrary.setOnClickListener(this)
actionFolders.setOnClickListener(this) actionFolders.setOnClickListener(this)
materialDialog = MaterialDialog(activity!!, BottomSheet()) materialDialog = MaterialDialog(activity!!, BottomSheet())
.show { .show {
icon(R.mipmap.ic_launcher_round) icon(R.mipmap.ic_launcher_round)
@ -72,11 +75,8 @@ class OptionsSheetDialogFragment : DialogFragment(), View.OnClickListener {
return materialDialog return materialDialog
} }
companion object { companion object {
private const val TAG: String = "MainOptionsBottomSheetD"
private const val WHICH_ONE = "which_one" private const val WHICH_ONE = "which_one"
@JvmField @JvmField
var LIBRARY: Int = 0 var LIBRARY: Int = 0

View file

@ -15,14 +15,14 @@ import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.ThemeStore
import code.name.monkey.retromusic.R 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.dialogs.*
import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment
import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.PaletteColorHolder import code.name.monkey.retromusic.interfaces.PaletteColorHolder
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.model.lyrics.Lyrics 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.util.*
import code.name.monkey.retromusic.views.FitSystemWindowsLayout import code.name.monkey.retromusic.views.FitSystemWindowsLayout
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -57,20 +57,15 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem
return true return true
} }
R.id.action_share -> { R.id.action_share -> {
if (fragmentManager != null) { SongShareDialog.create(song).show(requireFragmentManager(), "SHARE_SONG")
SongShareDialog.create(song).show(fragmentManager!!, "SHARE_SONG")
}
return true return true
} }
R.id.action_delete_from_device -> { R.id.action_delete_from_device -> {
DeleteSongsDialog.create(song) DeleteSongsDialog.create(song).show(requireFragmentManager(), "DELETE_SONGS")
.show(activity!!.supportFragmentManager, "DELETE_SONGS")
return true return true
} }
R.id.action_add_to_playlist -> { R.id.action_add_to_playlist -> {
if (fragmentManager != null) { AddToPlaylistDialog.create(song).show(requireFragmentManager(), "ADD_PLAYLIST")
AddToPlaylistDialog.create(song).show(fragmentManager!!, "ADD_PLAYLIST")
}
return true return true
} }
R.id.action_clear_playing_queue -> { R.id.action_clear_playing_queue -> {
@ -79,7 +74,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem
} }
R.id.action_save_playing_queue -> { R.id.action_save_playing_queue -> {
CreatePlaylistDialog.create(MusicPlayerRemote.playingQueue) CreatePlaylistDialog.create(MusicPlayerRemote.playingQueue)
.show(activity!!.supportFragmentManager, "ADD_TO_PLAYLIST") .show(requireFragmentManager(), "ADD_TO_PLAYLIST")
return true return true
} }
R.id.action_tag_editor -> { R.id.action_tag_editor -> {
@ -89,45 +84,43 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem
return true return true
} }
R.id.action_details -> { R.id.action_details -> {
if (fragmentManager != null) { SongDetailDialog.create(song).show(requireFragmentManager(), "SONG_DETAIL")
SongDetailDialog.create(song).show(fragmentManager!!, "SONG_DETAIL")
}
return true return true
} }
R.id.action_go_to_album -> { R.id.action_go_to_album -> {
NavigationUtil.goToAlbum(activity!!, song.albumId) NavigationUtil.goToAlbum(requireActivity(), song.albumId)
return true return true
} }
R.id.action_go_to_artist -> { R.id.action_go_to_artist -> {
NavigationUtil.goToArtist(activity!!, song.artistId) NavigationUtil.goToArtist(requireActivity(), song.artistId)
return true return true
} }
R.id.now_playing -> { R.id.now_playing -> {
NavigationUtil.goToPlayingQueue(activity!!) NavigationUtil.goToPlayingQueue(requireActivity())
return true return true
} }
R.id.action_show_lyrics -> { R.id.action_show_lyrics -> {
NavigationUtil.goToLyrics(activity!!) NavigationUtil.goToLyrics(requireActivity())
return true return true
} }
R.id.action_equalizer -> { R.id.action_equalizer -> {
NavigationUtil.openEqualizer(activity!!) NavigationUtil.openEqualizer(requireActivity())
return true return true
} }
R.id.action_sleep_timer -> { R.id.action_sleep_timer -> {
SleepTimerDialog().show(fragmentManager!!, TAG) SleepTimerDialog().show(requireFragmentManager(), TAG)
return true return true
} }
R.id.action_set_as_ringtone -> { R.id.action_set_as_ringtone -> {
if (RingtoneManager.requiresDialog(activity!!)) { if (RingtoneManager.requiresDialog(requireActivity())) {
RingtoneManager.getDialog(activity!!) RingtoneManager.getDialog(requireActivity())
} }
val ringtoneManager = RingtoneManager(activity!!) val ringtoneManager = RingtoneManager(requireActivity())
ringtoneManager.setRingtone(song) ringtoneManager.setRingtone(song)
return true return true
} }
R.id.action_settings -> { R.id.action_settings -> {
NavigationUtil.goToSettings(activity!!) NavigationUtil.goToSettings(requireActivity())
return true return true
} }
R.id.action_go_to_genre -> { R.id.action_go_to_genre -> {
@ -146,7 +139,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem
} }
protected open fun toggleFavorite(song: Song) { protected open fun toggleFavorite(song: Song) {
MusicUtil.toggleFavorite(activity!!, song) MusicUtil.toggleFavorite(requireActivity(), song)
} }
abstract fun playerToolbar(): Toolbar abstract fun playerToolbar(): Toolbar
@ -252,7 +245,7 @@ abstract class AbsPlayerFragment : AbsMusicServiceFragment(), Toolbar.OnMenuItem
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(ThemeStore.primaryColor(activity!!)) view.setBackgroundColor(ThemeStore.primaryColor(requireActivity()))
if (PreferenceUtil.getInstance().fullScreenMode && view.findViewById<View>(R.id.status_bar) != null) { if (PreferenceUtil.getInstance().fullScreenMode && view.findViewById<View>(R.id.status_bar) != null) {
view.findViewById<View>(R.id.status_bar).visibility = View.GONE view.findViewById<View>(R.id.status_bar).visibility = View.GONE
} }

View file

@ -1,7 +1,6 @@
package code.name.monkey.retromusic.fragments.mainactivity.folders; package code.name.monkey.retromusic.fragments.mainactivity.folders;
import android.app.Dialog; import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.os.Bundle; import android.os.Bundle;
@ -27,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.afollestad.materialcab.MaterialCab; import com.afollestad.materialcab.MaterialCab;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView; 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.FileUtil;
import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.PreferenceUtil;
import code.name.monkey.retromusic.util.RetroColorUtil; 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.util.ViewUtil;
import code.name.monkey.retromusic.views.BreadCrumbLayout; import code.name.monkey.retromusic.views.BreadCrumbLayout;
@ -729,14 +728,13 @@ public class FoldersFragment extends AbsMainActivityFragment implements
@Override @Override
protected Dialog createDialog(@NonNull Context context) { protected Dialog createDialog(@NonNull Context context) {
ProgressDialog dialog = new ProgressDialog(context); return new MaterialAlertDialogBuilder(context)
dialog.setIndeterminate(true); .setTitle(R.string.listing_files)
dialog.setTitle(R.string.listing_files); .setCancelable(false)
dialog.setCancelable(false); .setView(R.layout.loading)
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); .setOnCancelListener(dialog -> cancel(false))
dialog.setOnCancelListener(dialog1 -> cancel(false)); .setOnDismissListener(dialog -> cancel(false))
dialog.setOnDismissListener(dialog1 -> cancel(false)); .create();
return dialog;
} }
} }
} }

View file

@ -26,8 +26,6 @@ import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.loaders.SongLoader import code.name.monkey.retromusic.loaders.SongLoader
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService
@ -45,6 +43,13 @@ object MusicPlayerRemote {
val isPlaying: Boolean val isPlaying: Boolean
get() = musicService != null && musicService!!.isPlaying get() = musicService != null && musicService!!.isPlaying
fun isPlaying(song: Song): Boolean {
return if (!isPlaying) {
false
} else song.id == currentSong.id
}
val currentSong: Song val currentSong: Song
get() = if (musicService != null) { get() = if (musicService != null) {
musicService!!.currentSong musicService!!.currentSong
@ -278,7 +283,7 @@ object MusicPlayerRemote {
queue.add(song) queue.add(song)
openQueue(queue, 0, false) 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 true
} }
return false return false
@ -291,7 +296,7 @@ object MusicPlayerRemote {
} else { } else {
openQueue(songs, 0, false) 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() Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show()
return true return true
} }
@ -307,7 +312,7 @@ object MusicPlayerRemote {
queue.add(song) queue.add(song)
openQueue(queue, 0, false) 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 true
} }
return false return false
@ -320,7 +325,7 @@ object MusicPlayerRemote {
} else { } else {
openQueue(songs, 0, false) 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() Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show()
return true return true
} }

View file

@ -57,7 +57,6 @@ public final class FileUtil {
stream.close(); stream.close();
return baos.toByteArray(); return baos.toByteArray();
} }
@NonNull @NonNull
public static Observable<ArrayList<Song>> matchFilesWithMediaStore(@NonNull Context context, public static Observable<ArrayList<Song>> matchFilesWithMediaStore(@NonNull Context context,
@Nullable List<File> files) { @Nullable List<File> files) {
@ -263,4 +262,6 @@ public final class FileUtil {
return file.getAbsoluteFile(); return file.getAbsoluteFile();
} }
} }
} }

View file

@ -26,7 +26,6 @@ import android.os.Environment;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -263,31 +262,48 @@ public class MusicUtil {
} }
public static void deleteTracks(@NonNull final Activity activity, public static void deleteTracks(@NonNull final Activity activity,
@NonNull final List<Song> songs) { @NonNull final List<Song> songs,
@Nullable final List<Uri> safUris,
@Nullable final Runnable callback) {
final String[] projection = new String[]{ final String[] projection = new String[]{
BaseColumns._ID, MediaStore.MediaColumns.DATA BaseColumns._ID, MediaStore.MediaColumns.DATA
}; };
// 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(); final StringBuilder selection = new StringBuilder();
selection.append(BaseColumns._ID + " IN ("); selection.append(BaseColumns._ID + " IN (");
for (int i = 0; i < songs.size(); i++) {
selection.append(songs.get(i).getId()); for (int i = 0; (i < batchSize - 1) && (batchEnd < songCount - 1); i++, batchEnd++) {
if (i < songs.size() - 1) { 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(")"); selection.append(")");
try { try {
final Cursor cursor = activity.getContentResolver().query( final Cursor cursor = activity.getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(), MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
null, null); 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.
if (cursor != null) { if (cursor != null) {
// Step 1: Remove selected tracks from the current playlist, as well // Step 1: Remove selected tracks from the current playlist, as well
// as from the album art cache // as from the album art cache
cursor.moveToFirst(); cursor.moveToFirst();
while (!cursor.isAfterLast()) { while (!cursor.isAfterLast()) {
final int id = cursor.getInt(0); final int id = cursor.getInt(0);
Song song = SongLoader.INSTANCE.getSong(activity, id).blockingFirst(); final Song song = SongLoader.INSTANCE.getSong(activity, id).blockingFirst();
MusicPlayerRemote.INSTANCE.removeFromQueue(song); MusicPlayerRemote.INSTANCE.removeFromQueue(song);
cursor.moveToNext(); cursor.moveToNext();
} }
@ -298,31 +314,30 @@ public class MusicUtil {
// Step 3: Remove files from card // Step 3: Remove files from card
cursor.moveToFirst(); cursor.moveToFirst();
int i = batchStart;
while (!cursor.isAfterLast()) { while (!cursor.isAfterLast()) {
final String name = cursor.getString(1); final String name = cursor.getString(1);
try { // File.delete can throw a security exception final Uri safUri = safUris == null || safUris.size() <= i ? null : safUris.get(i);
final File f = new File(name); SAFUtil.delete(activity, name, safUri);
if (!f.delete()) { i++;
// 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);
}
cursor.moveToNext(); cursor.moveToNext();
} catch (@NonNull final SecurityException ex) {
cursor.moveToNext();
} catch (NullPointerException e) {
Log.e("MusicUtils", "Failed to find file " + name);
}
} }
cursor.close(); cursor.close();
} }
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) { } 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) { public static void deleteAlbumArt(@NonNull Context context, int albumId) {
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
Uri localUri = Uri.parse("content://media/external/audio/albumart"); Uri localUri = Uri.parse("content://media/external/audio/albumart");

View file

@ -19,6 +19,7 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.net.Uri;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import androidx.annotation.LayoutRes; 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_STYLE = "album_cover_style_id";
public static final String ALBUM_COVER_TRANSFORM = "album_cover_transform"; public static final String ALBUM_COVER_TRANSFORM = "album_cover_transform";
public static final String TAB_TEXT_MODE = "tab_text_mode"; 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 GENRE_SORT_ORDER = "genre_sort_order";
private static final String LAST_PAGE = "last_start_page"; private static final String LAST_PAGE = "last_start_page";
private static final String LAST_MUSIC_CHOOSER = "last_music_chooser"; 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")); return Integer.parseInt(mPreferences.getString(DEFAULT_START_PAGE, "-1"));
} }
public final int getLastPage() { public final int getLastPage() {
return mPreferences.getInt(LAST_PAGE, R.id.action_song); return mPreferences.getInt(LAST_PAGE, R.id.action_song);
} }
@ -292,7 +293,6 @@ public final class PreferenceUtil {
editor.apply(); editor.apply();
} }
public void setLastLyricsType(int group) { public void setLastLyricsType(int group) {
final SharedPreferences.Editor editor = mPreferences.edit(); final SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt(LAST_KNOWN_LYRICS_TYPE, group); editor.putInt(LAST_KNOWN_LYRICS_TYPE, group);
@ -388,7 +388,6 @@ public final class PreferenceUtil {
return mPreferences.getBoolean(IGNORE_MEDIA_STORE_ARTWORK, false); return mPreferences.getBoolean(IGNORE_MEDIA_STORE_ARTWORK, false);
} }
public int getLastSleepTimerValue() { public int getLastSleepTimerValue() {
return mPreferences.getInt(LAST_SLEEP_TIMER_VALUE, 30); return mPreferences.getInt(LAST_SLEEP_TIMER_VALUE, 30);
} }
@ -673,7 +672,6 @@ public final class PreferenceUtil {
return mPreferences.getBoolean(TOGGLE_HEADSET, false); return mPreferences.getBoolean(TOGGLE_HEADSET, false);
} }
public boolean isDominantColor() { public boolean isDominantColor() {
return mPreferences.getBoolean(DOMINANT_COLOR, false); return mPreferences.getBoolean(DOMINANT_COLOR, false);
} }
@ -698,7 +696,6 @@ public final class PreferenceUtil {
mPreferences.edit().putBoolean(CIRCULAR_ALBUM_ART, false).apply(); mPreferences.edit().putBoolean(CIRCULAR_ALBUM_ART, false).apply();
} }
public String getAlbumDetailsStyle() { public String getAlbumDetailsStyle() {
return mPreferences.getString(ALBUM_DETAIL_STYLE, "0"); return mPreferences.getString(ALBUM_DETAIL_STYLE, "0");
} }
@ -738,7 +735,6 @@ public final class PreferenceUtil {
return mPreferences.getBoolean(PAUSE_ON_ZERO_VOLUME, false); return mPreferences.getBoolean(PAUSE_ON_ZERO_VOLUME, false);
} }
public ViewPager.PageTransformer getAlbumCoverTransform() { public ViewPager.PageTransformer getAlbumCoverTransform() {
int style = Integer.parseInt(Objects.requireNonNull(mPreferences.getString(ALBUM_COVER_TRANSFORM, "0"))); int style = Integer.parseInt(Objects.requireNonNull(mPreferences.getString(ALBUM_COVER_TRANSFORM, "0")));
switch (style) { switch (style) {
@ -859,4 +855,12 @@ public final class PreferenceUtil {
defaultCategoryInfos.add(new CategoryInfo(CategoryInfo.Category.GENRES, false)); defaultCategoryInfos.add(new CategoryInfo(CategoryInfo.Category.GENRES, false));
return defaultCategoryInfos; 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();
}
} }

View file

@ -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<String> paths) {
for (String path : paths) {
if (isSAFRequired(path)) return true;
}
return false;
}
public static boolean isSAFRequiredForSongs(List<Song> 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<UriPermission> 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<String> 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<String> 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<String> 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());
}
}
}

View file

@ -56,6 +56,9 @@ public class OptionMenuItemView extends FrameLayout {
int accentColor = ThemeStore.Companion.accentColor(context); int accentColor = ThemeStore.Companion.accentColor(context);
setBackground(ContextCompat.getDrawable(context, R.drawable.menu_item_background)); setBackground(ContextCompat.getDrawable(context, R.drawable.menu_item_background));
setClickable(true);
setFocusable(true);
inflate(context, R.layout.item_option_menu, this); inflate(context, R.layout.item_option_menu, this);
setBackgroundTintList(ColorStateList.valueOf(ColorUtil.INSTANCE.adjustAlpha(accentColor, 0.22f))); setBackgroundTintList(ColorStateList.valueOf(ColorUtil.INSTANCE.adjustAlpha(accentColor, 0.22f)));

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

@ -12,7 +12,7 @@
~ See the GNU General Public License for more details. ~ See the GNU General Public License for more details.
--> -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"> <item android:state_selected="true" >
<shape> <shape>
<corners android:bottomRightRadius="30dp" android:topRightRadius="30dp" /> <corners android:bottomRightRadius="30dp" android:topRightRadius="30dp" />
<solid android:color="@color/md_red_400" /> <solid android:color="@color/md_red_400" />

View file

@ -13,9 +13,6 @@
android:id="@+id/actionLibrary" android:id="@+id/actionLibrary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?rectSelector"
android:clickable="true"
android:focusable="true"
app:optionIcon="@drawable/ic_library_music_white_24dp" app:optionIcon="@drawable/ic_library_music_white_24dp"
app:optionTitle="@string/library" /> app:optionTitle="@string/library" />
@ -23,9 +20,6 @@
android:id="@+id/actionFolders" android:id="@+id/actionFolders"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?rectSelector"
android:clickable="true"
android:focusable="true"
app:optionIcon="@drawable/ic_folder_white_24dp" app:optionIcon="@drawable/ic_folder_white_24dp"
app:optionTitle="@string/folders" /> app:optionTitle="@string/folders" />
@ -33,10 +27,13 @@
android:id="@+id/actionSettings" android:id="@+id/actionSettings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?rectSelector"
android:clickable="true"
android:focusable="true"
app:optionIcon="@drawable/ic_settings_white_24dp" app:optionIcon="@drawable/ic_settings_white_24dp"
app:optionTitle="@string/action_settings" /> app:optionTitle="@string/action_settings" />
<code.name.monkey.retromusic.views.OptionMenuItemView
android:id="@+id/actionRate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:optionIcon="@drawable/ic_star_white_24dp"
app:optionTitle="@string/rate_app" />
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<com.heinrichreimersoftware.materialintro.view.parallax.ParallaxLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fitsSystemWindows="false"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/mi_baseline">
<ImageView
android:id="@id/mi_image"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/mi_baseline"
android:layout_weight="1"
android:gravity="center"
app:layout_parallaxFactor="0.75"
tools:ignore="ContentDescription" />
<TextView
android:id="@id/mi_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/mi_baseline"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
tools:ignore="UnusedAttribute"
tools:text="Lorem ipsum" />
<TextView
android:id="@id/mi_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/mi_baseline"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="Lorem ipsum dolor sit amet, consectetur, adipisci velit, …" />
</com.heinrichreimersoftware.materialintro.view.parallax.ParallaxLinearLayout>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="14dp">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
</LinearLayout>

View file

@ -420,7 +420,8 @@
<string name="hinge">合页</string> <string name="hinge">合页</string>
<string name="select_preset">选择一个预设</string> <string name="select_preset">选择一个预设</string>
<string name="on">开启</string> <string name="on">开启</string>
<string name="shuffle">随机播放</string> ` <string name="shuffle">随机播放</string>
`
<string name="add_time_framed_lryics">加入带有时间轴的歌词</string> <string name="add_time_framed_lryics">加入带有时间轴的歌词</string>
<string name="dialog_title_set_ringtone">设置铃声</string> <string name="dialog_title_set_ringtone">设置铃声</string>
<string name="dialog_message_set_ringtone">请允许 Retro Music 更改音频设置</string> <string name="dialog_message_set_ringtone">请允许 Retro Music 更改音频设置</string>

View file

@ -632,4 +632,21 @@
<string name="pref_summary_library_categories">Configure visibility and order of library categories.</string> <string name="pref_summary_library_categories">Configure visibility and order of library categories.</string>
<string name="pref_header_controls">Controls</string> <string name="pref_header_controls">Controls</string>
<string name="pref_header_album">Album style</string> <string name="pref_header_album">Album style</string>
<!-- SAF -->
<string name="saf_error_uri">Can\'t get SAF URI</string>
<string name="saf_write_failed">File write failed: %s</string>
<string name="saf_delete_failed">File delete failed: %s</string>
<string name="saf_pick_sdcard">SD card access required. Please pick root directory of SD card</string>
<string name="saf_pick_file">File access required. Pick %s</string>
<!-- SAF guide -->
<string name="saf_guide_slide1_title">%s needs SD card access</string>
<string name="saf_guide_slide1_description_before_o">Enable \'Show SD card\' in overflow menu</string>
<string name="saf_guide_slide1_description">Open navigation drawer</string>
<string name="saf_guide_slide2_title">Select your SD card in navigation drawer</string>
<string name="saf_guide_slide2_description">You need to select your SD card root directory</string>
<string name="saf_guide_slide3_title">Tap \'select\' button at the bottom of the screen</string>
<string name="saf_guide_slide3_description">Do not open any sub-folders</string>
<string name="deleting_songs">Deleting songs</string>
</resources> </resources>

View file

@ -19,13 +19,25 @@
<!-- Deep Purple --> <!-- Deep Purple -->
<color name="md_deep_purple_A400">#651FFF</color> <color name="md_deep_purple_A400">#651FFF</color>
<color name="md_deep_purple_300">#9575CD</color>
<color name="md_deep_purple_400">#7E57C2</color>
<color name="md_deep_purple_600">#5E35B1</color>
<color name="md_deep_purple_700">#512DA8</color>
<color name="md_deep_purple_800">#4527A0</color>
<color name="md_deep_purple_500A12">#30673AB7</color> <color name="md_deep_purple_500A12">#30673AB7</color>
<color name="md_deep_purple_500">#673AB7</color> <color name="md_deep_purple_500">#673AB7</color>
<color name="md_deep_purple_A700">#6200EA</color> <color name="md_deep_purple_A700">#6200EA</color>
<!-- Indigo --> <!-- Indigo -->
<color name="md_indigo_500">#3F51B5</color> <color name="md_indigo_500">#3F51B5</color>
<color name="md_indigo_300">#7986CB</color>
<color name="md_indigo_400">#5C6BC0</color>
<color name="md_indigo_600">#3949AB</color>
<color name="md_indigo_700">#303F9F</color>
<color name="md_indigo_800">#283593</color>
<!-- Blue --> <!-- Blue -->
<color name="md_blue_500">#2196F3</color> <color name="md_blue_500">#2196F3</color>