Merge pull request #252 from kabouzeid/revert-241-sdcard

Revert "SD card write access using SAF API (#94)"
This commit is contained in:
Karim Abou Zeid 2017-08-31 12:14:58 +02:00 committed by GitHub
commit 1c0e4fda51
15 changed files with 42 additions and 693 deletions

View file

@ -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" />

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {
} }
} }

View file

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

View file

@ -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

View file

@ -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>