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.StringRes;
import android.support.v4.app.DialogFragment;
import android.support.v4.provider.DocumentFile;
import android.support.v7.app.AlertDialog;
import android.support.v7.view.menu.ActionMenuItemView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@ -177,9 +178,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return;
}
final Context context = getContext();
if (context == null)
throw new RuntimeException("Context was null");
context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
@ -321,11 +320,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
showFailedDialog(R.string.general_error);
return;
}
try {
continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), ""));
} catch (IOException e) {
showErrorActivity(e);
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
if (docFile == null) {
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 + "]");
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.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url);
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 -> {
if (item.getItemId() == R.id.okay) {
@ -504,15 +507,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
StoredDirectoryHelper mainStorageAudio = null;
StoredDirectoryHelper mainStorageVideo = null;
DownloadManager downloadManager = null;
MenuItem okButton = null;
ActionMenuItemView okButton = null;
Context context;
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) {
new AlertDialog.Builder(getContext())
new AlertDialog.Builder(context)
.setMessage(msg)
.setNegativeButton(android.R.string.ok, null)
.create()
@ -521,7 +526,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private void showErrorActivity(Exception e) {
ErrorActivity.reportError(
getContext(),
context,
Collections.singletonList(e),
null,
null,
@ -530,18 +535,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private void prepareSelectedDownload() {
final Context context = getContext();
StoredDirectoryHelper mainStorage;
MediaFormat format;
String mime;
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
String filename = getNameEditText() + ".";
if (filename.isEmpty()) {
filename = FilenameUtils.createFilename(context, currentInfo.getName());
}
filename += ".";
String filename = getNameEditText().concat(".");
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
@ -567,34 +568,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
if (mainStorage == null) {
// this part is called if...
// older android version running with SAF preferred
// save path not defined (via download settings)
// This part is called if with SAF preferred:
// * older android version running
// * save path not defined (via download settings)
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
return;
}
// check for existing file with the same name
Uri result = mainStorage.findFile(filename);
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
}
if (result == null) {
// 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
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
StoredFileHelper storage;
try {
storage = new StoredFileHelper(context, result, mime);
} catch (IOException e) {
if (mainStorage == null) {
// 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);
return;
}
@ -618,6 +618,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
msgBody = R.string.download_already_running;
break;
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;
msgBody = R.string.overwrite_unrelated_warning;
break;
@ -625,49 +644,73 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
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;
switch (state) {
case Finished:
case Pending:
downloadManager.forgetMission(storage);
case None:
// try take (or steal) the file permissions
try {
storageNew = new StoredFileHelper(context, result, mainStorage.getTag());
if (storageNew.canWrite())
continueSelectedDownload(storageNew);
else
showFailedDialog(R.string.error_file_creation);
} catch (IOException e) {
showErrorActivity(e);
}
break;
case PendingRunning:
// FIXME: createUniqueFile() is not tested properly
storageNew = mainStorage.createUniqueFile(finalFilename, mime);
if (storageNew == null)
showFailedDialog(R.string.error_file_creation);
else
continueSelectedDownload(storageNew);
break;
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setNegativeButton(android.R.string.cancel, null);
final StoredFileHelper finalStorage = storage;
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
switch (state) {
case Pending:
case Finished:
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
downloadManager.forgetMission(finalStorage);
continueSelectedDownload(finalStorage);
});
break;
}
askDialog.create().show();
return;
}
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)
.create()
.show();
if (storageNew != null && storageNew.canWrite())
continueSelectedDownload(storageNew);
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) {
final Context context = getContext();
if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied);
return;
@ -678,7 +721,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (storage.length() > 0) storage.truncate();
} catch (IOException e) {
Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e);
//showErrorActivity(e);
showFailedDialog(R.string.overwrite_failed);
return;
}
@ -748,7 +790,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (secondaryStreamUrl == null) {
urls = new String[]{selectedStream.getUrl()};
} 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);

View File

@ -14,18 +14,23 @@ import android.support.v7.preference.Preference;
import android.util.Log;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
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;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
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_AUDIO_PREFERENCE;
@ -35,41 +40,46 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
private Preference prefPathVideo;
private Preference prefPathAudio;
private Context ctx;
private boolean lastAPIJavaIO;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initKeys();
updatePreferencesSummary();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.download_settings);
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
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);
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
updatePathPickers(usingJavaIO());
lastAPIJavaIO = usingJavaIO();
updatePreferencesSummary();
updatePathPickers(lastAPIJavaIO);
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, 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();
// 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();
}
@ -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
public void onAttach(Context context) {
super.onAttach(context);
@ -91,20 +125,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null);
}
private void initKeys() {
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
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);
private void updatePreferencesSummary() {
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
}
private void updatePreferencesSummary() {
prefPathVideo.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary))
);
prefPathAudio.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))
);
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
String rawUri = defaultPreferences.getString(prefKey, null);
if (rawUri == null || rawUri.isEmpty()) {
target.setSummary(getString(defaultString));
return;
}
try {
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// nothing to do
}
target.setSummary(rawUri);
}
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)
private void forgetSAFTree(String prefKey) {
private void forgetSAFTree(Context ctx, String prefKey) {
if (IGNORE_RELEASE_OLD_PATH) {
return;
}
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 {
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null);
if (!mainStorage.isDirect()) {
mainStorage.revokePermissions();
Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!");
}
} catch (IOException err) {
Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err);
Uri uri = Uri.parse(oldPath);
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
} 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) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.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 {
i = new Intent(getActivity(), FilePickerActivityHelper.class)
.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) {
// steps:
// 1. acquire permissions on the new save path
// 2. save the new path, if step(1) was successful
// 1. revoke permissions on the old save path
// 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 {
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
mainStorage.acquirePermissions();
Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!");
Log.i(TAG, "Acquiring tree success from " + uri.toString());
if (!mainStorage.canWrite())
throw new IOException("No write permissions on " + uri.toString());
} 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);
return;
}
defaultPreferences.edit().putString(key, uri.toString()).apply();
} else {
defaultPreferences.edit().putString(key, uri.toString()).apply();
updatePreferencesSummary();
File target = new File(URI.create(uri.toString()));
if (!target.canWrite())
File target = Utils.getFileForUri(data.getData());
if (!target.canWrite()) {
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 FLOAT_SIZE = 4;
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
private long position = 0;
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 readCount;

View File

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

View File

@ -10,6 +10,9 @@ import java.util.regex.Pattern;
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.
* @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) {
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));
Pattern pattern = Pattern.compile(value);
final String charset_ld = context.getString(R.string.charset_letters_and_digits_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), "_");
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);
}

View File

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

View File

@ -2,7 +2,6 @@ package us.shandian.giga.get;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
@ -86,7 +85,7 @@ public class DownloadMission extends Mission {
/**
* 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]}
@ -483,7 +482,7 @@ public class DownloadMission extends Mission {
if (init != null && Thread.currentThread() != init && init.isAlive()) {
init.interrupt();
synchronized (blockState) {
resetState();
resetState(false, true, ERROR_NOTHING);
}
return;
}
@ -525,10 +524,18 @@ public class DownloadMission extends Mission {
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;
blocks = -1;
errCode = ERROR_NOTHING;
errCode = errorCode;
errObject = null;
fallback = false;
unknownLength = false;
finishCount = 0;
@ -537,7 +544,10 @@ public class DownloadMission extends Mission {
blockState.clear();
threads = new Thread[0];
Utility.writeToFile(metadata, DownloadMission.this);
if (rollback) current = 0;
if (persistChanges)
Utility.writeToFile(metadata, DownloadMission.this);
}
private void initializer() {
@ -633,33 +643,22 @@ public class DownloadMission extends Mission {
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
*
* @return {@code true}, if storage is invalid and cannot be used
*/
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
*
* @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() {
return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage();
public boolean isCorrupt() {
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage();
}
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
} catch (Exception e) {
if (DEBUG) {
Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e);
}
mMission.setThreadBytePosition(mId, total);
if (!mMission.running || e instanceof ClosedByInterruptException) break;
@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread {
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
}
retry = true;
}
}

View File

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

View File

@ -1,6 +1,5 @@
package us.shandian.giga.get;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.io.Serializable;
@ -36,15 +35,6 @@ public abstract class Mission implements Serializable {
*/
public StoredFileHelper storage;
/**
* get the target file on the storage
*
* @return File object
*/
public Uri getDownloadedFileUri() {
return storage.getUri();
}
/**
* Delete the downloaded file
*
@ -52,7 +42,7 @@ public abstract class Mission implements Serializable {
*/
public boolean delete() {
if (storage != null) return storage.delete();
return true;
return true;
}
/**
@ -65,6 +55,6 @@ public abstract class Mission implements Serializable {
public String toString() {
Calendar calendar = Calendar.getInstance();
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
*/
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
@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
* The statement to create the 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_SOURCE + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
)
).toString());
db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
db.insert(FINISHED_TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
db.endTransaction();
@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
mission.kind = kind.charAt(0);
try {
mission.storage = new StoredFileHelper(context, Uri.parse(path), "");
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
} catch (Exception 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;
@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
public ArrayList<FinishedMission> loadFinishedMissions() {
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");
int count = cursor.getCount();
@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(downloadMission);
database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
database.insert(FINISHED_TABLE_NAME, null, values);
}
public void deleteMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
String path = mission.getDownloadedFileUri().toString();
String ts = String.valueOf(mission.timestamp);
SQLiteDatabase database = getWritableDatabase();
if (mission instanceof FinishedMission)
database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path});
else
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
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");
}
}
public void updateMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null");
SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(mission);
String path = mission.getDownloadedFileUri().toString();
String ts = String.valueOf(mission.timestamp);
int rowsAffected;
if (mission instanceof FinishedMission)
rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path});
else
if (mission instanceof FinishedMission) {
if (mission.storage.isInvalid()) {
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");
}
if (rowsAffected != 1) {
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 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;
@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream {
reportPosition = NOTIFY_BYTES_INTERVAL;
}
private void flushAuxiliar() throws IOException {
private void flushAuxiliar(long amount) throws IOException {
if (aux.length < 1) {
return;
}
boolean underflow = out.getOffset() >= out.length;
out.flush();
aux.flush();
boolean underflow = aux.offset < aux.length || out.offset < out.length;
aux.target.seek(0);
out.target.seek(out.length);
long length = aux.length;
out.length += aux.length;
long length = amount;
while (length > 0) {
int read = (int) Math.min(length, Integer.MAX_VALUE);
read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length));
if (read < 1) {
amount -= length;
break;
}
out.writeProof(aux.queue, read);
length -= read;
}
if (underflow) {
out.offset += aux.offset;
out.target.seek(out.offset);
if (out.offset >= out.length) {
// 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 {
out.offset = out.length;
out.offset += amount;
aux.offset -= amount;
}
out.length += amount;
if (out.length > maxLengthKnown) {
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) {
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
*/
public long finalizeFile() throws IOException {
flushAuxiliar();
flushAuxiliar(aux.length);
out.flush();
@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream {
if (end == -1) {
available = Integer.MAX_VALUE;
} 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 {
available = end - offsetOut;
}
@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream {
length = aux.length + len;
}
if (length > available || length < THRESHOLD_AUX_LENGTH) {
aux.write(b, off, len);
} else {
if (underflow) {
aux.write(b, off, len);
flushAuxiliar();
} else {
flushAuxiliar();
out.write(b, off, len);// write directly on the output
}
aux.write(b, off, len);
if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
flushAuxiliar(available);
}
} else {
if (underflow) {
@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream {
@Override
public void seek(long offset) throws IOException {
long total = out.length + aux.length;
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
@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream {
}
protected void seek(long absoluteOffset) throws IOException {
if (absoluteOffset == offset) {
return;// nothing to do
}
offset = absoluteOffset;
target.seek(absoluteOffset);
}

View File

@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream {
public void seek(long offset) throws IOException {
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.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
@ -13,8 +15,13 @@ import android.support.v4.provider.DocumentFile;
import java.io.File;
import java.io.IOException;
import java.net.URI;
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 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 DocumentFile docTree;
private ContentResolver contentResolver;
private Context context;
private String tag;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
this.contentResolver = context.getContentResolver();
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);
if (this.docTree == null)
@ -37,23 +57,75 @@ public class StoredDirectoryHelper {
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull String location, String tag) {
public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location);
this.tag = tag;
}
@Nullable
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;
try {
if (docTree == null) {
storage = new StoredFileHelper(ioTree, filename, tag);
storage.sourceTree = Uri.fromFile(ioTree).toString();
} else {
storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag);
storage.sourceTree = docTree.getUri().toString();
}
if (docTree == null)
storage = new StoredFileHelper(ioTree, filename, mime);
else
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
} catch (IOException e) {
return null;
}
@ -63,67 +135,6 @@ public class StoredDirectoryHelper {
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() {
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
}
@ -136,34 +147,18 @@ public class StoredDirectoryHelper {
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) {
if (docTree == null)
return Uri.fromFile(new File(ioTree, filename));
if (docTree == null) {
File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null;
}
// findFile() method is very slow
DocumentFile file = docTree.findFile(filename);
DocumentFile res = findFileSAFHelper(context, docTree, filename);
return res == null ? null : res.getUri();
}
return file == null ? null : file.getUri();
public boolean canWrite() {
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
}
@NonNull
@ -172,4 +167,76 @@ public class StoredDirectoryHelper {
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.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.provider.DocumentFile;
@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable {
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient ContentResolver contentResolver;
private transient Context context;
protected String source;
String sourceTree;
private String sourceTree;
protected String tag;
private String srcName;
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.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) this.sourceTree = parent.toString();
this.tag = tag;
}
@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.contentResolver = contentResolver;
this.context = context;
// this is very slow, because SAF does not allow overwrite
DocumentFile res = this.docTree.findFile(filename);
DocumentFile res;
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(mime == null ? DEFAULT_MIME : mime, filename);
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) throw new IOException("Cannot create the file");
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = res.getUri().toString();
this.srcName = getName();
this.srcType = getType();
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
@TargetApi(Build.VERSION_CODES.KITKAT)
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 {
StoredFileHelper(File location, String filename, String mime) throws IOException {
this.ioFile = new File(location, filename);
this.tag = tag;
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete())
@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable {
}
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.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
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) {
instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree));
if (instance.docTree == null)
throw new IOException("Cannot deserialize the tree, ¿revoked permissions?");
}
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) instance.srcName = storage.srcName;
if (instance.srcType == null) instance.srcType = storage.srcType;
return instance;
}
@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable {
who.startActivityForResult(intent, requestCode);
}
public SharpStream getStream() throws IOException {
invalid();
if (docFile == null)
return new FileStream(ioFile);
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();
}
public Uri getParentUri() {
invalid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
invalid();
@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable {
}
public boolean delete() {
invalid();
if (source == null) return true;
if (docFile == null) return ioFile.delete();
boolean res = docFile.delete();
try {
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) {
// ¿what happen?
// nothing to do
}
return res;
@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable {
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public File getIOFile() {
return ioFile;
}
public String getName() {
if (source == null) return srcName;
return docFile == null ? ioFile.getName() : docFile.getName();
if (source == null)
return srcName;
else if (docFile == null)
return ioFile.getName();
String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null) return srcType;
return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO
if (source == null || docFile == null)
return srcType;
String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable {
if (source == null) return false;
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() {
invalid();
boolean result;
if (docFile == null) {
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) {
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());
if (res == null) return false;
docFile = res;
return true;
return result;
}
public void invalidate() {
@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable {
source = null;
sourceTree = null;
docTree = null;
docFile = null;
ioFile = null;
contentResolver = null;
}
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
context = null;
}
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())
@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable {
else
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 {
Mp4FromDashMuxer() {
super(2 * 1024 * 1024/* 2 MiB */, true);
super(3 * 1024 * 1024/* 3 MiB */, true);
}
@Override

View File

@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import us.shandian.giga.get.DownloadMission;
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_UNKNOWN_EXCEPTION;
public abstract class Postprocessing {
public abstract class Postprocessing implements Serializable {
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_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;
if (null == algorithmName) {
throw new NullPointerException("algorithmName");
} else switch (algorithmName) {
switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
instance = new TtmlConverter();
break;
@ -47,13 +46,14 @@ public abstract class Postprocessing {
instance = new M4aNoDash();
break;
/*case "example-algorithm":
instance = new ExampleAlgorithm(mission);*/
instance = new ExampleAlgorithm();*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
}
instance.args = args;
instance.name = algorithmName;
instance.name = algorithmName;// for debug only, maybe remove this field in the future
instance.cacheDir = cacheDir;
return instance;
}
@ -125,7 +125,6 @@ public abstract class Postprocessing {
return -1;
};
// TODO: use Context.getCache() for this operation
temp = new File(cacheDir, mission.storage.getName() + ".tmp");
out = new CircularFileWriter(mission.storage.getStream(), temp, checker);

View File

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

View File

@ -62,13 +62,15 @@ public class DownloadManager {
* @param context Context for the data source for finished downloads
* @param handler Thread required for Messaging
*/
DownloadManager(@NonNull Context context, Handler handler) {
DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) {
if (DEBUG) {
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
}
mFinishedMissionStore = new FinishedMissionStore(context);
mHandler = handler;
mMainStorageAudio = storageAudio;
mMainStorageVideo = storageVideo;
mMissionsFinished = loadFinishedMissions();
mPendingMissionsDir = getPendingDir(context);
@ -129,91 +131,59 @@ public class DownloadManager {
}
for (File sub : subs) {
if (sub.isFile()) {
DownloadMission mis = Utility.readFromFile(sub);
if (!sub.isFile()) continue;
if (mis == null) {
//noinspection ResultOfMethodCallIgnored
sub.delete();
} else {
if (mis.isFinished()) {
//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);
}
DownloadMission mis = Utility.readFromFile(sub);
if (mis == null || mis.isFinished()) {
//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(), 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));
}
}
/**
@ -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
@ -392,7 +381,7 @@ public class DownloadManager {
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (mission.running || !mission.canDownload()) continue;
if (mission.running || mission.isCorrupt()) continue;
flag = true;
mission.start();
@ -482,7 +471,7 @@ public class DownloadManager {
int paused = 0;
synchronized (this) {
for (DownloadMission mission : mMissionsPending) {
if (!mission.canDownload() || mission.isPsRunning()) continue;
if (mission.isCorrupt() || mission.isPsRunning()) continue;
if (mission.running && isMetered) {
paused++;
@ -542,6 +531,20 @@ public class DownloadManager {
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
private StoredDirectoryHelper getMainStorage(@NonNull String tag) {
if (tag.equals(TAG_AUDIO)) return mMainStorageAudio;
@ -656,7 +659,7 @@ public class DownloadManager {
synchronized (DownloadManager.this) {
for (DownloadMission mission : mMissionsPending) {
if (hidden.contains(mission) || mission.canDownload())
if (hidden.contains(mission) || mission.isCorrupt())
continue;
if (mission.running)

View File

@ -6,6 +6,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@ -40,6 +41,7 @@ import org.schabi.newpipe.player.helper.LockManager;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
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 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_THREADS = "DownloadManagerService.extra.threads";
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_SOURCE = "DownloadManagerService.extra.source";
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_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)
.setAction(Intent.ACTION_MAIN);
@ -182,7 +187,6 @@ public class DownloadManagerService extends Service {
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
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));
mLock = new LockManager(this);
setupStorageAPI(true);
}
@Override
@ -347,11 +349,12 @@ public class DownloadManagerService extends Service {
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
} 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))) {
loadMainStorage(key, DownloadManager.TAG_VIDEO, false);
mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO);
} 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.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls);
intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_KIND, kind);
intent.putExtra(EXTRA_THREADS, threads);
intent.putExtra(EXTRA_SOURCE, source);
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
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);
}
public void startMission(Intent intent) {
private void startMission(Intent intent) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
Uri path = intent.getParcelableExtra(EXTRA_PATH);
Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
String source = intent.getStringExtra(EXTRA_SOURCE);
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;
try {
storage = new StoredFileHelper(this, path, tag);
storage = new StoredFileHelper(this, parentPath, path, tag);
} catch (IOException e) {
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.source = source;
mission.nearLength = nearLength;
@ -525,60 +538,63 @@ public class DownloadManagerService extends Service {
mLockAcquired = acquire;
}
private void setupStorageAPI(boolean acquire) {
loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire);
loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire);
private StoredDirectoryHelper getVideoStorage() {
return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
}
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);
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));
final String defaultPath;
if (tag.equals(DownloadManager.TAG_VIDEO))
defaultPath = Environment.DIRECTORY_MOVIES;
else// if (tag.equals(DownloadManager.TAG_AUDIO))
defaultPath = Environment.DIRECTORY_MUSIC;
StoredDirectoryHelper mainStorage;
if (path == null || path.isEmpty()) {
mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null;
} else {
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;
}
}
}
switch (tag) {
case DownloadManager.TAG_VIDEO:
defaultPath = Environment.DIRECTORY_MOVIES;
break;
case DownloadManager.TAG_AUDIO:
defaultPath = Environment.DIRECTORY_MUSIC;
break;
default:
return null;
}
if (tag.equals(DownloadManager.TAG_VIDEO))
mManager.mMainStorageVideo = mainStorage;
else// if (tag.equals(DownloadManager.TAG_AUDIO))
mManager.mMainStorageAudio = mainStorage;
if (path == null || path.isEmpty()) {
return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : 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();
}
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.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
@ -346,7 +348,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.storage.getIOFile()
new File(URI.create(mission.storage.getUri().toString()))
);
} else {
uri = mission.storage.getUri();
@ -384,10 +386,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
}
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());
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;
}
@ -476,6 +486,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return;
case ERROR_PROGRESS_LOST:
msg = R.string.error_progress_lost;
break;
default:
if (mission.errCode >= 100 && mission.errCode < 600) {
msgEx = "HTTP " + mission.errCode;
@ -554,7 +565,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
return true;
case R.id.retry:
if (mission.hasInvalidStorage()) {
mRecover.tryRecover(mission);
mDownloadManager.tryRecover(mission);
if (mission.storage.isInvalid())
mRecover.tryRecover(mission);
else
recoverMission(mission);
return true;
}
mission.psContinue(true);
@ -672,13 +688,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (mDeleter != null) mDeleter.resume();
}
public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) {
public void recoverMission(DownloadMission mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue;
mission.changeStorage(newStorage);
mission.errCode = DownloadMission.ERROR_NOTHING;
mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS);
h.state = -1;
@ -822,9 +837,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
if (mission != null) {
if (mission.hasInvalidStorage()) {
retry.setEnabled(true);
delete.setEnabled(true);
showError.setEnabled(true);
retry.setVisible(true);
delete.setVisible(true);
showError.setVisible(true);
} else if (mission.isPsRunning()) {
switch (mission.errCode) {
case ERROR_INSUFFICIENT_STORAGE:

View File

@ -9,6 +9,7 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
@ -66,14 +67,7 @@ public class MissionsFragment extends Fragment {
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
mAdapter.deleterLoad(getView());
mAdapter.setRecover(mission ->
StoredFileHelper.requestSafWithFileCreation(
MissionsFragment.this,
REQUEST_DOWNLOAD_PATH_SAF,
mission.storage.getName(),
mission.storage.getType()
)
);
mAdapter.setRecover(MissionsFragment.this::recoverMission);
setAdapterButtons();
@ -92,7 +86,7 @@ public class MissionsFragment extends Fragment {
};
@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);
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
@ -239,8 +233,18 @@ public class MissionsFragment extends Fragment {
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
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
@ -285,8 +289,9 @@ public class MissionsFragment extends Fragment {
}
try {
StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag());
mAdapter.recoverMission(unsafeMissionTarget, storage);
String tag = unsafeMissionTarget.storage.getTag();
unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag);
mAdapter.recoverMission(unsafeMissionTarget);
} catch (IOException e) {
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.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R;
@ -81,6 +82,7 @@ public class Utility {
objectInputStream = new ObjectInputStream(new FileInputStream(file));
object = (T) objectInputStream.readObject();
} catch (Exception e) {
Log.e("Utility", "Failed to deserialize the object", e);
object = null;
}

View File

@ -442,8 +442,8 @@ abrir en modo popup</string>
<!-- message dialog about download error -->
<string name="show_error">Mostrar error</string>
<string name="label_code">Codigo</string>
<string name="error_file_creation">No se puede crear la carpeta de destino</string>
<string name="error_path_creation">No se puede crear el archivo</string>
<string name="error_file_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_ssl_exception">Fallo la conexión segura</string>
<string name="error_unknown_host">No se pudo encontrar el servidor</string>

View File

@ -176,13 +176,17 @@
</string-array>
<!-- 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_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">
<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 name="settings_filename_charset_name" translatable="false">
@ -190,8 +194,8 @@
<item>@string/charset_most_special_characters</item>
</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_default" translatable="false">3</string>
<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_replacement_character_summary">Invalid characters are replaced with this value</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_most_special_characters">Most special characters</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">
<Preference
app:iconSpaceReserved="false"
android:key="saf_test"
android:summary="Realiza una prueba del Storage Access Framework de Android"
android:title="Probar SAF"/>
<ListPreference
app:iconSpaceReserved="false"
android:defaultValue="@string/downloads_storage_api_default"

Binary file not shown.