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:
parent
f6b32823ba
commit
d00dc798f4
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -38,38 +43,43 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,7 +194,7 @@
|
||||||
<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>
|
||||||
|
|
|
@ -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|\\?*<":>/']+</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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
BIN
assets/db.dia
BIN
assets/db.dia
Binary file not shown.
Loading…
Reference in New Issue