Merge pull request #252 from kabouzeid/revert-241-sdcard
Revert "SD card write access using SAF API (#94)"
This commit is contained in:
commit
1c0e4fda51
15 changed files with 42 additions and 693 deletions
|
|
@ -113,9 +113,6 @@
|
||||||
android:name=".ui.activities.intro.AppIntroActivity"
|
android:name=".ui.activities.intro.AppIntroActivity"
|
||||||
android:label="@string/intro_label"
|
android:label="@string/intro_label"
|
||||||
android:theme="@style/Theme.Intro" />
|
android:theme="@style/Theme.Intro" />
|
||||||
<activity
|
|
||||||
android:name=".ui.activities.saf.SAFGuideActivity"
|
|
||||||
android:theme="@style/Theme.Intro"/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activities.bugreport.BugReportActivity"
|
android:name=".ui.activities.bugreport.BugReportActivity"
|
||||||
android:label="@string/report_an_issue" />
|
android:label="@string/report_an_issue" />
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,24 @@
|
||||||
package com.kabouzeid.gramophone.dialogs;
|
package com.kabouzeid.gramophone.dialogs;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.afollestad.materialdialogs.DialogAction;
|
import com.afollestad.materialdialogs.DialogAction;
|
||||||
import com.afollestad.materialdialogs.MaterialDialog;
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
import com.kabouzeid.gramophone.R;
|
import com.kabouzeid.gramophone.R;
|
||||||
import com.kabouzeid.gramophone.misc.DialogAsyncTask;
|
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
import com.kabouzeid.gramophone.ui.activities.saf.SAFGuideActivity;
|
|
||||||
import com.kabouzeid.gramophone.util.MusicUtil;
|
import com.kabouzeid.gramophone.util.MusicUtil;
|
||||||
import com.kabouzeid.gramophone.util.SAFUtil;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Karim Abou Zeid (kabouzeid), Aidan Follestad (afollestad)
|
* @author Karim Abou Zeid (kabouzeid), Aidan Follestad (afollestad)
|
||||||
*/
|
*/
|
||||||
public class DeleteSongsDialog extends DialogFragment {
|
public class DeleteSongsDialog extends DialogFragment {
|
||||||
|
|
||||||
private ArrayList<Song> songsToRemove;
|
|
||||||
private Song currentSong;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static DeleteSongsDialog create(Song song) {
|
public static DeleteSongsDialog create(Song song) {
|
||||||
ArrayList<Song> list = new ArrayList<>();
|
ArrayList<Song> list = new ArrayList<>();
|
||||||
|
|
@ -71,171 +54,14 @@ public class DeleteSongsDialog extends DialogFragment {
|
||||||
.content(content)
|
.content(content)
|
||||||
.positiveText(R.string.delete_action)
|
.positiveText(R.string.delete_action)
|
||||||
.negativeText(android.R.string.cancel)
|
.negativeText(android.R.string.cancel)
|
||||||
.autoDismiss(false)
|
|
||||||
.onPositive(new MaterialDialog.SingleButtonCallback() {
|
.onPositive(new MaterialDialog.SingleButtonCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
|
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
|
||||||
songsToRemove = songs;
|
if (getActivity() == null)
|
||||||
new DeleteSongsAsyncTask(DeleteSongsDialog.this).execute(new DeleteSongsAsyncTask.LoadingInfo(songs, null));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onNegative(new MaterialDialog.SingleButtonCallback() {
|
|
||||||
@Override
|
|
||||||
public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) {
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSongs(List<Song> songs, List<Uri> safUris) {
|
|
||||||
MusicUtil.deleteTracks(getActivity(), songs, safUris);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
private void deleteSongsKitkat() {
|
|
||||||
if (songsToRemove.size() < 1) {
|
|
||||||
dismiss();
|
|
||||||
return;
|
return;
|
||||||
|
MusicUtil.deleteTracks(getActivity(), songs);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
currentSong = songsToRemove.remove(0);
|
|
||||||
|
|
||||||
if (!SAFUtil.isSAFRequired(currentSong)) {
|
|
||||||
deleteSongs(Collections.singletonList(currentSong), null);
|
|
||||||
deleteSongsKitkat();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getActivity(), String.format(getString(R.string.saf_pick_file), currentSong.data), Toast.LENGTH_LONG).show();
|
|
||||||
SAFUtil.openFilePicker(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, intent);
|
|
||||||
switch (requestCode) {
|
|
||||||
case SAFGuideActivity.REQUEST_CODE_SAF_GUIDE:
|
|
||||||
SAFUtil.openTreePicker(this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SAFUtil.REQUEST_SAF_PICK_TREE:
|
|
||||||
case SAFUtil.REQUEST_SAF_PICK_FILE:
|
|
||||||
new DeleteSongsAsyncTask(this).execute(new DeleteSongsAsyncTask.LoadingInfo(requestCode, resultCode, intent));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DeleteSongsAsyncTask extends DialogAsyncTask<DeleteSongsAsyncTask.LoadingInfo, Integer, Void> {
|
|
||||||
private WeakReference<DeleteSongsDialog> dialog;
|
|
||||||
private WeakReference<FragmentActivity> activity;
|
|
||||||
|
|
||||||
public DeleteSongsAsyncTask(DeleteSongsDialog dialog) {
|
|
||||||
super(dialog.getActivity());
|
|
||||||
this.dialog = new WeakReference<>(dialog);
|
|
||||||
this.activity = new WeakReference<>(dialog.getActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(LoadingInfo... params) {
|
|
||||||
try {
|
|
||||||
LoadingInfo info = params[0];
|
|
||||||
|
|
||||||
DeleteSongsDialog dialog = this.dialog.get();
|
|
||||||
FragmentActivity activity = this.activity.get();
|
|
||||||
|
|
||||||
if (dialog == null || activity == 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(activity)) {
|
|
||||||
dialog.deleteSongs(info.songs, null);
|
|
||||||
} else {
|
|
||||||
dialog.startActivityForResult(new Intent(activity, SAFGuideActivity.class), SAFGuideActivity.REQUEST_CODE_SAF_GUIDE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialog.deleteSongsKitkat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (info.requestCode) {
|
|
||||||
case SAFUtil.REQUEST_SAF_PICK_TREE:
|
|
||||||
if (info.resultCode == Activity.RESULT_OK) {
|
|
||||||
SAFUtil.saveTreeUri(activity, info.intent);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
super.onPostExecute(v);
|
|
||||||
|
|
||||||
DeleteSongsDialog dialog = this.dialog.get();
|
|
||||||
FragmentActivity activity = this.activity.get();
|
|
||||||
if (dialog != null && activity != null && !activity.isFinishing()) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCancelled() {
|
|
||||||
super.onCancelled();
|
|
||||||
|
|
||||||
DeleteSongsDialog dialog = this.dialog.get();
|
|
||||||
FragmentActivity activity = this.activity.get();
|
|
||||||
if (dialog != null && activity != null && !activity.isFinishing()) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Dialog createDialog(@NonNull Context context) {
|
|
||||||
return new MaterialDialog.Builder(context)
|
|
||||||
.title(R.string.deleting_songs)
|
|
||||||
.cancelable(false)
|
|
||||||
.progress(true, 0)
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package com.kabouzeid.gramophone.ui.activities.saf;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import com.heinrichreimersoftware.materialintro.app.IntroActivity;
|
|
||||||
import com.heinrichreimersoftware.materialintro.slide.SimpleSlide;
|
|
||||||
import com.kabouzeid.gramophone.R;
|
|
||||||
|
|
||||||
|
|
||||||
public class SAFGuideActivity extends IntroActivity {
|
|
||||||
|
|
||||||
public static final int REQUEST_CODE_SAF_GUIDE = 98;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(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_indigo_300)
|
|
||||||
.backgroundDark(R.color.md_indigo_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_indigo_500)
|
|
||||||
.backgroundDark(R.color.md_indigo_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_indigo_700)
|
|
||||||
.backgroundDark(R.color.md_indigo_800)
|
|
||||||
.layout(R.layout.fragment_simple_slide_large_image)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.kabouzeid.gramophone.ui.activities.tageditor;
|
package com.kabouzeid.gramophone.ui.activities.tageditor;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
|
|
@ -10,7 +9,6 @@ import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
@ -22,7 +20,6 @@ import android.view.View;
|
||||||
import android.view.animation.OvershootInterpolator;
|
import android.view.animation.OvershootInterpolator;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog;
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
import com.github.ksoichiro.android.observablescrollview.ObservableScrollView;
|
import com.github.ksoichiro.android.observablescrollview.ObservableScrollView;
|
||||||
|
|
@ -34,25 +31,25 @@ import com.kabouzeid.gramophone.misc.DialogAsyncTask;
|
||||||
import com.kabouzeid.gramophone.misc.SimpleObservableScrollViewCallbacks;
|
import com.kabouzeid.gramophone.misc.SimpleObservableScrollViewCallbacks;
|
||||||
import com.kabouzeid.gramophone.misc.UpdateToastMediaScannerCompletionListener;
|
import com.kabouzeid.gramophone.misc.UpdateToastMediaScannerCompletionListener;
|
||||||
import com.kabouzeid.gramophone.ui.activities.base.AbsBaseActivity;
|
import com.kabouzeid.gramophone.ui.activities.base.AbsBaseActivity;
|
||||||
import com.kabouzeid.gramophone.ui.activities.saf.SAFGuideActivity;
|
|
||||||
import com.kabouzeid.gramophone.util.MusicUtil;
|
import com.kabouzeid.gramophone.util.MusicUtil;
|
||||||
import com.kabouzeid.gramophone.util.SAFUtil;
|
|
||||||
import com.kabouzeid.gramophone.util.Util;
|
import com.kabouzeid.gramophone.util.Util;
|
||||||
|
|
||||||
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.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
@ -98,11 +95,6 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
};
|
};
|
||||||
private List<String> songPaths;
|
private List<String> songPaths;
|
||||||
|
|
||||||
private List<String> savedSongPaths;
|
|
||||||
private String currentSongPath;
|
|
||||||
private Map<FieldKey, String> savedTags;
|
|
||||||
private ArtworkInfo savedArtworkInfo;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -265,16 +257,6 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
fab.setEnabled(true);
|
fab.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideFab() {
|
|
||||||
fab.animate()
|
|
||||||
.setDuration(500)
|
|
||||||
.setInterpolator(new OvershootInterpolator())
|
|
||||||
.scaleX(0)
|
|
||||||
.scaleY(0)
|
|
||||||
.start();
|
|
||||||
fab.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setImageBitmap(@Nullable final Bitmap bitmap, int bgColor) {
|
protected void setImageBitmap(@Nullable final Bitmap bitmap, int bgColor) {
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
image.setImageResource(R.drawable.default_album_art);
|
image.setImageResource(R.drawable.default_album_art);
|
||||||
|
|
@ -296,52 +278,15 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
protected void writeValuesToFiles(@NonNull final Map<FieldKey, String> fieldKeyValueMap, @Nullable final ArtworkInfo artworkInfo) {
|
protected void writeValuesToFiles(@NonNull final Map<FieldKey, String> fieldKeyValueMap, @Nullable final ArtworkInfo artworkInfo) {
|
||||||
Util.hideSoftKeyboard(this);
|
Util.hideSoftKeyboard(this);
|
||||||
|
|
||||||
hideFab();
|
new WriteTagsAsyncTask(this).execute(new 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(new Intent(this, SAFGuideActivity.class), SAFGuideActivity.REQUEST_CODE_SAF_GUIDE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeTagsKitkat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeTags(List<String> paths) {
|
|
||||||
new WriteTagsAsyncTask(this).execute(new WriteTagsAsyncTask.LoadingInfo(paths, savedTags, savedArtworkInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
private void writeTagsKitkat() {
|
|
||||||
if (savedSongPaths.size() < 1) return;
|
|
||||||
|
|
||||||
currentSongPath = savedSongPaths.remove(0);
|
|
||||||
|
|
||||||
if (!SAFUtil.isSAFRequired(currentSongPath)) {
|
|
||||||
writeTags(Collections.singletonList(currentSongPath));
|
|
||||||
writeTagsKitkat();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, String.format(getString(R.string.saf_pick_file), currentSongPath), Toast.LENGTH_LONG).show();
|
|
||||||
SAFUtil.openFilePicker(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WriteTagsAsyncTask extends DialogAsyncTask<WriteTagsAsyncTask.LoadingInfo, Integer, String[]> {
|
private static class WriteTagsAsyncTask extends DialogAsyncTask<WriteTagsAsyncTask.LoadingInfo, Integer, String[]> {
|
||||||
private WeakReference<Activity> activity;
|
Context applicationContext;
|
||||||
|
|
||||||
public WriteTagsAsyncTask(Activity activity) {
|
public WriteTagsAsyncTask(Context context) {
|
||||||
super(activity);
|
super(context);
|
||||||
this.activity = new WeakReference<>(activity);
|
applicationContext = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -367,14 +312,6 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
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();
|
||||||
|
|
||||||
|
|
@ -399,10 +336,8 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity activity = this.activity.get();
|
audioFile.commit();
|
||||||
|
} catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) {
|
||||||
SAFUtil.write(activity, audioFile, safUri);
|
|
||||||
} catch (@NonNull Exception e) {
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -416,18 +351,7 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<String> paths = info.filePaths;
|
return info.filePaths.toArray(new String[info.filePaths.size()]);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { // remove SAF URI from paths
|
|
||||||
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;
|
||||||
|
|
@ -447,10 +371,8 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scan(String[] toBeScanned) {
|
private void scan(String[] toBeScanned) {
|
||||||
Activity activity = this.activity.get();
|
Context context = getContext();
|
||||||
if (activity != null) {
|
MediaScannerConnection.scanFile(applicationContext, toBeScanned, null, context instanceof Activity ? new UpdateToastMediaScannerCompletionListener((Activity) context, toBeScanned) : null);
|
||||||
MediaScannerConnection.scanFile(activity, toBeScanned, null, new UpdateToastMediaScannerCompletionListener(activity, toBeScanned));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -499,32 +421,14 @@ public abstract class AbsTagEditorActivity extends AbsBaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, @NonNull Intent intent) {
|
protected void onActivityResult(int requestCode, int resultCode, @NonNull Intent imageReturnedIntent) {
|
||||||
super.onActivityResult(requestCode, resultCode, intent);
|
super.onActivityResult(requestCode, resultCode, imageReturnedIntent);
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case REQUEST_CODE_SELECT_IMAGE:
|
case REQUEST_CODE_SELECT_IMAGE:
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
Uri selectedImage = intent.getData();
|
Uri selectedImage = imageReturnedIntent.getData();
|
||||||
loadImageFromFile(selectedImage);
|
loadImageFromFile(selectedImage);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case SAFGuideActivity.REQUEST_CODE_SAF_GUIDE:
|
|
||||||
SAFUtil.openTreePicker(this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SAFUtil.REQUEST_SAF_PICK_TREE:
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
SAFUtil.saveTreeUri(this, intent);
|
|
||||||
writeTags(savedSongPaths);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SAFUtil.REQUEST_SAF_PICK_FILE:
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
writeTags(Collections.singletonList(currentSongPath + SAFUtil.SEPARATOR + intent.getDataString()));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import com.kabouzeid.gramophone.loader.SortedCursor;
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileFilter;
|
import java.io.FileFilter;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
|
@ -191,15 +190,4 @@ public final class FileUtil {
|
||||||
fin.close();
|
fin.close();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] readBytes(InputStream stream) throws IOException {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int count;
|
|
||||||
while ((count = stream.read(buffer)) != -1) {
|
|
||||||
baos.write(buffer, 0, count);
|
|
||||||
}
|
|
||||||
stream.close();
|
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.kabouzeid.gramophone.util;
|
package com.kabouzeid.gramophone.util;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
|
@ -15,6 +14,7 @@ import android.provider.Settings;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.kabouzeid.gramophone.R;
|
import com.kabouzeid.gramophone.R;
|
||||||
|
|
@ -23,6 +23,7 @@ import com.kabouzeid.gramophone.loader.PlaylistLoader;
|
||||||
import com.kabouzeid.gramophone.loader.SongLoader;
|
import com.kabouzeid.gramophone.loader.SongLoader;
|
||||||
import com.kabouzeid.gramophone.model.Artist;
|
import com.kabouzeid.gramophone.model.Artist;
|
||||||
import com.kabouzeid.gramophone.model.Playlist;
|
import com.kabouzeid.gramophone.model.Playlist;
|
||||||
|
import com.kabouzeid.gramophone.model.PlaylistSong;
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
|
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
|
||||||
|
|
||||||
|
|
@ -174,7 +175,7 @@ public class MusicUtil {
|
||||||
return albumArtDir;
|
return albumArtDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void deleteTracks(@NonNull final Activity activity, @NonNull final List<Song> songs, @Nullable final List<Uri> safUris) {
|
public static void deleteTracks(@NonNull final Context context, @NonNull final List<Song> songs) {
|
||||||
final String[] projection = new String[]{
|
final String[] projection = new String[]{
|
||||||
BaseColumns._ID, MediaStore.MediaColumns.DATA
|
BaseColumns._ID, MediaStore.MediaColumns.DATA
|
||||||
};
|
};
|
||||||
|
|
@ -189,7 +190,7 @@ public class MusicUtil {
|
||||||
selection.append(")");
|
selection.append(")");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Cursor cursor = activity.getContentResolver().query(
|
final Cursor cursor = context.getContentResolver().query(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
|
||||||
null, null);
|
null, null);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
|
|
@ -198,35 +199,37 @@ public class MusicUtil {
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
while (!cursor.isAfterLast()) {
|
while (!cursor.isAfterLast()) {
|
||||||
final int id = cursor.getInt(0);
|
final int id = cursor.getInt(0);
|
||||||
final Song song = SongLoader.getSong(activity, id);
|
final Song song = SongLoader.getSong(context, id);
|
||||||
MusicPlayerRemote.removeFromQueue(song);
|
MusicPlayerRemote.removeFromQueue(song);
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Remove selected tracks from the database
|
// Step 2: Remove selected tracks from the database
|
||||||
activity.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
selection.toString(), null);
|
selection.toString(), null);
|
||||||
|
|
||||||
// Step 3: Remove files from card
|
// Step 3: Remove files from card
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
int i = 0;
|
|
||||||
while (!cursor.isAfterLast()) {
|
while (!cursor.isAfterLast()) {
|
||||||
final String name = cursor.getString(1);
|
final String name = cursor.getString(1);
|
||||||
final Uri safUri = safUris == null || safUris.size() <= i ? null : safUris.get(i);
|
try { // File.delete can throw a security exception
|
||||||
SAFUtil.delete(activity, name, safUri);
|
final File f = new File(name);
|
||||||
i++;
|
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);
|
||||||
|
}
|
||||||
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);
|
context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
|
||||||
|
Toast.makeText(context, context.getString(R.string.deleted_x_songs, songs.size()), Toast.LENGTH_SHORT).show();
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(activity, activity.getString(R.string.deleted_x_songs, songs.size()), Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (SecurityException ignored) {
|
} catch (SecurityException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.kabouzeid.gramophone.util;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.StyleRes;
|
import android.support.annotation.StyleRes;
|
||||||
|
|
@ -72,8 +71,6 @@ public final class PreferenceUtil {
|
||||||
|
|
||||||
public static final String SYNCHRONIZED_LYRICS_SHOW = "synchronized_lyrics_show";
|
public static final String SYNCHRONIZED_LYRICS_SHOW = "synchronized_lyrics_show";
|
||||||
|
|
||||||
public static final String SAF_SDCARD_URI = "saf_sdcard_uri";
|
|
||||||
|
|
||||||
private static PreferenceUtil sInstance;
|
private static PreferenceUtil sInstance;
|
||||||
|
|
||||||
private final SharedPreferences mPreferences;
|
private final SharedPreferences mPreferences;
|
||||||
|
|
@ -409,12 +406,4 @@ public final class PreferenceUtil {
|
||||||
public final boolean synchronizedLyricsShow() {
|
public final boolean synchronizedLyricsShow() {
|
||||||
return mPreferences.getBoolean(SYNCHRONIZED_LYRICS_SHOW, true);
|
return mPreferences.getBoolean(SYNCHRONIZED_LYRICS_SHOW, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
||||||
package com.kabouzeid.gramophone.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.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v4.provider.DocumentFile;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.kabouzeid.gramophone.R;
|
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(context).setSAFSDCardUri(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public static boolean isTreeUriSaved(Context context) {
|
|
||||||
return !TextUtils.isEmpty(PreferenceUtil.getInstance(context).getSAFSDCardUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public static boolean isSDCardAccessGranted(Context context) {
|
|
||||||
if (!isTreeUriSaved(context)) return false;
|
|
||||||
|
|
||||||
String sdcardUri = PreferenceUtil.getInstance(context).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(context).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 (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(context).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(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
|
|
@ -262,7 +262,6 @@
|
||||||
<string name="copied_device_info_to_clipboard">Copied device info to clipboard.</string>
|
<string name="copied_device_info_to_clipboard">Copied device info to clipboard.</string>
|
||||||
<string name="your_account_data_is_only_used_for_authentication">Your account data is only used for authentication.</string>
|
<string name="your_account_data_is_only_used_for_authentication">Your account data is only used for authentication.</string>
|
||||||
<string name="you_will_be_forwarded_to_the_issue_tracker_website">You will be forwarded to the issue tracker website.</string>
|
<string name="you_will_be_forwarded_to_the_issue_tracker_website">You will be forwarded to the issue tracker website.</string>
|
||||||
<string name="deleting_songs">Deleting songs</string>
|
|
||||||
<!-- App Shortcuts -->
|
<!-- App Shortcuts -->
|
||||||
<string name="app_shortcut_shuffle_all_long">@string/action_shuffle_all</string>
|
<string name="app_shortcut_shuffle_all_long">@string/action_shuffle_all</string>
|
||||||
<string name="app_shortcut_shuffle_all_short">Shuffle</string>
|
<string name="app_shortcut_shuffle_all_short">Shuffle</string>
|
||||||
|
|
@ -273,18 +272,4 @@
|
||||||
<string name="playlist_is_empty">Playlist is empty</string>
|
<string name="playlist_is_empty">Playlist is empty</string>
|
||||||
<string name="playing_notification_description">The playing notification provides actions for play/pause etc.</string>
|
<string name="playing_notification_description">The playing notification provides actions for play/pause etc.</string>
|
||||||
<string name="playing_notification_name">Playing notification</string>
|
<string name="playing_notification_name">Playing notification</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 subfolders</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue