1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-21 18:42:35 +01:00

more SAF implementation

* full support for Directory API (Android Lollipop or later)
* best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download
* implemented directory choosing
* fix download database version upgrading
* misc. cleanup
* do not release permission on the old save path (if the user change the download directory) under SAF api
This commit is contained in:
kapodamy 2019-04-09 18:38:34 -03:00
parent f6b32823ba
commit d00dc798f4
28 changed files with 946 additions and 589 deletions

View File

@ -15,12 +15,13 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.support.v4.provider.DocumentFile;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.view.menu.ActionMenuItemView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -177,9 +178,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return; return;
} }
final Context context = getContext(); context = getContext();
if (context == null)
throw new RuntimeException("Context was null");
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
@ -321,11 +320,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
showFailedDialog(R.string.general_error); showFailedDialog(R.string.general_error);
return; return;
} }
try {
continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
} catch (IOException e) { if (docFile == null) {
showErrorActivity(e); showFailedDialog(R.string.general_error);
return;
} }
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
} }
} }
@ -337,14 +340,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false);// disable until the download service connection is done
toolbar.setTitle(R.string.download_dialog_title); toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url); toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false);// disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> { toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) { if (item.getItemId() == R.id.okay) {
@ -504,15 +507,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
StoredDirectoryHelper mainStorageAudio = null; StoredDirectoryHelper mainStorageAudio = null;
StoredDirectoryHelper mainStorageVideo = null; StoredDirectoryHelper mainStorageVideo = null;
DownloadManager downloadManager = null; DownloadManager downloadManager = null;
ActionMenuItemView okButton = null;
MenuItem okButton = null; Context context;
private String getNameEditText() { private String getNameEditText() {
return nameEditText.getText().toString().trim(); String str = nameEditText.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
} }
private void showFailedDialog(@StringRes int msg) { private void showFailedDialog(@StringRes int msg) {
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(context)
.setMessage(msg) .setMessage(msg)
.setNegativeButton(android.R.string.ok, null) .setNegativeButton(android.R.string.ok, null)
.create() .create()
@ -521,7 +526,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private void showErrorActivity(Exception e) { private void showErrorActivity(Exception e) {
ErrorActivity.reportError( ErrorActivity.reportError(
getContext(), context,
Collections.singletonList(e), Collections.singletonList(e),
null, null,
null, null,
@ -530,18 +535,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
} }
private void prepareSelectedDownload() { private void prepareSelectedDownload() {
final Context context = getContext();
StoredDirectoryHelper mainStorage; StoredDirectoryHelper mainStorage;
MediaFormat format; MediaFormat format;
String mime; String mime;
// first, build the filename and get the output folder (if possible) // first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
String filename = getNameEditText() + "."; String filename = getNameEditText().concat(".");
if (filename.isEmpty()) {
filename = FilenameUtils.createFilename(context, currentInfo.getName());
}
filename += ".";
switch (radioStreamsGroup.getCheckedRadioButtonId()) { switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: case R.id.audio_button:
@ -567,34 +568,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
} }
if (mainStorage == null) { if (mainStorage == null) {
// this part is called if... // This part is called if with SAF preferred:
// older android version running with SAF preferred // * older android version running
// save path not defined (via download settings) // * save path not defined (via download settings)
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
return; return;
} }
// check for existing file with the same name // check for existing file with the same name
Uri result = mainStorage.findFile(filename); checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
}
if (result == null) { private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
// the file does not exists, create
StoredFileHelper storage = mainStorage.createFile(filename, mime);
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
}
// the target filename is already use, try load
StoredFileHelper storage; StoredFileHelper storage;
try { try {
storage = new StoredFileHelper(context, result, mime); if (mainStorage == null) {
} catch (IOException e) { // using SAF on older android version
storage = new StoredFileHelper(context, null, targetFile, "");
} else if (targetFile == null) {
// the file does not exist, but it is probably used in a pending download
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
} else {
// the target filename is already use, attempt to use it
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
}
} catch (Exception e) {
showErrorActivity(e); showErrorActivity(e);
return; return;
} }
@ -618,6 +618,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
msgBody = R.string.download_already_running; msgBody = R.string.download_already_running;
break; break;
case None: case None:
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
continueSelectedDownload(storage);
return;
} else if (targetFile == null) {
// This part is called if:
// * the filename is not used in a pending/finished download
// * the file does not exists, create
storage = mainStorage.createFile(filename, mime);
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
}
msgBtn = R.string.overwrite; msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_unrelated_warning; msgBody = R.string.overwrite_unrelated_warning;
break; break;
@ -625,49 +644,73 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return; return;
} }
// handle user answer (overwrite or create another file with different name)
final String finalFilename = filename;
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
StoredFileHelper storageNew; AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
switch (state) { .setTitle(R.string.download_dialog_title)
case Finished: .setMessage(msgBody)
case Pending: .setNegativeButton(android.R.string.cancel, null);
downloadManager.forgetMission(storage); final StoredFileHelper finalStorage = storage;
case None:
// try take (or steal) the file permissions
try { if (mainStorage == null) {
storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); // This part is called if:
if (storageNew.canWrite()) // * using SAF on older android version
continueSelectedDownload(storageNew); // * save path not defined
else switch (state) {
showFailedDialog(R.string.error_file_creation); case Pending:
} catch (IOException e) { case Finished:
showErrorActivity(e); askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
} dialog.dismiss();
break; downloadManager.forgetMission(finalStorage);
case PendingRunning: continueSelectedDownload(finalStorage);
// FIXME: createUniqueFile() is not tested properly });
storageNew = mainStorage.createUniqueFile(finalFilename, mime); break;
if (storageNew == null) }
showFailedDialog(R.string.error_file_creation);
else askDialog.create().show();
continueSelectedDownload(storageNew); return;
break; }
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
StoredFileHelper storageNew;
switch (state) {
case Finished:
case Pending:
downloadManager.forgetMission(finalStorage);
case None:
if (targetFile == null) {
storageNew = mainStorage.createFile(filename, mime);
} else {
try {
// try take (or steal) the file
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
} catch (IOException e) {
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
storageNew = null;
}
} }
})
.setNegativeButton(android.R.string.cancel, null) if (storageNew != null && storageNew.canWrite())
.create() continueSelectedDownload(storageNew);
.show(); else
showFailedDialog(R.string.error_file_creation);
break;
case PendingRunning:
storageNew = mainStorage.createUniqueFile(filename, mime);
if (storageNew == null)
showFailedDialog(R.string.error_file_creation);
else
continueSelectedDownload(storageNew);
break;
}
});
askDialog.create().show();
} }
private void continueSelectedDownload(@NonNull StoredFileHelper storage) { private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
final Context context = getContext();
if (!storage.canWrite()) { if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied); showFailedDialog(R.string.permission_denied);
return; return;
@ -678,7 +721,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (storage.length() > 0) storage.truncate(); if (storage.length() > 0) storage.truncate();
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e);
//showErrorActivity(e);
showFailedDialog(R.string.overwrite_failed); showFailedDialog(R.string.overwrite_failed);
return; return;
} }
@ -748,7 +790,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (secondaryStreamUrl == null) { if (secondaryStreamUrl == null) {
urls = new String[]{selectedStream.getUrl()}; urls = new String[]{selectedStream.getUrl()};
} else { } else {
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()};
} }
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);

View File

@ -14,18 +14,23 @@ import android.support.v7.preference.Preference;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import us.shandian.giga.io.StoredDirectoryHelper; import us.shandian.giga.io.StoredDirectoryHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment { public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
public static final boolean IGNORE_RELEASE_OLD_PATH = true;
private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
private String DOWNLOAD_PATH_AUDIO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
@ -35,41 +40,46 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
private Preference prefPathVideo; private Preference prefPathVideo;
private Preference prefPathAudio; private Preference prefPathAudio;
private Context ctx; private Context ctx;
private boolean lastAPIJavaIO;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
initKeys(); DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
updatePreferencesSummary(); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
} DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api);
DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default);
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.download_settings);
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
updatePathPickers(usingJavaIO()); lastAPIJavaIO = usingJavaIO();
updatePreferencesSummary();
updatePathPickers(lastAPIJavaIO);
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> {
boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value);
if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (javaIO == lastAPIJavaIO) return true;
lastAPIJavaIO = javaIO;
boolean res;
if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// forget save paths (if necessary)
res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE);
res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE);
} else {
res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE);
}
if (res) {
Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show();
// forget save paths
forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE);
forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE);
defaultPreferences.edit()
.putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "")
.putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "")
.apply();
updatePreferencesSummary(); updatePreferencesSummary();
} }
@ -78,6 +88,30 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}); });
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean forgetPath(String prefKey) {
String path = defaultPreferences.getString(prefKey, "");
if (path == null || path.isEmpty()) return true;
if (path.startsWith("file://")) return false;
// forget SAF path (file:// is compatible with the SAF wrapper)
forgetSAFTree(getContext(), prefKey);
defaultPreferences.edit().putString(prefKey, "").apply();
return true;
}
private boolean hasInvalidPath(String prefKey) {
String value = defaultPreferences.getString(prefKey, null);
return value == null || value.isEmpty();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.download_settings);
}
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
@ -91,20 +125,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null);
} }
private void initKeys() { private void updatePreferencesSummary() {
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api);
DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default);
} }
private void updatePreferencesSummary() { private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
prefPathVideo.setSummary( String rawUri = defaultPreferences.getString(prefKey, null);
defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) if (rawUri == null || rawUri.isEmpty()) {
); target.setSummary(getString(defaultString));
prefPathAudio.setSummary( return;
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) }
);
try {
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// nothing to do
}
target.setSummary(rawUri);
} }
private void updatePathPickers(boolean useJavaIO) { private void updatePathPickers(boolean useJavaIO) {
@ -119,20 +158,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
); );
} }
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private void forgetSAFTree(String prefKey) { private void forgetSAFTree(Context ctx, String prefKey) {
if (IGNORE_RELEASE_OLD_PATH) {
return;
}
String oldPath = defaultPreferences.getString(prefKey, ""); String oldPath = defaultPreferences.getString(prefKey, "");
if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) {
try { try {
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); Uri uri = Uri.parse(oldPath);
if (!mainStorage.isDirect()) {
mainStorage.revokePermissions(); ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!"); ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
}
} catch (IOException err) { Log.i(TAG, "Revoke old path permissions success on " + oldPath);
Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err); } catch (Exception err) {
Log.e(TAG, "Error revoking old path permissions on " + oldPath, err);
} }
} }
} }
@ -167,7 +211,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra("android.content.extra.SHOW_ADVANCED", true) .putExtra("android.content.extra.SHOW_ADVANCED", true)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
} else { } else {
i = new Intent(getActivity(), FilePickerActivityHelper.class) i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
@ -208,27 +252,37 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// steps: // steps:
// 1. acquire permissions on the new save path // 1. revoke permissions on the old save path
// 2. save the new path, if step(1) was successful // 2. acquire permissions on the new save path
// 3. save the new path, if step(2) was successful
final Context ctx = getContext();
if (ctx == null) throw new NullPointerException("getContext()");
forgetSAFTree(ctx, key);
try { try {
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
mainStorage.acquirePermissions(); Log.i(TAG, "Acquiring tree success from " + uri.toString());
Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!");
if (!mainStorage.canWrite())
throw new IOException("No write permissions on " + uri.toString());
} catch (IOException err) { } catch (IOException err) {
Log.e(TAG, "Error acquiring permissions on " + uri.toString()); Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
showMessageDialog(R.string.general_error, R.string.no_available_dir); showMessageDialog(R.string.general_error, R.string.no_available_dir);
return; return;
} }
defaultPreferences.edit().putString(key, uri.toString()).apply();
} else { } else {
defaultPreferences.edit().putString(key, uri.toString()).apply(); File target = Utils.getFileForUri(data.getData());
updatePreferencesSummary(); if (!target.canWrite()) {
File target = new File(URI.create(uri.toString()));
if (!target.canWrite())
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
return;
}
uri = Uri.fromFile(target);
} }
defaultPreferences.edit().putString(key, uri.toString()).apply();
updatePreferencesSummary();
} }
} }

View File

@ -16,6 +16,8 @@ public class DataReader {
public final static int INTEGER_SIZE = 4; public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4; public final static int FLOAT_SIZE = 4;
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
private long position = 0; private long position = 0;
private final SharpStream stream; private final SharpStream stream;
@ -229,7 +231,7 @@ public class DataReader {
} }
} }
private final byte[] readBuffer = new byte[8 * 1024]; private final byte[] readBuffer = new byte[BUFFER_SIZE];
private int readOffset; private int readOffset;
private int readCount; private int readCount;

View File

@ -12,7 +12,6 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
*
* @author kapodamy * @author kapodamy
*/ */
public class Mp4FromDashWriter { public class Mp4FromDashWriter {
@ -262,12 +261,12 @@ public class Mp4FromDashWriter {
final int ftyp_size = make_ftyp(); final int ftyp_size = make_ftyp();
// reserve moov space in the output stream // reserve moov space in the output stream
if (outStream.canSetLength()) { /*if (outStream.canSetLength()) {
long length = writeOffset + auxSize; long length = writeOffset + auxSize;
outStream.setLength(length); outStream.setLength(length);
outSeek(length); outSeek(length);
} else { } else {*/
// hard way if (auxSize > 0) {
int length = auxSize; int length = auxSize;
byte[] buffer = new byte[8 * 1024];// 8 KiB byte[] buffer = new byte[8 * 1024];// 8 KiB
while (length > 0) { while (length > 0) {
@ -276,6 +275,7 @@ public class Mp4FromDashWriter {
length -= count; length -= count;
} }
} }
if (auxBuffer == null) { if (auxBuffer == null) {
outSeek(ftyp_size); outSeek(ftyp_size);
} }

View File

@ -10,6 +10,9 @@ import java.util.regex.Pattern;
public class FilenameUtils { public class FilenameUtils {
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
/** /**
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars. * #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
* @param context the context to retrieve strings and preferences from * @param context the context to retrieve strings and preferences from
@ -18,11 +21,28 @@ public class FilenameUtils {
*/ */
public static String createFilename(Context context, String title) { public static String createFilename(Context context, String title) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.settings_file_charset_key);
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value)); final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
Pattern pattern = Pattern.compile(value); final String charset_ms = context.getString(R.string.charset_most_special_value);
final String defaultCharset = context.getString(R.string.default_file_charset_value);
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_"); final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
final String charset;
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
if (selectedCharset.equals(charset_ld)) {
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
} else if (selectedCharset.equals(charset_ms)) {
charset = CHARSET_MOST_SPECIAL;
} else {
charset = selectedCharset;// ¿is the user using a custom charset?
}
Pattern pattern = Pattern.compile(charset);
return createFilename(title, pattern, replacementChar); return createFilename(title, pattern, replacementChar);
} }

View File

@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread {
@Override @Override
public void run() { public void run() {
if (mMission.current > 0) mMission.resetState(); if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING);
int retryCount = 0; int retryCount = 0;
while (true) { while (true) {

View File

@ -2,7 +2,6 @@ package us.shandian.giga.get;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import java.io.File; import java.io.File;
@ -86,7 +85,7 @@ public class DownloadMission extends Mission {
/** /**
* the post-processing algorithm instance * the post-processing algorithm instance
*/ */
public transient Postprocessing psAlgorithm; public Postprocessing psAlgorithm;
/** /**
* The current resource to download, see {@code urls[current]} and {@code offsets[current]} * The current resource to download, see {@code urls[current]} and {@code offsets[current]}
@ -483,7 +482,7 @@ public class DownloadMission extends Mission {
if (init != null && Thread.currentThread() != init && init.isAlive()) { if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt(); init.interrupt();
synchronized (blockState) { synchronized (blockState) {
resetState(); resetState(false, true, ERROR_NOTHING);
} }
return; return;
} }
@ -525,10 +524,18 @@ public class DownloadMission extends Mission {
return res; return res;
} }
void resetState() {
/**
* Resets the mission state
*
* @param rollback {@code true} true to forget all progress, otherwise, {@code false}
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
*/
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
done = 0; done = 0;
blocks = -1; blocks = -1;
errCode = ERROR_NOTHING; errCode = errorCode;
errObject = null;
fallback = false; fallback = false;
unknownLength = false; unknownLength = false;
finishCount = 0; finishCount = 0;
@ -537,7 +544,10 @@ public class DownloadMission extends Mission {
blockState.clear(); blockState.clear();
threads = new Thread[0]; threads = new Thread[0];
Utility.writeToFile(metadata, DownloadMission.this); if (rollback) current = 0;
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
} }
private void initializer() { private void initializer() {
@ -633,33 +643,22 @@ public class DownloadMission extends Mission {
threads[0].interrupt(); threads[0].interrupt();
} }
/**
* changes the StoredFileHelper for another and saves the changes to the metadata file
*
* @param newStorage the new StoredFileHelper instance to use
*/
public void changeStorage(@NonNull StoredFileHelper newStorage) {
storage = newStorage;
// commit changes on the metadata file
runAsync(-2, this::writeThisToFile);
}
/** /**
* Indicates whatever the backed storage is invalid * Indicates whatever the backed storage is invalid
* *
* @return {@code true}, if storage is invalid and cannot be used * @return {@code true}, if storage is invalid and cannot be used
*/ */
public boolean hasInvalidStorage() { public boolean hasInvalidStorage() {
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile();
} }
/** /**
* Indicates whatever is possible to start the mission * Indicates whatever is possible to start the mission
* *
* @return {@code true} is this mission is "sane", otherwise, {@code false} * @return {@code true} is this mission its "healthy", otherwise, {@code false}
*/ */
public boolean canDownload() { public boolean isCorrupt() {
return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage();
} }
private boolean doPostprocessing() { private boolean doPostprocessing() {

View File

@ -137,6 +137,10 @@ public class DownloadRunnable extends Thread {
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
} catch (Exception e) { } catch (Exception e) {
if (DEBUG) {
Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e);
}
mMission.setThreadBytePosition(mId, total); mMission.setThreadBytePosition(mId, total);
if (!mMission.running || e instanceof ClosedByInterruptException) break; if (!mMission.running || e instanceof ClosedByInterruptException) break;
@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread {
break; break;
} }
if (DEBUG) {
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
}
retry = true; retry = true;
} }
} }

View File

@ -12,5 +12,7 @@ public class FinishedMission extends Mission {
length = mission.length;// ¿or mission.done? length = mission.length;// ¿or mission.done?
timestamp = mission.timestamp; timestamp = mission.timestamp;
kind = mission.kind; kind = mission.kind;
storage = mission.storage;
} }
} }

View File

@ -1,6 +1,5 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.io.Serializable; import java.io.Serializable;
@ -36,15 +35,6 @@ public abstract class Mission implements Serializable {
*/ */
public StoredFileHelper storage; public StoredFileHelper storage;
/**
* get the target file on the storage
*
* @return File object
*/
public Uri getDownloadedFileUri() {
return storage.getUri();
}
/** /**
* Delete the downloaded file * Delete the downloaded file
* *
@ -52,7 +42,7 @@ public abstract class Mission implements Serializable {
*/ */
public boolean delete() { public boolean delete() {
if (storage != null) return storage.delete(); if (storage != null) return storage.delete();
return true; return true;
} }
/** /**
@ -65,6 +55,6 @@ public abstract class Mission implements Serializable {
public String toString() { public String toString() {
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp); calendar.setTimeInMillis(timestamp);
return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
} }
} }

View File

@ -35,7 +35,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
/** /**
* The table name of download missions * The table name of download missions
*/ */
private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; private static final String FINISHED_TABLE_NAME = "finished_missions";
/** /**
* The key to the urls of a mission * The key to the urls of a mission
@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
* The statement to create the table * The statement to create the table
*/ */
private static final String MISSIONS_CREATE_TABLE = private static final String MISSIONS_CREATE_TABLE =
"CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + "CREATE TABLE " + FINISHED_TABLE_NAME + " (" +
KEY_PATH + " TEXT NOT NULL, " + KEY_PATH + " TEXT NOT NULL, " +
KEY_SOURCE + " TEXT NOT NULL, " + KEY_SOURCE + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " + KEY_DONE + " INTEGER NOT NULL, " +
@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
) )
).toString()); ).toString());
db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); db.insert(FINISHED_TABLE_NAME, null, values);
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
mission.kind = kind.charAt(0); mission.kind = kind.charAt(0);
try { try {
mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
} catch (Exception e) { } catch (Exception e) {
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
mission.storage = new StoredFileHelper(path, "", ""); mission.storage = new StoredFileHelper(null, path, "", "");
} }
return mission; return mission;
@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
public ArrayList<FinishedMission> loadFinishedMissions() { public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = getReadableDatabase(); SQLiteDatabase database = getReadableDatabase();
Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null,
null, null, null, KEY_TIMESTAMP + " DESC"); null, null, null, KEY_TIMESTAMP + " DESC");
int count = cursor.getCount(); int count = cursor.getCount();
@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
if (downloadMission == null) throw new NullPointerException("downloadMission is null"); if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(downloadMission); ContentValues values = getValuesOfMission(downloadMission);
database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); database.insert(FINISHED_TABLE_NAME, null, values);
} }
public void deleteMission(Mission mission) { public void deleteMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null"); if (mission == null) throw new NullPointerException("mission is null");
String path = mission.getDownloadedFileUri().toString(); String ts = String.valueOf(mission.timestamp);
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
if (mission instanceof FinishedMission) if (mission instanceof FinishedMission) {
database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); if (mission.storage.isInvalid()) {
else database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
ts, mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission"); throw new UnsupportedOperationException("DownloadMission");
}
} }
public void updateMission(Mission mission) { public void updateMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null"); if (mission == null) throw new NullPointerException("mission is null");
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(mission); ContentValues values = getValuesOfMission(mission);
String path = mission.getDownloadedFileUri().toString(); String ts = String.valueOf(mission.timestamp);
int rowsAffected; int rowsAffected;
if (mission instanceof FinishedMission) if (mission instanceof FinishedMission) {
rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); if (mission.storage.isInvalid()) {
else rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
} else {
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
mission.storage.getUri().toString()
});
}
} else {
throw new UnsupportedOperationException("DownloadMission"); throw new UnsupportedOperationException("DownloadMission");
}
if (rowsAffected != 1) { if (rowsAffected != 1) {
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);

View File

@ -12,7 +12,7 @@ public class CircularFileWriter extends SharpStream {
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB
private OffsetChecker callback; private OffsetChecker callback;
@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream {
reportPosition = NOTIFY_BYTES_INTERVAL; reportPosition = NOTIFY_BYTES_INTERVAL;
} }
private void flushAuxiliar() throws IOException { private void flushAuxiliar(long amount) throws IOException {
if (aux.length < 1) { if (aux.length < 1) {
return; return;
} }
boolean underflow = out.getOffset() >= out.length;
out.flush(); out.flush();
aux.flush(); aux.flush();
boolean underflow = aux.offset < aux.length || out.offset < out.length;
aux.target.seek(0); aux.target.seek(0);
out.target.seek(out.length); out.target.seek(out.length);
long length = aux.length; long length = amount;
out.length += aux.length;
while (length > 0) { while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE); int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length));
if (read < 1) {
amount -= length;
break;
}
out.writeProof(aux.queue, read); out.writeProof(aux.queue, read);
length -= read; length -= read;
} }
if (underflow) { if (underflow) {
out.offset += aux.offset; if (out.offset >= out.length) {
out.target.seek(out.offset); // calculate the aux underflow pointer
if (aux.offset < amount) {
out.offset += aux.offset;
aux.offset = 0;
out.target.seek(out.offset);
} else {
aux.offset -= amount;
out.offset = out.length + amount;
}
} else {
aux.offset = 0;
}
} else { } else {
out.offset = out.length; out.offset += amount;
aux.offset -= amount;
} }
out.length += amount;
if (out.length > maxLengthKnown) { if (out.length > maxLengthKnown) {
maxLengthKnown = out.length; maxLengthKnown = out.length;
} }
if (amount < aux.length) {
// move the excess data to the beginning of the file
long readOffset = amount;
long writeOffset = 0;
byte[] buffer = new byte[128 * 1024]; // 128 KiB
aux.length -= amount;
length = aux.length;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
aux.target.seek(writeOffset);
aux.writeProof(buffer, read);
writeOffset += read;
readOffset += read;
length -= read;
aux.target.seek(readOffset);
}
aux.target.setLength(aux.length);
return;
}
if (aux.length > THRESHOLD_AUX_LENGTH) { if (aux.length > THRESHOLD_AUX_LENGTH) {
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0); aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
} }
@ -94,7 +137,7 @@ public class CircularFileWriter extends SharpStream {
* @throws IOException if an I/O error occurs * @throws IOException if an I/O error occurs
*/ */
public long finalizeFile() throws IOException { public long finalizeFile() throws IOException {
flushAuxiliar(); flushAuxiliar(aux.length);
out.flush(); out.flush();
@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream {
if (end == -1) { if (end == -1) {
available = Integer.MAX_VALUE; available = Integer.MAX_VALUE;
} else if (end < offsetOut) { } else if (end < offsetOut) {
throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut)); throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut);
} else { } else {
available = end - offsetOut; available = end - offsetOut;
} }
@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream {
length = aux.length + len; length = aux.length + len;
} }
if (length > available || length < THRESHOLD_AUX_LENGTH) { aux.write(b, off, len);
aux.write(b, off, len);
} else { if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
if (underflow) { flushAuxiliar(available);
aux.write(b, off, len);
flushAuxiliar();
} else {
flushAuxiliar();
out.write(b, off, len);// write directly on the output
}
} }
} else { } else {
if (underflow) { if (underflow) {
@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream {
@Override @Override
public void seek(long offset) throws IOException { public void seek(long offset) throws IOException {
long total = out.length + aux.length; long total = out.length + aux.length;
if (offset == total) { if (offset == total) {
return;// nothing to do // do not ignore the seek offset if a underflow exists
long relativeOffset = out.getOffset() + aux.getOffset();
if (relativeOffset == total) {
return;
}
} }
// flush everything, avoid any underflow // flush everything, avoid any underflow
@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream {
} }
protected void seek(long absoluteOffset) throws IOException { protected void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) {
return;// nothing to do
}
offset = absoluteOffset; offset = absoluteOffset;
target.seek(absoluteOffset); target.seek(absoluteOffset);
} }

View File

@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream {
public void seek(long offset) throws IOException { public void seek(long offset) throws IOException {
channel.position(offset); channel.position(offset);
} }
@Override
public long length() throws IOException {
return channel.size();
}
} }

View File

@ -4,8 +4,10 @@ import android.annotation.TargetApi;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
@ -13,8 +15,13 @@ import android.support.v4.provider.DocumentFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
public class StoredDirectoryHelper { public class StoredDirectoryHelper {
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
@ -22,14 +29,27 @@ public class StoredDirectoryHelper {
private File ioTree; private File ioTree;
private DocumentFile docTree; private DocumentFile docTree;
private ContentResolver contentResolver; private Context context;
private String tag; private String tag;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
this.contentResolver = context.getContentResolver();
this.tag = tag; this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
this.ioTree = new File(URI.create(path.toString()));
return;
}
this.context = context;
try {
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
} catch (Exception e) {
throw new IOException(e);
}
this.docTree = DocumentFile.fromTreeUri(context, path); this.docTree = DocumentFile.fromTreeUri(context, path);
if (this.docTree == null) if (this.docTree == null)
@ -37,23 +57,75 @@ public class StoredDirectoryHelper {
} }
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull String location, String tag) { public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location); ioTree = new File(location);
this.tag = tag; this.tag = tag;
} }
@Nullable
public StoredFileHelper createFile(String filename, String mime) { public StoredFileHelper createFile(String filename, String mime) {
return createFile(filename, mime, false);
}
public StoredFileHelper createUniqueFile(String name, String mime) {
ArrayList<String> matches = new ArrayList<>();
String[] filename = splitFilename(name);
String lcFilename = filename[0].toLowerCase();
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
for (File file : ioTree.listFiles())
addIfStartWith(matches, lcFilename, file.getName());
} else {
// warning: SAF file listing is very slow
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
);
String[] projection = new String[]{COLUMN_DISPLAY_NAME};
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
if (cursor != null) {
while (cursor.moveToNext())
addIfStartWith(matches, lcFilename, cursor.getString(0));
}
}
}
if (matches.size() < 1) {
return createFile(name, mime, true);
} else {
// check if the filename is in use
String lcName = name.toLowerCase();
for (String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
}
}
// check if not in use
if (lcName != null) return createFile(name, mime, true);
}
Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
}
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
}
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
StoredFileHelper storage; StoredFileHelper storage;
try { try {
if (docTree == null) { if (docTree == null)
storage = new StoredFileHelper(ioTree, filename, tag); storage = new StoredFileHelper(ioTree, filename, mime);
storage.sourceTree = Uri.fromFile(ioTree).toString(); else
} else { storage = new StoredFileHelper(context, docTree, filename, mime, safe);
storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag);
storage.sourceTree = docTree.getUri().toString();
}
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
@ -63,67 +135,6 @@ public class StoredDirectoryHelper {
return storage; return storage;
} }
public StoredFileHelper createUniqueFile(String filename, String mime) {
ArrayList<String> existingNames = new ArrayList<>(50);
String ext;
int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
ext = "";
} else {
ext = filename.substring(dotIndex);
filename = filename.substring(0, dotIndex - 1);
}
String name;
if (docTree == null) {
for (File file : ioTree.listFiles()) {
name = file.getName().toLowerCase();
if (name.startsWith(filename)) existingNames.add(name);
}
} else {
for (DocumentFile file : docTree.listFiles()) {
name = file.getName();
if (name == null) continue;
name = name.toLowerCase();
if (name.startsWith(filename)) existingNames.add(name);
}
}
boolean free = true;
String lwFilename = filename.toLowerCase();
for (String testName : existingNames) {
if (testName.equals(lwFilename)) {
free = false;
break;
}
}
if (free) return createFile(filename, mime);
String[] sortedNames = existingNames.toArray(new String[0]);
Arrays.sort(sortedNames);
String newName;
int downloadIndex = 0;
do {
newName = filename + " (" + downloadIndex + ")" + ext;
++downloadIndex;
if (downloadIndex == 1000) { // Probably an error on our side
newName = System.currentTimeMillis() + ext;
break;
}
} while (Arrays.binarySearch(sortedNames, newName) >= 0);
return createFile(newName, mime);
}
public boolean isDirect() {
return docTree == null;
}
public Uri getUri() { public Uri getUri() {
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
} }
@ -136,34 +147,18 @@ public class StoredDirectoryHelper {
return tag; return tag;
} }
public void acquirePermissions() throws IOException {
if (docTree == null) return;
try {
contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
} catch (Throwable e) {
throw new IOException(e);
}
}
public void revokePermissions() throws IOException {
if (docTree == null) return;
try {
contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
} catch (Throwable e) {
throw new IOException(e);
}
}
public Uri findFile(String filename) { public Uri findFile(String filename) {
if (docTree == null) if (docTree == null) {
return Uri.fromFile(new File(ioTree, filename)); File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null;
}
// findFile() method is very slow DocumentFile res = findFileSAFHelper(context, docTree, filename);
DocumentFile file = docTree.findFile(filename); return res == null ? null : res.getUri();
}
return file == null ? null : file.getUri(); public boolean canWrite() {
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
} }
@NonNull @NonNull
@ -172,4 +167,76 @@ public class StoredDirectoryHelper {
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
} }
////////////////////
// Utils
///////////////////
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
if (str == null || str.isEmpty()) return;
str = str.toLowerCase();
if (str.startsWith(base)) list.add(str);
}
private static String[] splitFilename(@NonNull String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
return new String[]{filename, ""};
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
}
private static String makeFileName(String name, int idx, String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
}
/**
* Fast (but not enough) file/directory finder under the storage access framework
*
* @param context The context
* @param tree Directory where search
* @param filename Target filename
* @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null
*/
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return tree.findFile(filename);// warning: this is very slow
}
if (!tree.canRead()) return null;// missing read permission
final int name = 0;
final int documentId = 1;
// LOWER() SQL function is not supported
String selection = COLUMN_DISPLAY_NAME + " = ?";
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
);
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
ContentResolver contentResolver = context.getContentResolver();
filename = filename.toLowerCase();
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
if (cursor == null) return null;
while (cursor.moveToNext()) {
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
continue;
return DocumentFile.fromSingleUri(
context, DocumentsContract.buildDocumentUriUsingTree(
tree.getUri(), cursor.getString(documentId)
)
);
}
}
return null;
}
} }

View File

@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.provider.DocumentFile; import android.support.v4.provider.DocumentFile;
@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable {
private transient DocumentFile docFile; private transient DocumentFile docFile;
private transient DocumentFile docTree; private transient DocumentFile docTree;
private transient File ioFile; private transient File ioFile;
private transient ContentResolver contentResolver; private transient Context context;
protected String source; protected String source;
String sourceTree; private String sourceTree;
protected String tag; protected String tag;
private String srcName; private String srcName;
private String srcType; private String srcType;
public StoredFileHelper(String filename, String mime, String tag) { public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename; this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime; this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) this.sourceTree = parent.toString();
this.tag = tag; this.tag = tag;
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
this.docTree = tree; this.docTree = tree;
this.contentResolver = contentResolver; this.context = context;
// this is very slow, because SAF does not allow overwrite DocumentFile res;
DocumentFile res = this.docTree.findFile(filename);
if (res != null && res.exists() && res.isDirectory()) { if (safe) {
if (!res.delete()) // no conflicts (the filename is not in use)
throw new IOException("Directory with the same name found but cannot delete"); res = this.docTree.createFile(mime, filename);
res = null;
}
if (res == null) {
res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename);
if (res == null) throw new IOException("Cannot create the file"); if (res == null) throw new IOException("Cannot create the file");
} else {
res = createSAF(context, mime, filename);
} }
this.docFile = res; this.docFile = res;
this.source = res.getUri().toString();
this.srcName = getName(); this.source = docFile.getUri().toString();
this.srcType = getType(); this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
} }
@TargetApi(Build.VERSION_CODES.KITKAT) StoredFileHelper(File location, String filename, String mime) throws IOException {
public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException {
this.source = path.toString();
this.tag = tag;
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) {
this.ioFile = new File(URI.create(this.source));
} else {
DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null)
throw new UnsupportedOperationException("Cannot get the file via SAF");
this.contentResolver = context.getContentResolver();
this.docFile = file;
try {
this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (Exception e) {
throw new IOException(e);
}
}
this.srcName = getName();
this.srcType = getType();
}
public StoredFileHelper(File location, String filename, String tag) throws IOException {
this.ioFile = new File(location, filename); this.ioFile = new File(location, filename);
this.tag = tag;
if (this.ioFile.exists()) { if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete()) if (!this.ioFile.isFile() && !this.ioFile.delete())
@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable {
} }
this.source = Uri.fromFile(this.ioFile).toString(); this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) throw new RuntimeException("SAF not available");
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
this.docTree = DocumentFile.fromTreeUri(context, parent);
this.sourceTree = parent.toString();
}
this.srcName = getName(); this.srcName = getName();
this.srcType = getType(); this.srcType = getType();
} }
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid()) if (storage.isInvalid())
return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
if (storage.sourceTree != null) { // under SAF, if the target document is deleted, conserve the filename and mime
instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); if (instance.srcName == null) instance.srcName = storage.srcName;
if (instance.srcType == null) instance.srcType = storage.srcType;
if (instance.docTree == null)
throw new IOException("Cannot deserialize the tree, ¿revoked permissions?");
}
return instance; return instance;
} }
@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable {
who.startActivityForResult(intent, requestCode); who.startActivityForResult(intent, requestCode);
} }
public SharpStream getStream() throws IOException { public SharpStream getStream() throws IOException {
invalid(); invalid();
if (docFile == null) if (docFile == null)
return new FileStream(ioFile); return new FileStream(ioFile);
else else
return new FileStreamSAF(contentResolver, docFile.getUri()); return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
} }
/** /**
@ -173,6 +184,12 @@ public class StoredFileHelper implements Serializable {
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
} }
public Uri getParentUri() {
invalid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException { public void truncate() throws IOException {
invalid(); invalid();
@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable {
} }
public boolean delete() { public boolean delete() {
invalid(); if (source == null) return true;
if (docFile == null) return ioFile.delete(); if (docFile == null) return ioFile.delete();
boolean res = docFile.delete(); boolean res = docFile.delete();
try { try {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (Exception ex) { } catch (Exception ex) {
// ¿what happen? // nothing to do
} }
return res; return res;
@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable {
return docFile == null ? ioFile.canWrite() : docFile.canWrite(); return docFile == null ? ioFile.canWrite() : docFile.canWrite();
} }
public File getIOFile() {
return ioFile;
}
public String getName() { public String getName() {
if (source == null) return srcName; if (source == null)
return docFile == null ? ioFile.getName() : docFile.getName(); return srcName;
else if (docFile == null)
return ioFile.getName();
String name = docFile.getName();
return name == null ? srcName : name;
} }
public String getType() { public String getType() {
if (source == null) return srcType; if (source == null || docFile == null)
return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO return srcType;
String type = docFile.getType();
return type == null ? srcType : type;
} }
public String getTag() { public String getTag() {
@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable {
if (source == null) return false; if (source == null) return false;
boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
return exists && asFile; return exists && isFile;
} }
public boolean create() { public boolean create() {
invalid(); invalid();
boolean result;
if (docFile == null) { if (docFile == null) {
try { try {
return ioFile.createNewFile(); result = ioFile.createNewFile();
} catch (IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) return false;
try {
docFile = createSAF(context, srcType, srcName);
if (docFile == null || docFile.getName() == null) return false;
result = true;
} catch (IOException e) { } catch (IOException e) {
return false; return false;
} }
} }
if (docTree == null || docFile.getName() == null) return false; if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); return result;
if (res == null) return false;
docFile = res;
return true;
} }
public void invalidate() { public void invalidate() {
@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable {
source = null; source = null;
sourceTree = null;
docTree = null; docTree = null;
docFile = null; docFile = null;
ioFile = null; ioFile = null;
contentResolver = null; context = null;
}
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
} }
public boolean equals(StoredFileHelper storage) { public boolean equals(StoredFileHelper storage) {
if (this.isInvalid() != storage.isInvalid()) return false; if (this == storage) return true;
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
return false;
if (this.isInvalid() || storage.isInvalid()) {
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) return false; if (this.isDirect() != storage.isDirect()) return false;
if (this.isDirect()) if (this.isDirect())
@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable {
else else
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag; return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
} }
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (Exception e) {
if (docFile.getName() == null) throw new IOException(e);
}
}
private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete())
throw new IOException("Directory with the same name found but cannot delete");
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) throw new IOException("Cannot create the file");
}
return res;
}
private String getLowerCase(String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(String str1, String str2) {
if (str1 == null && str2 == null) return false;
if ((str1 == null) != (str2 == null)) return true;
return !str1.equals(str2);
}
} }

View File

@ -11,7 +11,7 @@ import java.io.IOException;
class Mp4FromDashMuxer extends Postprocessing { class Mp4FromDashMuxer extends Postprocessing {
Mp4FromDashMuxer() { Mp4FromDashMuxer() {
super(2 * 1024 * 1024/* 2 MiB */, true); super(3 * 1024 * 1024/* 3 MiB */, true);
} }
@Override @Override

View File

@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.ChunkFileInputStream; import us.shandian.giga.io.ChunkFileInputStream;
@ -19,7 +20,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
public abstract class Postprocessing { public abstract class Postprocessing implements Serializable {
static transient final byte OK_RESULT = ERROR_NOTHING; static transient final byte OK_RESULT = ERROR_NOTHING;
@ -28,12 +29,10 @@ public abstract class Postprocessing {
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public static Postprocessing getAlgorithm(String algorithmName, String[] args) { public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) {
Postprocessing instance; Postprocessing instance;
if (null == algorithmName) { switch (algorithmName) {
throw new NullPointerException("algorithmName");
} else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER: case ALGORITHM_TTML_CONVERTER:
instance = new TtmlConverter(); instance = new TtmlConverter();
break; break;
@ -47,13 +46,14 @@ public abstract class Postprocessing {
instance = new M4aNoDash(); instance = new M4aNoDash();
break; break;
/*case "example-algorithm": /*case "example-algorithm":
instance = new ExampleAlgorithm(mission);*/ instance = new ExampleAlgorithm();*/
default: default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
} }
instance.args = args; instance.args = args;
instance.name = algorithmName; instance.name = algorithmName;// for debug only, maybe remove this field in the future
instance.cacheDir = cacheDir;
return instance; return instance;
} }
@ -125,7 +125,6 @@ public abstract class Postprocessing {
return -1; return -1;
}; };
// TODO: use Context.getCache() for this operation
temp = new File(cacheDir, mission.storage.getName() + ".tmp"); temp = new File(cacheDir, mission.storage.getName() + ".tmp");
out = new CircularFileWriter(mission.storage.getStream(), temp, checker); out = new CircularFileWriter(mission.storage.getStream(), temp, checker);

View File

@ -13,7 +13,7 @@ import java.io.IOException;
class WebMMuxer extends Postprocessing { class WebMMuxer extends Postprocessing {
WebMMuxer() { WebMMuxer() {
super(2048 * 1024/* 2 MiB */, true); super(5 * 1024 * 1024/* 5 MiB */, true);
} }
@Override @Override

View File

@ -62,13 +62,15 @@ public class DownloadManager {
* @param context Context for the data source for finished downloads * @param context Context for the data source for finished downloads
* @param handler Thread required for Messaging * @param handler Thread required for Messaging
*/ */
DownloadManager(@NonNull Context context, Handler handler) { DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
} }
mFinishedMissionStore = new FinishedMissionStore(context); mFinishedMissionStore = new FinishedMissionStore(context);
mHandler = handler; mHandler = handler;
mMainStorageAudio = storageAudio;
mMainStorageVideo = storageVideo;
mMissionsFinished = loadFinishedMissions(); mMissionsFinished = loadFinishedMissions();
mPendingMissionsDir = getPendingDir(context); mPendingMissionsDir = getPendingDir(context);
@ -129,91 +131,59 @@ public class DownloadManager {
} }
for (File sub : subs) { for (File sub : subs) {
if (sub.isFile()) { if (!sub.isFile()) continue;
DownloadMission mis = Utility.readFromFile(sub);
if (mis == null) { DownloadMission mis = Utility.readFromFile(sub);
//noinspection ResultOfMethodCallIgnored if (mis == null || mis.isFinished()) {
sub.delete(); //noinspection ResultOfMethodCallIgnored
} else { sub.delete();
if (mis.isFinished()) { continue;
//noinspection ResultOfMethodCallIgnored
sub.delete();
continue;
}
boolean exists;
try {
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
} catch (Exception ex) {
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString());
mis.storage.invalidate();
exists = false;
}
if (mis.isPsRunning()) {
if (mis.psAlgorithm.worksOnSameFile) {
// Incomplete post-processing results in a corrupted download file
// because the selected algorithm works on the same file to save space.
if (exists && !mis.storage.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
}
mis.psState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
mis.errObject = null;
} else if (!exists) {
StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag());
if (!mis.storage.isInvalid() && !mis.storage.create()) {
// using javaIO cannot recreate the file
// using SAF in older devices (no tree available)
//
// force the user to pick again the save path
mis.storage.invalidate();
} else if (mainStorage != null) {
// if the user has changed the save path before this download, the original save path will be lost
StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType());
if (newStorage == null)
mis.storage.invalidate();
else
mis.storage = newStorage;
}
if (mis.isInitialized()) {
// the progress is lost, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm);
m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount;
m.source = mis.source;
m.nearLength = mis.nearLength;
m.enqueued = mis.enqueued;
m.errCode = DownloadMission.ERROR_PROGRESS_LOST;
mis = m;
}
}
if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir();
mis.running = false;
mis.recovered = exists;
mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler;
mMissionsPending.add(mis);
}
} }
boolean exists;
try {
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
} catch (Exception ex) {
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex);
mis.storage.invalidate();
exists = false;
}
if (mis.isPsRunning()) {
if (mis.psAlgorithm.worksOnSameFile) {
// Incomplete post-processing results in a corrupted download file
// because the selected algorithm works on the same file to save space.
if (exists && !mis.storage.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
}
mis.psState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
mis.errObject = null;
} else if (!exists) {
tryRecover(mis);
// the progress is lost, reset mission state
if (mis.isInitialized())
mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST);
}
if (mis.psAlgorithm != null)
mis.psAlgorithm.cacheDir = pickAvailableCacheDir(ctx);
mis.recovered = exists;
mis.metadata = sub;
mis.maxRetry = mPrefMaxRetry;
mis.mHandler = mHandler;
mMissionsPending.add(mis);
} }
if (mMissionsPending.size() > 1) { if (mMissionsPending.size() > 1)
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp)); Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
}
} }
/** /**
@ -313,6 +283,25 @@ public class DownloadManager {
} }
} }
public void tryRecover(DownloadMission mission) {
StoredDirectoryHelper mainStorage = getMainStorage(mission.storage.getTag());
if (!mission.storage.isInvalid() && mission.storage.create()) return;
// using javaIO cannot recreate the file
// using SAF in older devices (no tree available)
//
// force the user to pick again the save path
mission.storage.invalidate();
if (mainStorage == null) return;
// if the user has changed the save path before this download, the original save path will be lost
StoredFileHelper newStorage = mainStorage.createFile(mission.storage.getName(), mission.storage.getType());
if (newStorage != null) mission.storage = newStorage;
}
/** /**
* Get a pending mission by its path * Get a pending mission by its path
@ -392,7 +381,7 @@ public class DownloadManager {
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (mission.running || !mission.canDownload()) continue; if (mission.running || mission.isCorrupt()) continue;
flag = true; flag = true;
mission.start(); mission.start();
@ -482,7 +471,7 @@ public class DownloadManager {
int paused = 0; int paused = 0;
synchronized (this) { synchronized (this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (!mission.canDownload() || mission.isPsRunning()) continue; if (mission.isCorrupt() || mission.isPsRunning()) continue;
if (mission.running && isMetered) { if (mission.running && isMetered) {
paused++; paused++;
@ -542,6 +531,20 @@ public class DownloadManager {
return MissionState.None; return MissionState.None;
} }
private static boolean isDirectoryAvailable(File directory) {
return directory != null && directory.canWrite();
}
static File pickAvailableCacheDir(@NonNull Context ctx) {
if (isDirectoryAvailable(ctx.getExternalCacheDir()))
return ctx.getExternalCacheDir();
else if (isDirectoryAvailable(ctx.getCacheDir()))
return ctx.getCacheDir();
// this never should happen
return ctx.getDir("tmp", Context.MODE_PRIVATE);
}
@Nullable @Nullable
private StoredDirectoryHelper getMainStorage(@NonNull String tag) { private StoredDirectoryHelper getMainStorage(@NonNull String tag) {
if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; if (tag.equals(TAG_AUDIO)) return mMainStorageAudio;
@ -656,7 +659,7 @@ public class DownloadManager {
synchronized (DownloadManager.this) { synchronized (DownloadManager.this) {
for (DownloadMission mission : mMissionsPending) { for (DownloadMission mission : mMissionsPending) {
if (hidden.contains(mission) || mission.canDownload()) if (hidden.contains(mission) || mission.isCorrupt())
continue; continue;
if (mission.running) if (mission.running)

View File

@ -6,6 +6,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
@ -40,6 +41,7 @@ import org.schabi.newpipe.player.helper.LockManager;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
@ -65,14 +67,15 @@ public class DownloadManagerService extends Service {
private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
private static final String EXTRA_PATH = "DownloadManagerService.extra.path";
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
@ -136,7 +139,9 @@ public class DownloadManagerService extends Service {
} }
}; };
mManager = new DownloadManager(this, mHandler); mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage());
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
.setAction(Intent.ACTION_MAIN); .setAction(Intent.ACTION_MAIN);
@ -182,7 +187,6 @@ public class DownloadManagerService extends Service {
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} }
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network));
@ -190,8 +194,6 @@ public class DownloadManagerService extends Service {
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
mLock = new LockManager(this); mLock = new LockManager(this);
setupStorageAPI(true);
} }
@Override @Override
@ -347,11 +349,12 @@ public class DownloadManagerService extends Service {
} else if (key.equals(getString(R.string.downloads_queue_limit))) { } else if (key.equals(getString(R.string.downloads_queue_limit))) {
mManager.mPrefQueueLimit = prefs.getBoolean(key, true); mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
} else if (key.equals(getString(R.string.downloads_storage_api))) { } else if (key.equals(getString(R.string.downloads_storage_api))) {
setupStorageAPI(false); mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
} else if (key.equals(getString(R.string.download_path_video_key))) { } else if (key.equals(getString(R.string.download_path_video_key))) {
loadMainStorage(key, DownloadManager.TAG_VIDEO, false); mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO);
} else if (key.equals(getString(R.string.download_path_audio_key))) { } else if (key.equals(getString(R.string.download_path_audio_key))) {
loadMainStorage(key, DownloadManager.TAG_AUDIO, false); mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO);
} }
} }
@ -387,36 +390,46 @@ public class DownloadManagerService extends Service {
Intent intent = new Intent(context, DownloadManagerService.class); Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN); intent.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls); intent.putExtra(EXTRA_URLS, urls);
intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_KIND, kind);
intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_THREADS, threads);
intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_SOURCE, source);
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag());
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
context.startService(intent); context.startService(intent);
} }
public void startMission(Intent intent) { private void startMission(Intent intent) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS); String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
Uri path = intent.getParcelableExtra(EXTRA_PATH); Uri path = intent.getParcelableExtra(EXTRA_PATH);
Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
int threads = intent.getIntExtra(EXTRA_THREADS, 1); int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?'); char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
String source = intent.getStringExtra(EXTRA_SOURCE); String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
StoredFileHelper storage; StoredFileHelper storage;
try { try {
storage = new StoredFileHelper(this, path, tag); storage = new StoredFileHelper(this, parentPath, path, tag);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e);// this never should happen throw new RuntimeException(e);// this never should happen
} }
final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); Postprocessing ps;
if (psName == null)
ps = null;
else
ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this));
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads; mission.threadCount = threads;
mission.source = source; mission.source = source;
mission.nearLength = nearLength; mission.nearLength = nearLength;
@ -525,60 +538,63 @@ public class DownloadManagerService extends Service {
mLockAcquired = acquire; mLockAcquired = acquire;
} }
private void setupStorageAPI(boolean acquire) { private StoredDirectoryHelper getVideoStorage() {
loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire);
} }
void loadMainStorage(String prefKey, String tag, boolean acquire) { private StoredDirectoryHelper getAudioStorage() {
return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
}
private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) {
String path = mPrefs.getString(prefKey, null); String path = mPrefs.getString(prefKey, null);
final String JAVA_IO = getString(R.string.downloads_storage_api_default); final String JAVA_IO = getString(R.string.downloads_storage_api_default);
boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO));
final String defaultPath; final String defaultPath;
if (tag.equals(DownloadManager.TAG_VIDEO)) switch (tag) {
defaultPath = Environment.DIRECTORY_MOVIES; case DownloadManager.TAG_VIDEO:
else// if (tag.equals(DownloadManager.TAG_AUDIO)) defaultPath = Environment.DIRECTORY_MOVIES;
defaultPath = Environment.DIRECTORY_MUSIC; break;
case DownloadManager.TAG_AUDIO:
StoredDirectoryHelper mainStorage; defaultPath = Environment.DIRECTORY_MUSIC;
if (path == null || path.isEmpty()) { break;
mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; default:
} else { return null;
if (path.charAt(0) == File.separatorChar) {
Log.i(TAG, "Migrating old save path: " + path);
useJavaIO = true;
path = Uri.fromFile(new File(path)).toString();
mPrefs.edit().putString(prefKey, path).apply();
}
if (useJavaIO) {
mainStorage = new StoredDirectoryHelper(path, tag);
} else {
// tree api is not available in older versions
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mainStorage = null;
} else {
try {
mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag);
if (acquire) mainStorage.acquirePermissions();
} catch (IOException e) {
Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e);
mainStorage = null;
}
}
}
} }
if (tag.equals(DownloadManager.TAG_VIDEO)) if (path == null || path.isEmpty()) {
mManager.mMainStorageVideo = mainStorage; return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null;
else// if (tag.equals(DownloadManager.TAG_AUDIO)) }
mManager.mMainStorageAudio = mainStorage;
if (path.charAt(0) == File.separatorChar) {
Log.i(TAG, "Migrating old save path: " + path);
useJavaIO = true;
path = Uri.fromFile(new File(path)).toString();
mPrefs.edit().putString(prefKey, path).apply();
}
boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
if (useJavaIO || override) {
return new StoredDirectoryHelper(URI.create(path), tag);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return null;// SAF Directory API is not available in older versions
}
try {
return new StoredDirectoryHelper(this, Uri.parse(path), tag);
} catch (Exception e) {
Log.e(TAG, "Failed to load the storage of " + tag + " from " + path, e);
Toast.makeText(this, R.string.no_available_dir, Toast.LENGTH_LONG).show();
}
return null;
} }
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -41,7 +41,9 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -346,7 +348,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
uri = FileProvider.getUriForFile( uri = FileProvider.getUriForFile(
mContext, mContext,
BuildConfig.APPLICATION_ID + ".provider", BuildConfig.APPLICATION_ID + ".provider",
mission.storage.getIOFile() new File(URI.create(mission.storage.getUri().toString()))
); );
} else { } else {
uri = mission.storage.getUri(); uri = mission.storage.getUri();
@ -384,10 +386,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
} }
private static String resolveMimeType(@NonNull Mission mission) { private static String resolveMimeType(@NonNull Mission mission) {
String mimeType;
if (!mission.storage.isInvalid()) {
mimeType = mission.storage.getType();
if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME))
return mimeType;
}
String ext = Utility.getFileExt(mission.storage.getName()); String ext = Utility.getFileExt(mission.storage.getName());
if (ext == null) return DEFAULT_MIME_TYPE; if (ext == null) return DEFAULT_MIME_TYPE;
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
} }
@ -476,6 +486,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return; return;
case ERROR_PROGRESS_LOST: case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost; msg = R.string.error_progress_lost;
break;
default: default:
if (mission.errCode >= 100 && mission.errCode < 600) { if (mission.errCode >= 100 && mission.errCode < 600) {
msgEx = "HTTP " + mission.errCode; msgEx = "HTTP " + mission.errCode;
@ -554,7 +565,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return true; return true;
case R.id.retry: case R.id.retry:
if (mission.hasInvalidStorage()) { if (mission.hasInvalidStorage()) {
mRecover.tryRecover(mission); mDownloadManager.tryRecover(mission);
if (mission.storage.isInvalid())
mRecover.tryRecover(mission);
else
recoverMission(mission);
return true; return true;
} }
mission.psContinue(true); mission.psContinue(true);
@ -672,13 +688,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (mDeleter != null) mDeleter.resume(); if (mDeleter != null) mDeleter.resume();
} }
public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { public void recoverMission(DownloadMission mission) {
for (ViewHolderItem h : mPendingDownloadsItems) { for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue; if (mission != h.item.mission) continue;
mission.changeStorage(newStorage);
mission.errCode = DownloadMission.ERROR_NOTHING;
mission.errObject = null; mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS); h.status.setText(UNDEFINED_PROGRESS);
h.state = -1; h.state = -1;
@ -822,9 +837,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (mission != null) { if (mission != null) {
if (mission.hasInvalidStorage()) { if (mission.hasInvalidStorage()) {
retry.setEnabled(true); retry.setVisible(true);
delete.setEnabled(true); delete.setVisible(true);
showError.setEnabled(true); showError.setVisible(true);
} else if (mission.isPsRunning()) { } else if (mission.isPsRunning()) {
switch (mission.errCode) { switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE: case ERROR_INSUFFICIENT_STORAGE:

View File

@ -9,6 +9,7 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@ -66,14 +67,7 @@ public class MissionsFragment extends Fragment {
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(getView()); mAdapter.deleterLoad(getView());
mAdapter.setRecover(mission -> mAdapter.setRecover(MissionsFragment.this::recoverMission);
StoredFileHelper.requestSafWithFileCreation(
MissionsFragment.this,
REQUEST_DOWNLOAD_PATH_SAF,
mission.storage.getName(),
mission.storage.getType()
)
);
setAdapterButtons(); setAdapterButtons();
@ -92,7 +86,7 @@ public class MissionsFragment extends Fragment {
}; };
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false); View v = inflater.inflate(R.layout.missions, container, false);
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
@ -239,8 +233,18 @@ public class MissionsFragment extends Fragment {
mAdapter.setMasterButtons(mStart, mPause); mAdapter.setMasterButtons(mStart, mPause);
} }
private void recoverMission(@NonNull DownloadMission mission) {
unsafeMissionTarget = mission;
StoredFileHelper.requestSafWithFileCreation(
MissionsFragment.this,
REQUEST_DOWNLOAD_PATH_SAF,
mission.storage.getName(),
mission.storage.getType()
);
}
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
if (mAdapter != null) { if (mAdapter != null) {
@ -285,8 +289,9 @@ public class MissionsFragment extends Fragment {
} }
try { try {
StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); String tag = unsafeMissionTarget.storage.getTag();
mAdapter.recoverMission(unsafeMissionTarget, storage); unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag);
mAdapter.recoverMission(unsafeMissionTarget);
} catch (IOException e) { } catch (IOException e) {
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
} }

View File

@ -9,6 +9,7 @@ import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -81,6 +82,7 @@ public class Utility {
objectInputStream = new ObjectInputStream(new FileInputStream(file)); objectInputStream = new ObjectInputStream(new FileInputStream(file));
object = (T) objectInputStream.readObject(); object = (T) objectInputStream.readObject();
} catch (Exception e) { } catch (Exception e) {
Log.e("Utility", "Failed to deserialize the object", e);
object = null; object = null;
} }

View File

@ -442,8 +442,8 @@ abrir en modo popup</string>
<!-- message dialog about download error --> <!-- message dialog about download error -->
<string name="show_error">Mostrar error</string> <string name="show_error">Mostrar error</string>
<string name="label_code">Codigo</string> <string name="label_code">Codigo</string>
<string name="error_file_creation">No se puede crear la carpeta de destino</string> <string name="error_file_creation">No se puede crear el archivo</string>
<string name="error_path_creation">No se puede crear el archivo</string> <string name="error_path_creation">No se puede crear la carpeta de destino</string>
<string name="error_permission_denied">Permiso denegado por el sistema</string> <string name="error_permission_denied">Permiso denegado por el sistema</string>
<string name="error_ssl_exception">Fallo la conexión segura</string> <string name="error_ssl_exception">Fallo la conexión segura</string>
<string name="error_unknown_host">No se pudo encontrar el servidor</string> <string name="error_unknown_host">No se pudo encontrar el servidor</string>

View File

@ -176,13 +176,17 @@
</string-array> </string-array>
<!-- FileName Downloads --> <!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename</string> <string name="settings_file_charset_key" translatable="false">file_rename_charset</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
<string name="settings_file_replacement_character_default_value" translatable="false">_</string> <string name="settings_file_replacement_character_default_value" translatable="false">_</string>
<string name="charset_letters_and_digits_value" translatable="false">CHARSET_LETTERS_AND_DIGITS</string>
<string name="charset_most_special_value" translatable="false">CHARSET_MOST_SPECIAL</string>
<string-array name="settings_filename_charset" translatable="false"> <string-array name="settings_filename_charset" translatable="false">
<item>@string/charset_letters_and_digits_value</item> <item>@string/charset_letters_and_digits_value</item>
<item>@string/charset_most_special_characters_value</item> <item>@string/charset_most_special_value</item>
</string-array> </string-array>
<string-array name="settings_filename_charset_name" translatable="false"> <string-array name="settings_filename_charset_name" translatable="false">
@ -190,8 +194,8 @@
<item>@string/charset_most_special_characters</item> <item>@string/charset_most_special_characters</item>
</string-array> </string-array>
<string name="default_file_charset_value" translatable="false">@string/charset_most_special_characters_value</string> <string name="default_file_charset_value" translatable="false">@string/charset_most_special_value</string>
<string name="downloads_maximum_retry" translatable="false">downloads_max_retry</string> <string name="downloads_maximum_retry" translatable="false">downloads_max_retry</string>
<string name="downloads_maximum_retry_default" translatable="false">3</string> <string name="downloads_maximum_retry_default" translatable="false">3</string>
<string-array name="downloads_maximum_retry_list" translatable="false"> <string-array name="downloads_maximum_retry_list" translatable="false">

View File

@ -305,8 +305,7 @@
<string name="settings_file_charset_title">Allowed characters in filenames</string> <string name="settings_file_charset_title">Allowed characters in filenames</string>
<string name="settings_file_replacement_character_summary">Invalid characters are replaced with this value</string> <string name="settings_file_replacement_character_summary">Invalid characters are replaced with this value</string>
<string name="settings_file_replacement_character_title">Replacement character</string> <string name="settings_file_replacement_character_title">Replacement character</string>
<string name="charset_letters_and_digits_value" translatable="false">[^\\w\\d]+</string>
<string name="charset_most_special_characters_value" translatable="false">[\\n\\r|\\?*&lt;":&gt;/']+</string>
<string name="charset_letters_and_digits">Letters and digits</string> <string name="charset_letters_and_digits">Letters and digits</string>
<string name="charset_most_special_characters">Most special characters</string> <string name="charset_most_special_characters">Most special characters</string>
<string name="toast_no_player">No app installed to play this file</string> <string name="toast_no_player">No app installed to play this file</string>

View File

@ -5,12 +5,6 @@
android:title="@string/settings_category_downloads_title"> android:title="@string/settings_category_downloads_title">
<Preference
app:iconSpaceReserved="false"
android:key="saf_test"
android:summary="Realiza una prueba del Storage Access Framework de Android"
android:title="Probar SAF"/>
<ListPreference <ListPreference
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:defaultValue="@string/downloads_storage_api_default" android:defaultValue="@string/downloads_storage_api_default"

Binary file not shown.