mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	more SAF implementation
* full support for Directory API (Android Lollipop or later) * best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download * implemented directory choosing * fix download database version upgrading * misc. cleanup * do not release permission on the old save path (if the user change the download directory) under SAF api
This commit is contained in:
		| @@ -15,12 +15,13 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.StringRes; | ||||
| import android.support.v4.app.DialogFragment; | ||||
| import android.support.v4.provider.DocumentFile; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.view.menu.ActionMenuItemView; | ||||
| import android.support.v7.widget.Toolbar; | ||||
| import android.util.Log; | ||||
| import android.util.SparseArray; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.AdapterView; | ||||
| @@ -177,9 +178,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         final Context context = getContext(); | ||||
|         if (context == null) | ||||
|             throw new RuntimeException("Context was null"); | ||||
|         context = getContext(); | ||||
|  | ||||
|         setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); | ||||
|         Icepick.restoreInstanceState(this, savedInstanceState); | ||||
| @@ -321,11 +320,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|                 showFailedDialog(R.string.general_error); | ||||
|                 return; | ||||
|             } | ||||
|             try { | ||||
|                 continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); | ||||
|             } catch (IOException e) { | ||||
|                 showErrorActivity(e); | ||||
|  | ||||
|             DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData()); | ||||
|             if (docFile == null) { | ||||
|                 showFailedDialog(R.string.general_error); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // check if the selected file was previously used | ||||
|             checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -337,14 +340,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); | ||||
|  | ||||
|         boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); | ||||
|         okButton = toolbar.findViewById(R.id.okay); | ||||
|         okButton.setEnabled(false);// disable until the download service connection is done | ||||
|  | ||||
|         toolbar.setTitle(R.string.download_dialog_title); | ||||
|         toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); | ||||
|         toolbar.inflateMenu(R.menu.dialog_url); | ||||
|         toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); | ||||
|  | ||||
|         okButton = toolbar.findViewById(R.id.okay); | ||||
|         okButton.setEnabled(false);// disable until the download service connection is done | ||||
|  | ||||
|         toolbar.setOnMenuItemClickListener(item -> { | ||||
|             if (item.getItemId() == R.id.okay) { | ||||
| @@ -504,15 +507,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     StoredDirectoryHelper mainStorageAudio = null; | ||||
|     StoredDirectoryHelper mainStorageVideo = null; | ||||
|     DownloadManager downloadManager = null; | ||||
|  | ||||
|     MenuItem okButton = null; | ||||
|     ActionMenuItemView okButton = null; | ||||
|     Context context; | ||||
|  | ||||
|     private String getNameEditText() { | ||||
|         return nameEditText.getText().toString().trim(); | ||||
|         String str = nameEditText.getText().toString().trim(); | ||||
|  | ||||
|         return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); | ||||
|     } | ||||
|  | ||||
|     private void showFailedDialog(@StringRes int msg) { | ||||
|         new AlertDialog.Builder(getContext()) | ||||
|         new AlertDialog.Builder(context) | ||||
|                 .setMessage(msg) | ||||
|                 .setNegativeButton(android.R.string.ok, null) | ||||
|                 .create() | ||||
| @@ -521,7 +526,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|  | ||||
|     private void showErrorActivity(Exception e) { | ||||
|         ErrorActivity.reportError( | ||||
|                 getContext(), | ||||
|                 context, | ||||
|                 Collections.singletonList(e), | ||||
|                 null, | ||||
|                 null, | ||||
| @@ -530,18 +535,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     } | ||||
|  | ||||
|     private void prepareSelectedDownload() { | ||||
|         final Context context = getContext(); | ||||
|         StoredDirectoryHelper mainStorage; | ||||
|         MediaFormat format; | ||||
|         String mime; | ||||
|  | ||||
|         // first, build the filename and get the output folder (if possible) | ||||
|         // later, run a very very very large file checking logic | ||||
|  | ||||
|         String filename = getNameEditText() + "."; | ||||
|         if (filename.isEmpty()) { | ||||
|             filename = FilenameUtils.createFilename(context, currentInfo.getName()); | ||||
|         } | ||||
|         filename += "."; | ||||
|         String filename = getNameEditText().concat("."); | ||||
|  | ||||
|         switch (radioStreamsGroup.getCheckedRadioButtonId()) { | ||||
|             case R.id.audio_button: | ||||
| @@ -567,34 +568,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         } | ||||
|  | ||||
|         if (mainStorage == null) { | ||||
|             // this part is called if... | ||||
|             //                          older android version running with SAF preferred | ||||
|             //                          save path not defined (via download settings) | ||||
|             // This part is called if with SAF preferred: | ||||
|             //  * older android version running | ||||
|             //  * save path not defined (via download settings) | ||||
|  | ||||
|             StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // check for existing file with the same name | ||||
|         Uri result = mainStorage.findFile(filename); | ||||
|         checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); | ||||
|     } | ||||
|  | ||||
|         if (result == null) { | ||||
|             // the file does not exists, create | ||||
|             StoredFileHelper storage = mainStorage.createFile(filename, mime); | ||||
|             if (storage == null || !storage.canWrite()) { | ||||
|                 showFailedDialog(R.string.error_file_creation); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             continueSelectedDownload(storage); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // the target filename is already use, try load | ||||
|     private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) { | ||||
|         StoredFileHelper storage; | ||||
|  | ||||
|         try { | ||||
|             storage = new StoredFileHelper(context, result, mime); | ||||
|         } catch (IOException e) { | ||||
|             if (mainStorage == null) { | ||||
|                 // using SAF on older android version | ||||
|                 storage = new StoredFileHelper(context, null, targetFile, ""); | ||||
|             } else if (targetFile == null) { | ||||
|                 // the file does not exist, but it is probably used in a pending download | ||||
|                 storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag()); | ||||
|             } else { | ||||
|                 // the target filename is already use, attempt to use it | ||||
|                 storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             showErrorActivity(e); | ||||
|             return; | ||||
|         } | ||||
| @@ -618,6 +618,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|                 msgBody = R.string.download_already_running; | ||||
|                 break; | ||||
|             case None: | ||||
|                 if (mainStorage == null) { | ||||
|                     // This part is called if: | ||||
|                     // * using SAF on older android version | ||||
|                     // * save path not defined | ||||
|                     continueSelectedDownload(storage); | ||||
|                     return; | ||||
|                 } else if (targetFile == null) { | ||||
|                     // This part is called if: | ||||
|                     // * the filename is not used in a pending/finished download | ||||
|                     // * the file does not exists, create | ||||
|                     storage = mainStorage.createFile(filename, mime); | ||||
|                     if (storage == null || !storage.canWrite()) { | ||||
|                         showFailedDialog(R.string.error_file_creation); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     continueSelectedDownload(storage); | ||||
|                     return; | ||||
|                 } | ||||
|                 msgBtn = R.string.overwrite; | ||||
|                 msgBody = R.string.overwrite_unrelated_warning; | ||||
|                 break; | ||||
| @@ -625,49 +644,73 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|                 return; | ||||
|         } | ||||
|  | ||||
|         // handle user answer (overwrite or create another file with different name) | ||||
|         final String finalFilename = filename; | ||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(context); | ||||
|         builder.setTitle(R.string.download_dialog_title) | ||||
|                 .setMessage(msgBody) | ||||
|                 .setPositiveButton(msgBtn, (dialog, which) -> { | ||||
|                     dialog.dismiss(); | ||||
|  | ||||
|                     StoredFileHelper storageNew; | ||||
|                     switch (state) { | ||||
|                         case Finished: | ||||
|                         case Pending: | ||||
|                             downloadManager.forgetMission(storage); | ||||
|                         case None: | ||||
|                             // try take (or steal) the file permissions | ||||
|                             try { | ||||
|                                 storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); | ||||
|                                 if (storageNew.canWrite()) | ||||
|                                     continueSelectedDownload(storageNew); | ||||
|                                 else | ||||
|                                     showFailedDialog(R.string.error_file_creation); | ||||
|                             } catch (IOException e) { | ||||
|                                 showErrorActivity(e); | ||||
|                             } | ||||
|                             break; | ||||
|                         case PendingRunning: | ||||
|                             // FIXME: createUniqueFile() is not tested properly | ||||
|                             storageNew = mainStorage.createUniqueFile(finalFilename, mime); | ||||
|                             if (storageNew == null) | ||||
|                                 showFailedDialog(R.string.error_file_creation); | ||||
|                             else | ||||
|                                 continueSelectedDownload(storageNew); | ||||
|                             break; | ||||
|         AlertDialog.Builder askDialog = new AlertDialog.Builder(context) | ||||
|                 .setTitle(R.string.download_dialog_title) | ||||
|                 .setMessage(msgBody) | ||||
|                 .setNegativeButton(android.R.string.cancel, null); | ||||
|         final StoredFileHelper finalStorage = storage; | ||||
|  | ||||
|  | ||||
|         if (mainStorage == null) { | ||||
|             // This part is called if: | ||||
|             // * using SAF on older android version | ||||
|             // * save path not defined | ||||
|             switch (state) { | ||||
|                 case Pending: | ||||
|                 case Finished: | ||||
|                     askDialog.setPositiveButton(msgBtn, (dialog, which) -> { | ||||
|                         dialog.dismiss(); | ||||
|                         downloadManager.forgetMission(finalStorage); | ||||
|                         continueSelectedDownload(finalStorage); | ||||
|                     }); | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|             askDialog.create().show(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         askDialog.setPositiveButton(msgBtn, (dialog, which) -> { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             StoredFileHelper storageNew; | ||||
|             switch (state) { | ||||
|                 case Finished: | ||||
|                 case Pending: | ||||
|                     downloadManager.forgetMission(finalStorage); | ||||
|                 case None: | ||||
|                     if (targetFile == null) { | ||||
|                         storageNew = mainStorage.createFile(filename, mime); | ||||
|                     } else { | ||||
|                         try { | ||||
|                             // try take (or steal) the file | ||||
|                             storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); | ||||
|                         } catch (IOException e) { | ||||
|                             Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString()); | ||||
|                             storageNew = null; | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|                 .show(); | ||||
|  | ||||
|                     if (storageNew != null && storageNew.canWrite()) | ||||
|                         continueSelectedDownload(storageNew); | ||||
|                     else | ||||
|                         showFailedDialog(R.string.error_file_creation); | ||||
|                     break; | ||||
|                 case PendingRunning: | ||||
|                     storageNew = mainStorage.createUniqueFile(filename, mime); | ||||
|                     if (storageNew == null) | ||||
|                         showFailedDialog(R.string.error_file_creation); | ||||
|                     else | ||||
|                         continueSelectedDownload(storageNew); | ||||
|                     break; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         askDialog.create().show(); | ||||
|     } | ||||
|  | ||||
|     private void continueSelectedDownload(@NonNull StoredFileHelper storage) { | ||||
|         final Context context = getContext(); | ||||
|  | ||||
|         if (!storage.canWrite()) { | ||||
|             showFailedDialog(R.string.permission_denied); | ||||
|             return; | ||||
| @@ -678,7 +721,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|             if (storage.length() > 0) storage.truncate(); | ||||
|         } catch (IOException e) { | ||||
|             Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); | ||||
|             //showErrorActivity(e); | ||||
|             showFailedDialog(R.string.overwrite_failed); | ||||
|             return; | ||||
|         } | ||||
| @@ -748,7 +790,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         if (secondaryStreamUrl == null) { | ||||
|             urls = new String[]{selectedStream.getUrl()}; | ||||
|         } else { | ||||
|             urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; | ||||
|             urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()}; | ||||
|         } | ||||
|  | ||||
|         DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); | ||||
|   | ||||
| @@ -14,18 +14,23 @@ import android.support.v7.preference.Preference; | ||||
| import android.util.Log; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import com.nononsenseapps.filepicker.Utils; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.net.URI; | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.URLDecoder; | ||||
| import java.nio.charset.StandardCharsets; | ||||
|  | ||||
| import us.shandian.giga.io.StoredDirectoryHelper; | ||||
|  | ||||
| public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|     private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; | ||||
|     private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; | ||||
|     public static final boolean IGNORE_RELEASE_OLD_PATH = true; | ||||
|  | ||||
|     private String DOWNLOAD_PATH_VIDEO_PREFERENCE; | ||||
|     private String DOWNLOAD_PATH_AUDIO_PREFERENCE; | ||||
| @@ -38,38 +43,43 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|  | ||||
|     private Context ctx; | ||||
|  | ||||
|     private boolean lastAPIJavaIO; | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|  | ||||
|         initKeys(); | ||||
|         updatePreferencesSummary(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||
|         addPreferencesFromResource(R.xml.download_settings); | ||||
|         DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); | ||||
|         DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); | ||||
|         DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); | ||||
|         DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); | ||||
|  | ||||
|         prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); | ||||
|         prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||
|  | ||||
|         updatePathPickers(usingJavaIO()); | ||||
|         lastAPIJavaIO = usingJavaIO(); | ||||
|  | ||||
|         updatePreferencesSummary(); | ||||
|         updatePathPickers(lastAPIJavaIO); | ||||
|  | ||||
|         findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { | ||||
|             boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); | ||||
|  | ||||
|             if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             if (javaIO == lastAPIJavaIO) return true; | ||||
|             lastAPIJavaIO = javaIO; | ||||
|  | ||||
|             boolean res; | ||||
|  | ||||
|             if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                 // forget save paths (if necessary) | ||||
|                 res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); | ||||
|                 res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||
|             } else { | ||||
|                 res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||
|             } | ||||
|  | ||||
|             if (res) { | ||||
|                 Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); | ||||
|  | ||||
|                 // forget save paths | ||||
|                 forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); | ||||
|                 forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||
|  | ||||
|                 defaultPreferences.edit() | ||||
|                         .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") | ||||
|                         .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") | ||||
|                         .apply(); | ||||
|  | ||||
|                 updatePreferencesSummary(); | ||||
|             } | ||||
|  | ||||
| @@ -78,6 +88,30 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | ||||
|     private boolean forgetPath(String prefKey) { | ||||
|         String path = defaultPreferences.getString(prefKey, ""); | ||||
|         if (path == null || path.isEmpty()) return true; | ||||
|  | ||||
|         if (path.startsWith("file://")) return false; | ||||
|  | ||||
|         // forget SAF path (file:// is compatible with the SAF wrapper) | ||||
|         forgetSAFTree(getContext(), prefKey); | ||||
|         defaultPreferences.edit().putString(prefKey, "").apply(); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private boolean hasInvalidPath(String prefKey) { | ||||
|         String value = defaultPreferences.getString(prefKey, null); | ||||
|         return value == null || value.isEmpty(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||
|         addPreferencesFromResource(R.xml.download_settings); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onAttach(Context context) { | ||||
|         super.onAttach(context); | ||||
| @@ -91,20 +125,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|         findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); | ||||
|     } | ||||
|  | ||||
|     private void initKeys() { | ||||
|         DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); | ||||
|         DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); | ||||
|         DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); | ||||
|         DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); | ||||
|     private void updatePreferencesSummary() { | ||||
|         showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo); | ||||
|         showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio); | ||||
|     } | ||||
|  | ||||
|     private void updatePreferencesSummary() { | ||||
|         prefPathVideo.setSummary( | ||||
|                 defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) | ||||
|         ); | ||||
|         prefPathAudio.setSummary( | ||||
|                 defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) | ||||
|         ); | ||||
|     private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) { | ||||
|         String rawUri = defaultPreferences.getString(prefKey, null); | ||||
|         if (rawUri == null || rawUri.isEmpty()) { | ||||
|             target.setSummary(getString(defaultString)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); | ||||
|         } catch (UnsupportedEncodingException e) { | ||||
|             // nothing to do | ||||
|         } | ||||
|  | ||||
|         target.setSummary(rawUri); | ||||
|     } | ||||
|  | ||||
|     private void updatePathPickers(boolean useJavaIO) { | ||||
| @@ -119,20 +158,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible | ||||
|     @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     private void forgetSAFTree(String prefKey) { | ||||
|     private void forgetSAFTree(Context ctx, String prefKey) { | ||||
|         if (IGNORE_RELEASE_OLD_PATH) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         String oldPath = defaultPreferences.getString(prefKey, ""); | ||||
|  | ||||
|         if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { | ||||
|         if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) { | ||||
|             try { | ||||
|                 StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); | ||||
|                 if (!mainStorage.isDirect()) { | ||||
|                     mainStorage.revokePermissions(); | ||||
|                     Log.i(TAG, "revokePermissions()  [uri=" + oldPath + "]  ¡success!"); | ||||
|                 } | ||||
|             } catch (IOException err) { | ||||
|                 Log.e(TAG, "Error revoking Tree uri permissions  [uri=" + oldPath + "]", err); | ||||
|                 Uri uri = Uri.parse(oldPath); | ||||
|  | ||||
|                 ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|                 ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|  | ||||
|                 Log.i(TAG, "Revoke old path permissions success on " + oldPath); | ||||
|             } catch (Exception err) { | ||||
|                 Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -167,7 +211,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|             if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                 i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) | ||||
|                         .putExtra("android.content.extra.SHOW_ADVANCED", true) | ||||
|                         .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|                         .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|             } else { | ||||
|                 i = new Intent(getActivity(), FilePickerActivityHelper.class) | ||||
|                         .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) | ||||
| @@ -208,27 +252,37 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||
|  | ||||
|         if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             // steps: | ||||
|             //       1. acquire permissions on the new save path | ||||
|             //       2. save the new path, if step(1) was successful | ||||
|             //       1. revoke permissions on the old save path | ||||
|             //       2. acquire permissions on the new save path | ||||
|             //       3. save the new path, if step(2) was successful | ||||
|             final Context ctx = getContext(); | ||||
|             if (ctx == null) throw new NullPointerException("getContext()"); | ||||
|  | ||||
|             forgetSAFTree(ctx, key); | ||||
|  | ||||
|             try { | ||||
|                 ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|  | ||||
|                 StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); | ||||
|                 mainStorage.acquirePermissions(); | ||||
|                 Log.i(TAG, "acquirePermissions()  [uri=" + uri.toString() + "]  ¡success!"); | ||||
|                 Log.i(TAG, "Acquiring tree success from " + uri.toString()); | ||||
|  | ||||
|                 if (!mainStorage.canWrite()) | ||||
|                     throw new IOException("No write permissions on " + uri.toString()); | ||||
|             } catch (IOException err) { | ||||
|                 Log.e(TAG, "Error acquiring permissions on " + uri.toString()); | ||||
|                 Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); | ||||
|                 showMessageDialog(R.string.general_error, R.string.no_available_dir); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             defaultPreferences.edit().putString(key, uri.toString()).apply(); | ||||
|         } else { | ||||
|             defaultPreferences.edit().putString(key, uri.toString()).apply(); | ||||
|             updatePreferencesSummary(); | ||||
|  | ||||
|             File target = new File(URI.create(uri.toString())); | ||||
|             if (!target.canWrite()) | ||||
|             File target = Utils.getFileForUri(data.getData()); | ||||
|             if (!target.canWrite()) { | ||||
|                 showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); | ||||
|                 return; | ||||
|             } | ||||
|             uri = Uri.fromFile(target); | ||||
|         } | ||||
|  | ||||
|         defaultPreferences.edit().putString(key, uri.toString()).apply(); | ||||
|         updatePreferencesSummary(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,8 @@ public class DataReader { | ||||
|     public final static int INTEGER_SIZE = 4; | ||||
|     public final static int FLOAT_SIZE = 4; | ||||
|  | ||||
|     private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB | ||||
|  | ||||
|     private long position = 0; | ||||
|     private final SharpStream stream; | ||||
|  | ||||
| @@ -229,7 +231,7 @@ public class DataReader { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private final byte[] readBuffer = new byte[8 * 1024]; | ||||
|     private final byte[] readBuffer = new byte[BUFFER_SIZE]; | ||||
|     private int readOffset; | ||||
|     private int readCount; | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,6 @@ import java.io.IOException; | ||||
| import java.nio.ByteBuffer; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| public class Mp4FromDashWriter { | ||||
| @@ -262,12 +261,12 @@ public class Mp4FromDashWriter { | ||||
|         final int ftyp_size = make_ftyp(); | ||||
|  | ||||
|         // reserve moov space in the output stream | ||||
|         if (outStream.canSetLength()) { | ||||
|         /*if (outStream.canSetLength()) { | ||||
|             long length = writeOffset + auxSize; | ||||
|             outStream.setLength(length); | ||||
|             outSeek(length); | ||||
|         } else { | ||||
|             // hard way | ||||
|         } else {*/ | ||||
|         if (auxSize > 0) { | ||||
|             int length = auxSize; | ||||
|             byte[] buffer = new byte[8 * 1024];// 8 KiB | ||||
|             while (length > 0) { | ||||
| @@ -276,6 +275,7 @@ public class Mp4FromDashWriter { | ||||
|                 length -= count; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (auxBuffer == null) { | ||||
|             outSeek(ftyp_size); | ||||
|         } | ||||
|   | ||||
| @@ -10,6 +10,9 @@ import java.util.regex.Pattern; | ||||
|  | ||||
| public class FilenameUtils { | ||||
|  | ||||
|     private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"; | ||||
|     private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+"; | ||||
|  | ||||
|     /** | ||||
|      * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. | ||||
|      * @param context the context to retrieve strings and preferences from | ||||
| @@ -18,11 +21,28 @@ public class FilenameUtils { | ||||
|      */ | ||||
|     public static String createFilename(Context context, String title) { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         final String key = context.getString(R.string.settings_file_charset_key); | ||||
|         final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value)); | ||||
|         Pattern pattern = Pattern.compile(value); | ||||
|  | ||||
|         final String charset_ld = context.getString(R.string.charset_letters_and_digits_value); | ||||
|         final String charset_ms = context.getString(R.string.charset_most_special_value); | ||||
|         final String defaultCharset = context.getString(R.string.default_file_charset_value); | ||||
|  | ||||
|         final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_"); | ||||
|         String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null); | ||||
|  | ||||
|         final String charset; | ||||
|  | ||||
|         if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset; | ||||
|  | ||||
|         if (selectedCharset.equals(charset_ld)) { | ||||
|             charset = CHARSET_ONLY_LETTERS_AND_DIGITS; | ||||
|         } else if (selectedCharset.equals(charset_ms)) { | ||||
|             charset = CHARSET_MOST_SPECIAL; | ||||
|         } else { | ||||
|             charset = selectedCharset;// ¿is the user using a custom charset? | ||||
|         } | ||||
|  | ||||
|         Pattern pattern = Pattern.compile(charset); | ||||
|  | ||||
|         return createFilename(title, pattern, replacementChar); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread { | ||||
|  | ||||
|     @Override | ||||
|     public void run() { | ||||
|         if (mMission.current > 0) mMission.resetState(); | ||||
|         if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING); | ||||
|  | ||||
|         int retryCount = 0; | ||||
|         while (true) { | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package us.shandian.giga.get; | ||||
|  | ||||
| import android.os.Handler; | ||||
| import android.os.Message; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| @@ -86,7 +85,7 @@ public class DownloadMission extends Mission { | ||||
|     /** | ||||
|      * the post-processing algorithm instance | ||||
|      */ | ||||
|     public transient Postprocessing psAlgorithm; | ||||
|     public Postprocessing psAlgorithm; | ||||
|  | ||||
|     /** | ||||
|      * The current resource to download, see {@code urls[current]} and {@code offsets[current]} | ||||
| @@ -483,7 +482,7 @@ public class DownloadMission extends Mission { | ||||
|         if (init != null && Thread.currentThread() != init && init.isAlive()) { | ||||
|             init.interrupt(); | ||||
|             synchronized (blockState) { | ||||
|                 resetState(); | ||||
|                 resetState(false, true, ERROR_NOTHING); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
| @@ -525,10 +524,18 @@ public class DownloadMission extends Mission { | ||||
|         return res; | ||||
|     } | ||||
|  | ||||
|     void resetState() { | ||||
|  | ||||
|     /** | ||||
|      * Resets the mission state | ||||
|      * | ||||
|      * @param rollback       {@code true} true to forget all progress, otherwise, {@code false} | ||||
|      * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false} | ||||
|      */ | ||||
|     public void resetState(boolean rollback, boolean persistChanges, int errorCode) { | ||||
|         done = 0; | ||||
|         blocks = -1; | ||||
|         errCode = ERROR_NOTHING; | ||||
|         errCode = errorCode; | ||||
|         errObject = null; | ||||
|         fallback = false; | ||||
|         unknownLength = false; | ||||
|         finishCount = 0; | ||||
| @@ -537,7 +544,10 @@ public class DownloadMission extends Mission { | ||||
|         blockState.clear(); | ||||
|         threads = new Thread[0]; | ||||
|  | ||||
|         Utility.writeToFile(metadata, DownloadMission.this); | ||||
|         if (rollback) current = 0; | ||||
|  | ||||
|         if (persistChanges) | ||||
|             Utility.writeToFile(metadata, DownloadMission.this); | ||||
|     } | ||||
|  | ||||
|     private void initializer() { | ||||
| @@ -633,33 +643,22 @@ public class DownloadMission extends Mission { | ||||
|         threads[0].interrupt(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * changes the StoredFileHelper for another and saves the changes to the metadata file | ||||
|      * | ||||
|      * @param newStorage the new StoredFileHelper instance to use | ||||
|      */ | ||||
|     public void changeStorage(@NonNull StoredFileHelper newStorage) { | ||||
|         storage = newStorage; | ||||
|         // commit changes on the metadata file | ||||
|         runAsync(-2, this::writeThisToFile); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Indicates whatever the backed storage is invalid | ||||
|      * | ||||
|      * @return {@code true}, if storage is invalid and cannot be used | ||||
|      */ | ||||
|     public boolean hasInvalidStorage() { | ||||
|         return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); | ||||
|         return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Indicates whatever is possible to start the mission | ||||
|      * | ||||
|      * @return {@code true} is this mission is "sane", otherwise, {@code false} | ||||
|      * @return {@code true} is this mission its "healthy", otherwise, {@code false} | ||||
|      */ | ||||
|     public boolean canDownload() { | ||||
|         return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); | ||||
|     public boolean isCorrupt() { | ||||
|         return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage(); | ||||
|     } | ||||
|  | ||||
|     private boolean doPostprocessing() { | ||||
|   | ||||
| @@ -137,6 +137,10 @@ public class DownloadRunnable extends Thread { | ||||
|                     mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block | ||||
|  | ||||
|             } catch (Exception e) { | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e); | ||||
|                 } | ||||
|  | ||||
|                 mMission.setThreadBytePosition(mId, total); | ||||
|  | ||||
|                 if (!mMission.running || e instanceof ClosedByInterruptException) break; | ||||
| @@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread { | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); | ||||
|                 } | ||||
|  | ||||
|                 retry = true; | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -12,5 +12,7 @@ public class FinishedMission extends  Mission { | ||||
|         length = mission.length;// ¿or mission.done? | ||||
|         timestamp = mission.timestamp; | ||||
|         kind = mission.kind; | ||||
|         storage = mission.storage; | ||||
|  | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| @@ -36,15 +35,6 @@ public abstract class Mission implements Serializable { | ||||
|      */ | ||||
|     public StoredFileHelper storage; | ||||
|  | ||||
|     /** | ||||
|      * get the target file on the storage | ||||
|      * | ||||
|      * @return File object | ||||
|      */ | ||||
|     public Uri getDownloadedFileUri() { | ||||
|         return storage.getUri(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delete the downloaded file | ||||
|      * | ||||
| @@ -52,7 +42,7 @@ public abstract class Mission implements Serializable { | ||||
|      */ | ||||
|     public boolean delete() { | ||||
|         if (storage != null) return storage.delete(); | ||||
|         return  true; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -65,6 +55,6 @@ public abstract class Mission implements Serializable { | ||||
|     public String toString() { | ||||
|         Calendar calendar = Calendar.getInstance(); | ||||
|         calendar.setTimeInMillis(timestamp); | ||||
|         return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); | ||||
|         return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|     /** | ||||
|      * The table name of download missions | ||||
|      */ | ||||
|     private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; | ||||
|     private static final String FINISHED_TABLE_NAME = "finished_missions"; | ||||
|  | ||||
|     /** | ||||
|      * The key to the urls of a mission | ||||
| @@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|      * The statement to create the table | ||||
|      */ | ||||
|     private static final String MISSIONS_CREATE_TABLE = | ||||
|             "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + | ||||
|             "CREATE TABLE " + FINISHED_TABLE_NAME + " (" + | ||||
|                     KEY_PATH + " TEXT NOT NULL, " + | ||||
|                     KEY_SOURCE + " TEXT NOT NULL, " + | ||||
|                     KEY_DONE + " INTEGER NOT NULL, " + | ||||
| @@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|                             ) | ||||
|                     ).toString()); | ||||
|  | ||||
|                     db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); | ||||
|                     db.insert(FINISHED_TABLE_NAME, null, values); | ||||
|                 } | ||||
|                 db.setTransactionSuccessful(); | ||||
|                 db.endTransaction(); | ||||
| @@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|         mission.kind = kind.charAt(0); | ||||
|  | ||||
|         try { | ||||
|             mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); | ||||
|             mission.storage = new StoredFileHelper(context,null, Uri.parse(path), ""); | ||||
|         } catch (Exception e) { | ||||
|             Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); | ||||
|             mission.storage = new StoredFileHelper(path, "", ""); | ||||
|             mission.storage = new StoredFileHelper(null, path, "", ""); | ||||
|         } | ||||
|  | ||||
|         return mission; | ||||
| @@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|  | ||||
|     public ArrayList<FinishedMission> loadFinishedMissions() { | ||||
|         SQLiteDatabase database = getReadableDatabase(); | ||||
|         Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, | ||||
|         Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null, | ||||
|                 null, null, null, KEY_TIMESTAMP + " DESC"); | ||||
|  | ||||
|         int count = cursor.getCount(); | ||||
| @@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = getWritableDatabase(); | ||||
|         ContentValues values = getValuesOfMission(downloadMission); | ||||
|         database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); | ||||
|         database.insert(FINISHED_TABLE_NAME, null, values); | ||||
|     } | ||||
|  | ||||
|     public void deleteMission(Mission mission) { | ||||
|         if (mission == null) throw new NullPointerException("mission is null"); | ||||
|         String path = mission.getDownloadedFileUri().toString(); | ||||
|         String ts = String.valueOf(mission.timestamp); | ||||
|  | ||||
|         SQLiteDatabase database = getWritableDatabase(); | ||||
|  | ||||
|         if (mission instanceof FinishedMission) | ||||
|             database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); | ||||
|         else | ||||
|         if (mission instanceof FinishedMission) { | ||||
|             if (mission.storage.isInvalid()) { | ||||
|                 database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts}); | ||||
|             } else { | ||||
|                 database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{ | ||||
|                         ts, mission.storage.getUri().toString() | ||||
|                 }); | ||||
|             } | ||||
|         } else { | ||||
|             throw new UnsupportedOperationException("DownloadMission"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void updateMission(Mission mission) { | ||||
|         if (mission == null) throw new NullPointerException("mission is null"); | ||||
|         SQLiteDatabase database = getWritableDatabase(); | ||||
|         ContentValues values = getValuesOfMission(mission); | ||||
|         String path = mission.getDownloadedFileUri().toString(); | ||||
|         String ts = String.valueOf(mission.timestamp); | ||||
|  | ||||
|         int rowsAffected; | ||||
|  | ||||
|         if (mission instanceof FinishedMission) | ||||
|             rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); | ||||
|         else | ||||
|         if (mission instanceof FinishedMission) { | ||||
|             if (mission.storage.isInvalid()) { | ||||
|                 rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts}); | ||||
|             } else { | ||||
|                 rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{ | ||||
|                         mission.storage.getUri().toString() | ||||
|                 }); | ||||
|             } | ||||
|         } else { | ||||
|             throw new UnsupportedOperationException("DownloadMission"); | ||||
|         } | ||||
|  | ||||
|         if (rowsAffected != 1) { | ||||
|             Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); | ||||
|   | ||||
| @@ -12,7 +12,7 @@ public class CircularFileWriter extends SharpStream { | ||||
|  | ||||
|     private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB | ||||
|     private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB | ||||
|     private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB | ||||
|     private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB | ||||
|  | ||||
|     private OffsetChecker callback; | ||||
|  | ||||
| @@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream { | ||||
|         reportPosition = NOTIFY_BYTES_INTERVAL; | ||||
|     } | ||||
|  | ||||
|     private void flushAuxiliar() throws IOException { | ||||
|     private void flushAuxiliar(long amount) throws IOException { | ||||
|         if (aux.length < 1) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         boolean underflow = out.getOffset() >= out.length; | ||||
|  | ||||
|         out.flush(); | ||||
|         aux.flush(); | ||||
|  | ||||
|         boolean underflow = aux.offset < aux.length || out.offset < out.length; | ||||
|  | ||||
|         aux.target.seek(0); | ||||
|         out.target.seek(out.length); | ||||
|  | ||||
|         long length = aux.length; | ||||
|         out.length += aux.length; | ||||
|  | ||||
|         long length = amount; | ||||
|         while (length > 0) { | ||||
|             int read = (int) Math.min(length, Integer.MAX_VALUE); | ||||
|             read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); | ||||
|  | ||||
|             if (read < 1) { | ||||
|                 amount -= length; | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             out.writeProof(aux.queue, read); | ||||
|             length -= read; | ||||
|         } | ||||
|  | ||||
|         if (underflow) { | ||||
|             out.offset += aux.offset; | ||||
|             out.target.seek(out.offset); | ||||
|             if (out.offset >= out.length) { | ||||
|                 // calculate the aux underflow pointer | ||||
|                 if (aux.offset < amount) { | ||||
|                     out.offset += aux.offset; | ||||
|                     aux.offset = 0; | ||||
|                     out.target.seek(out.offset); | ||||
|                 } else { | ||||
|                     aux.offset -= amount; | ||||
|                     out.offset = out.length + amount; | ||||
|                 } | ||||
|             } else { | ||||
|                 aux.offset = 0; | ||||
|             } | ||||
|         } else { | ||||
|             out.offset = out.length; | ||||
|             out.offset += amount; | ||||
|             aux.offset -= amount; | ||||
|         } | ||||
|  | ||||
|         out.length += amount; | ||||
|  | ||||
|         if (out.length > maxLengthKnown) { | ||||
|             maxLengthKnown = out.length; | ||||
|         } | ||||
|  | ||||
|         if (amount < aux.length) { | ||||
|             // move the excess data to the beginning of the file | ||||
|             long readOffset = amount; | ||||
|             long writeOffset = 0; | ||||
|             byte[] buffer = new byte[128 * 1024]; // 128 KiB | ||||
|  | ||||
|             aux.length -= amount; | ||||
|             length = aux.length; | ||||
|             while (length > 0) { | ||||
|                 int read = (int) Math.min(length, Integer.MAX_VALUE); | ||||
|                 read = aux.target.read(buffer, 0, Math.min(read, buffer.length)); | ||||
|  | ||||
|                 aux.target.seek(writeOffset); | ||||
|                 aux.writeProof(buffer, read); | ||||
|  | ||||
|                 writeOffset += read; | ||||
|                 readOffset += read; | ||||
|                 length -= read; | ||||
|  | ||||
|                 aux.target.seek(readOffset); | ||||
|             } | ||||
|  | ||||
|             aux.target.setLength(aux.length); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (aux.length > THRESHOLD_AUX_LENGTH) { | ||||
|             aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0); | ||||
|         } | ||||
| @@ -94,7 +137,7 @@ public class CircularFileWriter extends SharpStream { | ||||
|      * @throws IOException if an I/O error occurs | ||||
|      */ | ||||
|     public long finalizeFile() throws IOException { | ||||
|         flushAuxiliar(); | ||||
|         flushAuxiliar(aux.length); | ||||
|  | ||||
|         out.flush(); | ||||
|  | ||||
| @@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream { | ||||
|         if (end == -1) { | ||||
|             available = Integer.MAX_VALUE; | ||||
|         } else if (end < offsetOut) { | ||||
|             throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut)); | ||||
|             throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut); | ||||
|         } else { | ||||
|             available = end - offsetOut; | ||||
|         } | ||||
| @@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream { | ||||
|                 length = aux.length + len; | ||||
|             } | ||||
|  | ||||
|             if (length > available || length < THRESHOLD_AUX_LENGTH) { | ||||
|                 aux.write(b, off, len); | ||||
|             } else { | ||||
|                 if (underflow) { | ||||
|                     aux.write(b, off, len); | ||||
|                     flushAuxiliar(); | ||||
|                 } else { | ||||
|                     flushAuxiliar(); | ||||
|                     out.write(b, off, len);// write directly on the output | ||||
|                 } | ||||
|             aux.write(b, off, len); | ||||
|  | ||||
|             if (length >= THRESHOLD_AUX_LENGTH && length <= available) { | ||||
|                 flushAuxiliar(available); | ||||
|             } | ||||
|         } else { | ||||
|             if (underflow) { | ||||
| @@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream { | ||||
|     @Override | ||||
|     public void seek(long offset) throws IOException { | ||||
|         long total = out.length + aux.length; | ||||
|  | ||||
|         if (offset == total) { | ||||
|             return;// nothing to do | ||||
|             // do not ignore the seek offset if a underflow exists | ||||
|             long relativeOffset = out.getOffset() + aux.getOffset(); | ||||
|             if (relativeOffset == total) { | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // flush everything, avoid any underflow | ||||
| @@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream { | ||||
|         } | ||||
|  | ||||
|         protected void seek(long absoluteOffset) throws IOException { | ||||
|             if (absoluteOffset == offset) { | ||||
|                 return;// nothing to do | ||||
|             } | ||||
|             offset = absoluteOffset; | ||||
|             target.seek(absoluteOffset); | ||||
|         } | ||||
|   | ||||
| @@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream { | ||||
|     public void seek(long offset) throws IOException { | ||||
|         channel.position(offset); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long length() throws IOException { | ||||
|         return channel.size(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,8 +4,10 @@ import android.annotation.TargetApi; | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.database.Cursor; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.provider.DocumentsContract; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.RequiresApi; | ||||
| @@ -13,8 +15,13 @@ import android.support.v4.provider.DocumentFile; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.net.URI; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
|  | ||||
| import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; | ||||
| import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; | ||||
|  | ||||
|  | ||||
| public class StoredDirectoryHelper { | ||||
|     public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; | ||||
| @@ -22,14 +29,27 @@ public class StoredDirectoryHelper { | ||||
|     private File ioTree; | ||||
|     private DocumentFile docTree; | ||||
|  | ||||
|     private ContentResolver contentResolver; | ||||
|     private Context context; | ||||
|  | ||||
|     private String tag; | ||||
|  | ||||
|     @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { | ||||
|         this.contentResolver = context.getContentResolver(); | ||||
|         this.tag = tag; | ||||
|  | ||||
|         if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { | ||||
|             this.ioTree = new File(URI.create(path.toString())); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.context = context; | ||||
|  | ||||
|         try { | ||||
|             this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS); | ||||
|         } catch (Exception e) { | ||||
|             throw new IOException(e); | ||||
|         } | ||||
|  | ||||
|         this.docTree = DocumentFile.fromTreeUri(context, path); | ||||
|  | ||||
|         if (this.docTree == null) | ||||
| @@ -37,23 +57,75 @@ public class StoredDirectoryHelper { | ||||
|     } | ||||
|  | ||||
|     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||
|     public StoredDirectoryHelper(@NonNull String location, String tag) { | ||||
|     public StoredDirectoryHelper(@NonNull URI location, String tag) { | ||||
|         ioTree = new File(location); | ||||
|         this.tag = tag; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public StoredFileHelper createFile(String filename, String mime) { | ||||
|         return createFile(filename, mime, false); | ||||
|     } | ||||
|  | ||||
|     public StoredFileHelper createUniqueFile(String name, String mime) { | ||||
|         ArrayList<String> matches = new ArrayList<>(); | ||||
|         String[] filename = splitFilename(name); | ||||
|         String lcFilename = filename[0].toLowerCase(); | ||||
|  | ||||
|         if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             for (File file : ioTree.listFiles()) | ||||
|                 addIfStartWith(matches, lcFilename, file.getName()); | ||||
|         } else { | ||||
|             // warning: SAF file listing is very slow | ||||
|             Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( | ||||
|                     docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()) | ||||
|             ); | ||||
|  | ||||
|             String[] projection = new String[]{COLUMN_DISPLAY_NAME}; | ||||
|             String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; | ||||
|             ContentResolver cr = context.getContentResolver(); | ||||
|  | ||||
|             try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) { | ||||
|                 if (cursor != null) { | ||||
|                     while (cursor.moveToNext()) | ||||
|                         addIfStartWith(matches, lcFilename, cursor.getString(0)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (matches.size() < 1) { | ||||
|             return createFile(name, mime, true); | ||||
|         } else { | ||||
|             // check if the filename is in use | ||||
|             String lcName = name.toLowerCase(); | ||||
|             for (String testName : matches) { | ||||
|                 if (testName.equals(lcName)) { | ||||
|                     lcName = null; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // check if not in use | ||||
|             if (lcName != null) return createFile(name, mime, true); | ||||
|         } | ||||
|  | ||||
|         Collections.sort(matches, String::compareTo); | ||||
|  | ||||
|         for (int i = 1; i < 1000; i++) { | ||||
|             if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) | ||||
|                 return createFile(makeFileName(filename[0], i, filename[1]), mime, true); | ||||
|         } | ||||
|  | ||||
|         return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false); | ||||
|     } | ||||
|  | ||||
|     private StoredFileHelper createFile(String filename, String mime, boolean safe) { | ||||
|         StoredFileHelper storage; | ||||
|  | ||||
|         try { | ||||
|             if (docTree == null) { | ||||
|                 storage = new StoredFileHelper(ioTree, filename, tag); | ||||
|                 storage.sourceTree = Uri.fromFile(ioTree).toString(); | ||||
|             } else { | ||||
|                 storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); | ||||
|                 storage.sourceTree = docTree.getUri().toString(); | ||||
|             } | ||||
|             if (docTree == null) | ||||
|                 storage = new StoredFileHelper(ioTree, filename, mime); | ||||
|             else | ||||
|                 storage = new StoredFileHelper(context, docTree, filename, mime, safe); | ||||
|         } catch (IOException e) { | ||||
|             return null; | ||||
|         } | ||||
| @@ -63,67 +135,6 @@ public class StoredDirectoryHelper { | ||||
|         return storage; | ||||
|     } | ||||
|  | ||||
|     public StoredFileHelper createUniqueFile(String filename, String mime) { | ||||
|         ArrayList<String> existingNames = new ArrayList<>(50); | ||||
|  | ||||
|         String ext; | ||||
|  | ||||
|         int dotIndex = filename.lastIndexOf('.'); | ||||
|         if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { | ||||
|             ext = ""; | ||||
|         } else { | ||||
|             ext = filename.substring(dotIndex); | ||||
|             filename = filename.substring(0, dotIndex - 1); | ||||
|         } | ||||
|  | ||||
|         String name; | ||||
|         if (docTree == null) { | ||||
|             for (File file : ioTree.listFiles()) { | ||||
|                 name = file.getName().toLowerCase(); | ||||
|                 if (name.startsWith(filename)) existingNames.add(name); | ||||
|             } | ||||
|         } else { | ||||
|             for (DocumentFile file : docTree.listFiles()) { | ||||
|                 name = file.getName(); | ||||
|                 if (name == null) continue; | ||||
|                 name = name.toLowerCase(); | ||||
|                 if (name.startsWith(filename)) existingNames.add(name); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         boolean free = true; | ||||
|         String lwFilename = filename.toLowerCase(); | ||||
|         for (String testName : existingNames) { | ||||
|             if (testName.equals(lwFilename)) { | ||||
|                 free = false; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (free) return createFile(filename, mime); | ||||
|  | ||||
|         String[] sortedNames = existingNames.toArray(new String[0]); | ||||
|         Arrays.sort(sortedNames); | ||||
|  | ||||
|         String newName; | ||||
|         int downloadIndex = 0; | ||||
|         do { | ||||
|             newName = filename + " (" + downloadIndex + ")" + ext; | ||||
|             ++downloadIndex; | ||||
|             if (downloadIndex == 1000) {  // Probably an error on our side | ||||
|                 newName = System.currentTimeMillis() + ext; | ||||
|                 break; | ||||
|             } | ||||
|         } while (Arrays.binarySearch(sortedNames, newName) >= 0); | ||||
|  | ||||
|  | ||||
|         return createFile(newName, mime); | ||||
|     } | ||||
|  | ||||
|     public boolean isDirect() { | ||||
|         return docTree == null; | ||||
|     } | ||||
|  | ||||
|     public Uri getUri() { | ||||
|         return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); | ||||
|     } | ||||
| @@ -136,34 +147,18 @@ public class StoredDirectoryHelper { | ||||
|         return tag; | ||||
|     } | ||||
|  | ||||
|     public void acquirePermissions() throws IOException { | ||||
|         if (docTree == null) return; | ||||
|  | ||||
|         try { | ||||
|             contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); | ||||
|         } catch (Throwable e) { | ||||
|             throw new IOException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void revokePermissions() throws IOException { | ||||
|         if (docTree == null) return; | ||||
|  | ||||
|         try { | ||||
|             contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); | ||||
|         } catch (Throwable e) { | ||||
|             throw new IOException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public Uri findFile(String filename) { | ||||
|         if (docTree == null) | ||||
|             return Uri.fromFile(new File(ioTree, filename)); | ||||
|         if (docTree == null) { | ||||
|             File res = new File(ioTree, filename); | ||||
|             return res.exists() ? Uri.fromFile(res) : null; | ||||
|         } | ||||
|  | ||||
|         // findFile() method is very slow | ||||
|         DocumentFile file = docTree.findFile(filename); | ||||
|         DocumentFile res = findFileSAFHelper(context, docTree, filename); | ||||
|         return res == null ? null : res.getUri(); | ||||
|     } | ||||
|  | ||||
|         return file == null ? null : file.getUri(); | ||||
|     public boolean canWrite() { | ||||
|         return docTree == null ? ioTree.canWrite() : docTree.canWrite(); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
| @@ -172,4 +167,76 @@ public class StoredDirectoryHelper { | ||||
|         return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     //////////////////// | ||||
|     //      Utils | ||||
|     /////////////////// | ||||
|  | ||||
|     private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) { | ||||
|         if (str == null || str.isEmpty()) return; | ||||
|         str = str.toLowerCase(); | ||||
|         if (str.startsWith(base)) list.add(str); | ||||
|     } | ||||
|  | ||||
|     private static String[] splitFilename(@NonNull String filename) { | ||||
|         int dotIndex = filename.lastIndexOf('.'); | ||||
|  | ||||
|         if (dotIndex < 0 || (dotIndex == filename.length() - 1)) | ||||
|             return new String[]{filename, ""}; | ||||
|  | ||||
|         return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)}; | ||||
|     } | ||||
|  | ||||
|     private static String makeFileName(String name, int idx, String ext) { | ||||
|         return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fast (but not enough) file/directory finder under the storage access framework | ||||
|      * | ||||
|      * @param context  The context | ||||
|      * @param tree     Directory where search | ||||
|      * @param filename Target filename | ||||
|      * @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null | ||||
|      */ | ||||
|     static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) { | ||||
|         if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             return tree.findFile(filename);// warning: this is very slow | ||||
|         } | ||||
|  | ||||
|         if (!tree.canRead()) return null;// missing read permission | ||||
|  | ||||
|         final int name = 0; | ||||
|         final int documentId = 1; | ||||
|  | ||||
|         // LOWER() SQL function is not supported | ||||
|         String selection = COLUMN_DISPLAY_NAME + " = ?"; | ||||
|         //String selection = COLUMN_DISPLAY_NAME + " LIKE ?%"; | ||||
|  | ||||
|         Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( | ||||
|                 tree.getUri(), DocumentsContract.getDocumentId(tree.getUri()) | ||||
|         ); | ||||
|         String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; | ||||
|         ContentResolver contentResolver = context.getContentResolver(); | ||||
|  | ||||
|         filename = filename.toLowerCase(); | ||||
|  | ||||
|         try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) { | ||||
|             if (cursor == null) return null; | ||||
|  | ||||
|             while (cursor.moveToNext()) { | ||||
|                 if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename)) | ||||
|                     continue; | ||||
|  | ||||
|                 return DocumentFile.fromSingleUri( | ||||
|                         context, DocumentsContract.buildDocumentUriUsingTree( | ||||
|                                 tree.getUri(), cursor.getString(documentId) | ||||
|                         ) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.provider.DocumentsContract; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v4.provider.DocumentFile; | ||||
|  | ||||
| @@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable { | ||||
|     private transient DocumentFile docFile; | ||||
|     private transient DocumentFile docTree; | ||||
|     private transient File ioFile; | ||||
|     private transient ContentResolver contentResolver; | ||||
|     private transient Context context; | ||||
|  | ||||
|     protected String source; | ||||
|     String sourceTree; | ||||
|     private String sourceTree; | ||||
|  | ||||
|     protected String tag; | ||||
|  | ||||
|     private String srcName; | ||||
|     private String srcType; | ||||
|  | ||||
|     public StoredFileHelper(String filename, String mime, String tag) { | ||||
|     public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) { | ||||
|         this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods | ||||
|  | ||||
|         this.srcName = filename; | ||||
|         this.srcType = mime == null ? DEFAULT_MIME : mime; | ||||
|         if (parent != null) this.sourceTree = parent.toString(); | ||||
|  | ||||
|         this.tag = tag; | ||||
|     } | ||||
|  | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { | ||||
|     StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException { | ||||
|         this.docTree = tree; | ||||
|         this.contentResolver = contentResolver; | ||||
|         this.context = context; | ||||
|  | ||||
|         // this is very slow, because SAF does not allow overwrite | ||||
|         DocumentFile res = this.docTree.findFile(filename); | ||||
|         DocumentFile res; | ||||
|  | ||||
|         if (res != null && res.exists() && res.isDirectory()) { | ||||
|             if (!res.delete()) | ||||
|                 throw new IOException("Directory with the same name found but cannot delete"); | ||||
|             res = null; | ||||
|         } | ||||
|  | ||||
|         if (res == null) { | ||||
|             res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); | ||||
|         if (safe) { | ||||
|             // no conflicts (the filename is not in use) | ||||
|             res = this.docTree.createFile(mime, filename); | ||||
|             if (res == null) throw new IOException("Cannot create the file"); | ||||
|         } else { | ||||
|             res = createSAF(context, mime, filename); | ||||
|         } | ||||
|  | ||||
|         this.docFile = res; | ||||
|         this.source = res.getUri().toString(); | ||||
|         this.srcName = getName(); | ||||
|         this.srcType = getType(); | ||||
|  | ||||
|         this.source = docFile.getUri().toString(); | ||||
|         this.sourceTree = docTree.getUri().toString(); | ||||
|  | ||||
|         this.srcName = this.docFile.getName(); | ||||
|         this.srcType = this.docFile.getType(); | ||||
|     } | ||||
|  | ||||
|     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||
|     public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { | ||||
|         this.source = path.toString(); | ||||
|         this.tag = tag; | ||||
|  | ||||
|         if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { | ||||
|             this.ioFile = new File(URI.create(this.source)); | ||||
|         } else { | ||||
|             DocumentFile file = DocumentFile.fromSingleUri(context, path); | ||||
|             if (file == null) | ||||
|                 throw new UnsupportedOperationException("Cannot get the file via SAF"); | ||||
|  | ||||
|             this.contentResolver = context.getContentResolver(); | ||||
|             this.docFile = file; | ||||
|  | ||||
|             try { | ||||
|                 this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|             } catch (Exception e) { | ||||
|                 throw new IOException(e); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.srcName = getName(); | ||||
|         this.srcType = getType(); | ||||
|     } | ||||
|  | ||||
|     public StoredFileHelper(File location, String filename, String tag) throws IOException { | ||||
|     StoredFileHelper(File location, String filename, String mime) throws IOException { | ||||
|         this.ioFile = new File(location, filename); | ||||
|         this.tag = tag; | ||||
|  | ||||
|         if (this.ioFile.exists()) { | ||||
|             if (!this.ioFile.isFile() && !this.ioFile.delete()) | ||||
| @@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable { | ||||
|         } | ||||
|  | ||||
|         this.source = Uri.fromFile(this.ioFile).toString(); | ||||
|         this.sourceTree = Uri.fromFile(location).toString(); | ||||
|  | ||||
|         this.srcName = ioFile.getName(); | ||||
|         this.srcType = mime; | ||||
|     } | ||||
|  | ||||
|     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||
|     public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException { | ||||
|         this.tag = tag; | ||||
|         this.source = path.toString(); | ||||
|  | ||||
|         if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) { | ||||
|             this.ioFile = new File(URI.create(this.source)); | ||||
|         } else { | ||||
|             DocumentFile file = DocumentFile.fromSingleUri(context, path); | ||||
|  | ||||
|             if (file == null) throw new RuntimeException("SAF not available"); | ||||
|  | ||||
|             this.context = context; | ||||
|  | ||||
|             if (file.getName() == null) { | ||||
|                 this.source = null; | ||||
|                 return; | ||||
|             } else { | ||||
|                 this.docFile = file; | ||||
|                 takePermissionSAF(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (parent != null) { | ||||
|             if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) | ||||
|                 this.docTree = DocumentFile.fromTreeUri(context, parent); | ||||
|  | ||||
|             this.sourceTree = parent.toString(); | ||||
|         } | ||||
|  | ||||
|         this.srcName = getName(); | ||||
|         this.srcType = getType(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { | ||||
|         Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree); | ||||
|  | ||||
|         if (storage.isInvalid()) | ||||
|             return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); | ||||
|             return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag); | ||||
|  | ||||
|         StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); | ||||
|         StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag); | ||||
|  | ||||
|         if (storage.sourceTree != null) { | ||||
|             instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); | ||||
|  | ||||
|             if (instance.docTree == null) | ||||
|                 throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); | ||||
|         } | ||||
|         // under SAF, if the target document is deleted, conserve the filename and mime | ||||
|         if (instance.srcName == null) instance.srcName = storage.srcName; | ||||
|         if (instance.srcType == null) instance.srcType = storage.srcType; | ||||
|  | ||||
|         return instance; | ||||
|     } | ||||
| @@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable { | ||||
|         who.startActivityForResult(intent, requestCode); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public SharpStream getStream() throws IOException { | ||||
|         invalid(); | ||||
|  | ||||
|         if (docFile == null) | ||||
|             return new FileStream(ioFile); | ||||
|         else | ||||
|             return new FileStreamSAF(contentResolver, docFile.getUri()); | ||||
|             return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -173,6 +184,12 @@ public class StoredFileHelper implements Serializable { | ||||
|         return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); | ||||
|     } | ||||
|  | ||||
|     public Uri getParentUri() { | ||||
|         invalid(); | ||||
|  | ||||
|         return sourceTree == null ? null : Uri.parse(sourceTree); | ||||
|     } | ||||
|  | ||||
|     public void truncate() throws IOException { | ||||
|         invalid(); | ||||
|  | ||||
| @@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable { | ||||
|     } | ||||
|  | ||||
|     public boolean delete() { | ||||
|         invalid(); | ||||
|  | ||||
|         if (source == null) return true; | ||||
|         if (docFile == null) return ioFile.delete(); | ||||
|  | ||||
|  | ||||
|         boolean res = docFile.delete(); | ||||
|  | ||||
|         try { | ||||
|             int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; | ||||
|             contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); | ||||
|             context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags); | ||||
|         } catch (Exception ex) { | ||||
|             // ¿what happen? | ||||
|             // nothing to do | ||||
|         } | ||||
|  | ||||
|         return res; | ||||
| @@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable { | ||||
|         return docFile == null ? ioFile.canWrite() : docFile.canWrite(); | ||||
|     } | ||||
|  | ||||
|     public File getIOFile() { | ||||
|         return ioFile; | ||||
|     } | ||||
|  | ||||
|     public String getName() { | ||||
|         if (source == null) return srcName; | ||||
|         return docFile == null ? ioFile.getName() : docFile.getName(); | ||||
|         if (source == null) | ||||
|             return srcName; | ||||
|         else if (docFile == null) | ||||
|             return ioFile.getName(); | ||||
|  | ||||
|         String name = docFile.getName(); | ||||
|         return name == null ? srcName : name; | ||||
|     } | ||||
|  | ||||
|     public String getType() { | ||||
|         if (source == null) return srcType; | ||||
|         return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO | ||||
|         if (source == null || docFile == null) | ||||
|             return srcType; | ||||
|  | ||||
|         String type = docFile.getType(); | ||||
|         return type == null ? srcType : type; | ||||
|     } | ||||
|  | ||||
|     public String getTag() { | ||||
| @@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable { | ||||
|         if (source == null) return false; | ||||
|  | ||||
|         boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); | ||||
|         boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? | ||||
|         boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? | ||||
|  | ||||
|         return exists && asFile; | ||||
|         return exists && isFile; | ||||
|     } | ||||
|  | ||||
|     public boolean create() { | ||||
|         invalid(); | ||||
|         boolean result; | ||||
|  | ||||
|         if (docFile == null) { | ||||
|             try { | ||||
|                 return ioFile.createNewFile(); | ||||
|                 result = ioFile.createNewFile(); | ||||
|             } catch (IOException e) { | ||||
|                 return false; | ||||
|             } | ||||
|         } else if (docTree == null) { | ||||
|             result = false; | ||||
|         } else { | ||||
|             if (!docTree.canRead() || !docTree.canWrite()) return false; | ||||
|             try { | ||||
|                 docFile = createSAF(context, srcType, srcName); | ||||
|                 if (docFile == null || docFile.getName() == null) return false; | ||||
|                 result = true; | ||||
|             } catch (IOException e) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (docTree == null || docFile.getName() == null) return false; | ||||
|         if (result) { | ||||
|             source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString(); | ||||
|             srcName = getName(); | ||||
|             srcType = getType(); | ||||
|         } | ||||
|  | ||||
|         DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); | ||||
|         if (res == null) return false; | ||||
|  | ||||
|         docFile = res; | ||||
|         return true; | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public void invalidate() { | ||||
| @@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable { | ||||
|  | ||||
|         source = null; | ||||
|  | ||||
|         sourceTree = null; | ||||
|         docTree = null; | ||||
|         docFile = null; | ||||
|         ioFile = null; | ||||
|         contentResolver = null; | ||||
|     } | ||||
|  | ||||
|     private void invalid() { | ||||
|         if (source == null) | ||||
|             throw new IllegalStateException("In invalid state"); | ||||
|         context = null; | ||||
|     } | ||||
|  | ||||
|     public boolean equals(StoredFileHelper storage) { | ||||
|         if (this.isInvalid() != storage.isInvalid()) return false; | ||||
|         if (this == storage) return true; | ||||
|  | ||||
|         // note: do not compare tags, files can have the same parent folder | ||||
|         //if (stringMismatch(this.tag, storage.tag)) return false; | ||||
|  | ||||
|         if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree))) | ||||
|             return false; | ||||
|  | ||||
|         if (this.isInvalid() || storage.isInvalid()) { | ||||
|             return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType); | ||||
|         } | ||||
|  | ||||
|         if (this.isDirect() != storage.isDirect()) return false; | ||||
|  | ||||
|         if (this.isDirect()) | ||||
| @@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable { | ||||
|         else | ||||
|             return "sourceFile=" + source + "  treeSource=" + (sourceTree == null ? "" : sourceTree) + "  tag=" + tag; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private void invalid() { | ||||
|         if (source == null) | ||||
|             throw new IllegalStateException("In invalid state"); | ||||
|     } | ||||
|  | ||||
|     private void takePermissionSAF() throws IOException { | ||||
|         try { | ||||
|             context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); | ||||
|         } catch (Exception e) { | ||||
|             if (docFile.getName() == null) throw new IOException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException { | ||||
|         DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename); | ||||
|  | ||||
|         if (res != null && res.exists() && res.isDirectory()) { | ||||
|             if (!res.delete()) | ||||
|                 throw new IOException("Directory with the same name found but cannot delete"); | ||||
|             res = null; | ||||
|         } | ||||
|  | ||||
|         if (res == null) { | ||||
|             res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename); | ||||
|             if (res == null) throw new IOException("Cannot create the file"); | ||||
|         } | ||||
|  | ||||
|         return res; | ||||
|     } | ||||
|  | ||||
|     private String getLowerCase(String str) { | ||||
|         return str == null ? null : str.toLowerCase(); | ||||
|     } | ||||
|  | ||||
|     private boolean stringMismatch(String str1, String str2) { | ||||
|         if (str1 == null && str2 == null) return false; | ||||
|         if ((str1 == null) != (str2 == null)) return true; | ||||
|  | ||||
|         return !str1.equals(str2); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import java.io.IOException; | ||||
| class Mp4FromDashMuxer extends Postprocessing { | ||||
|  | ||||
|     Mp4FromDashMuxer() { | ||||
|         super(2 * 1024 * 1024/* 2 MiB */, true); | ||||
|         super(3 * 1024 * 1024/* 3 MiB */, true); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.io.SharpStream; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.io.ChunkFileInputStream; | ||||
| @@ -19,7 +20,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | ||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | ||||
| import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | ||||
|  | ||||
| public abstract class Postprocessing { | ||||
| public abstract class Postprocessing implements Serializable { | ||||
|  | ||||
|     static transient final byte OK_RESULT = ERROR_NOTHING; | ||||
|  | ||||
| @@ -28,12 +29,10 @@ public abstract class Postprocessing { | ||||
|     public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; | ||||
|     public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; | ||||
|  | ||||
|     public static Postprocessing getAlgorithm(String algorithmName, String[] args) { | ||||
|     public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) { | ||||
|         Postprocessing instance; | ||||
|  | ||||
|         if (null == algorithmName) { | ||||
|             throw new NullPointerException("algorithmName"); | ||||
|         } else switch (algorithmName) { | ||||
|         switch (algorithmName) { | ||||
|             case ALGORITHM_TTML_CONVERTER: | ||||
|                 instance = new TtmlConverter(); | ||||
|                 break; | ||||
| @@ -47,13 +46,14 @@ public abstract class Postprocessing { | ||||
|                 instance = new M4aNoDash(); | ||||
|                 break; | ||||
|             /*case "example-algorithm": | ||||
|                 instance = new ExampleAlgorithm(mission);*/ | ||||
|                 instance = new ExampleAlgorithm();*/ | ||||
|             default: | ||||
|                 throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); | ||||
|         } | ||||
|  | ||||
|         instance.args = args; | ||||
|         instance.name = algorithmName; | ||||
|         instance.name = algorithmName;// for debug only, maybe remove this field in the future | ||||
|         instance.cacheDir = cacheDir; | ||||
|  | ||||
|         return instance; | ||||
|     } | ||||
| @@ -125,7 +125,6 @@ public abstract class Postprocessing { | ||||
|                         return -1; | ||||
|                     }; | ||||
|  | ||||
|                     // TODO: use Context.getCache() for this operation | ||||
|                     temp = new File(cacheDir, mission.storage.getName() + ".tmp"); | ||||
|  | ||||
|                     out = new CircularFileWriter(mission.storage.getStream(), temp, checker); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import java.io.IOException; | ||||
| class WebMMuxer extends Postprocessing { | ||||
|  | ||||
|     WebMMuxer() { | ||||
|         super(2048 * 1024/* 2 MiB */, true); | ||||
|         super(5 * 1024 * 1024/* 5 MiB */, true); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -62,13 +62,15 @@ public class DownloadManager { | ||||
|      * @param context Context for the data source for finished downloads | ||||
|      * @param handler Thread required for Messaging | ||||
|      */ | ||||
|     DownloadManager(@NonNull Context context, Handler handler) { | ||||
|     DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); | ||||
|         } | ||||
|  | ||||
|         mFinishedMissionStore = new FinishedMissionStore(context); | ||||
|         mHandler = handler; | ||||
|         mMainStorageAudio = storageAudio; | ||||
|         mMainStorageVideo = storageVideo; | ||||
|         mMissionsFinished = loadFinishedMissions(); | ||||
|         mPendingMissionsDir = getPendingDir(context); | ||||
|  | ||||
| @@ -129,91 +131,59 @@ public class DownloadManager { | ||||
|         } | ||||
|  | ||||
|         for (File sub : subs) { | ||||
|             if (sub.isFile()) { | ||||
|                 DownloadMission mis = Utility.readFromFile(sub); | ||||
|             if (!sub.isFile()) continue; | ||||
|  | ||||
|                 if (mis == null) { | ||||
|                     //noinspection ResultOfMethodCallIgnored | ||||
|                     sub.delete(); | ||||
|                 } else { | ||||
|                     if (mis.isFinished()) { | ||||
|                         //noinspection ResultOfMethodCallIgnored | ||||
|                         sub.delete(); | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     boolean exists; | ||||
|                     try { | ||||
|                         mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); | ||||
|                         exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); | ||||
|  | ||||
|                     } catch (Exception ex) { | ||||
|                         Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); | ||||
|                         mis.storage.invalidate(); | ||||
|                         exists = false; | ||||
|                     } | ||||
|  | ||||
|                     if (mis.isPsRunning()) { | ||||
|                         if (mis.psAlgorithm.worksOnSameFile) { | ||||
|                             // Incomplete post-processing results in a corrupted download file | ||||
|                             // because the selected algorithm works on the same file to save space. | ||||
|                             if (exists && !mis.storage.delete()) | ||||
|                                 Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); | ||||
|  | ||||
|                             exists = true; | ||||
|                         } | ||||
|  | ||||
|                         mis.psState = 0; | ||||
|                         mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; | ||||
|                         mis.errObject = null; | ||||
|                     } else if (!exists) { | ||||
|  | ||||
|                         StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); | ||||
|  | ||||
|                         if (!mis.storage.isInvalid() && !mis.storage.create()) { | ||||
|                             // using javaIO cannot recreate the file | ||||
|                             // using SAF in older devices (no tree available) | ||||
|                             // | ||||
|                             // force the user to pick again the save path | ||||
|                             mis.storage.invalidate(); | ||||
|                         } else if (mainStorage != null) { | ||||
|                             // if the user has changed the save path before this download, the original save path will be lost | ||||
|                             StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); | ||||
|                             if (newStorage == null) | ||||
|                                 mis.storage.invalidate(); | ||||
|                             else | ||||
|                                 mis.storage = newStorage; | ||||
|                         } | ||||
|  | ||||
|                         if (mis.isInitialized()) { | ||||
|                             // the progress is lost, reset mission state | ||||
|                             DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); | ||||
|                             m.timestamp = mis.timestamp; | ||||
|                             m.threadCount = mis.threadCount; | ||||
|                             m.source = mis.source; | ||||
|                             m.nearLength = mis.nearLength; | ||||
|                             m.enqueued = mis.enqueued; | ||||
|                             m.errCode = DownloadMission.ERROR_PROGRESS_LOST; | ||||
|                             mis = m; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); | ||||
|  | ||||
|                     mis.running = false; | ||||
|                     mis.recovered = exists; | ||||
|                     mis.metadata = sub; | ||||
|                     mis.maxRetry = mPrefMaxRetry; | ||||
|                     mis.mHandler = mHandler; | ||||
|  | ||||
|                     mMissionsPending.add(mis); | ||||
|                 } | ||||
|             DownloadMission mis = Utility.readFromFile(sub); | ||||
|             if (mis == null || mis.isFinished()) { | ||||
|                 //noinspection ResultOfMethodCallIgnored | ||||
|                 sub.delete(); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             boolean exists; | ||||
|             try { | ||||
|                 mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); | ||||
|                 exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); | ||||
|             } catch (Exception ex) { | ||||
|                 Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex); | ||||
|                 mis.storage.invalidate(); | ||||
|                 exists = false; | ||||
|             } | ||||
|  | ||||
|             if (mis.isPsRunning()) { | ||||
|                 if (mis.psAlgorithm.worksOnSameFile) { | ||||
|                     // Incomplete post-processing results in a corrupted download file | ||||
|                     // because the selected algorithm works on the same file to save space. | ||||
|                     if (exists && !mis.storage.delete()) | ||||
|                         Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); | ||||
|  | ||||
|                     exists = true; | ||||
|                 } | ||||
|  | ||||
|                 mis.psState = 0; | ||||
|                 mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; | ||||
|                 mis.errObject = null; | ||||
|             } else if (!exists) { | ||||
|                 tryRecover(mis); | ||||
|  | ||||
|                 // the progress is lost, reset mission state | ||||
|                 if (mis.isInitialized()) | ||||
|                     mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); | ||||
|             } | ||||
|  | ||||
|             if (mis.psAlgorithm != null) | ||||
|                 mis.psAlgorithm.cacheDir = pickAvailableCacheDir(ctx); | ||||
|  | ||||
|             mis.recovered = exists; | ||||
|             mis.metadata = sub; | ||||
|             mis.maxRetry = mPrefMaxRetry; | ||||
|             mis.mHandler = mHandler; | ||||
|  | ||||
|             mMissionsPending.add(mis); | ||||
|         } | ||||
|  | ||||
|         if (mMissionsPending.size() > 1) { | ||||
|         if (mMissionsPending.size() > 1) | ||||
|             Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -313,6 +283,25 @@ public class DownloadManager { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void tryRecover(DownloadMission mission) { | ||||
|         StoredDirectoryHelper mainStorage = getMainStorage(mission.storage.getTag()); | ||||
|  | ||||
|         if (!mission.storage.isInvalid() && mission.storage.create()) return; | ||||
|  | ||||
|         // using javaIO cannot recreate the file | ||||
|         // using SAF in older devices (no tree available) | ||||
|         // | ||||
|         // force the user to pick again the save path | ||||
|         mission.storage.invalidate(); | ||||
|  | ||||
|         if (mainStorage == null) return; | ||||
|  | ||||
|         // if the user has changed the save path before this download, the original save path will be lost | ||||
|         StoredFileHelper newStorage = mainStorage.createFile(mission.storage.getName(), mission.storage.getType()); | ||||
|  | ||||
|         if (newStorage != null) mission.storage = newStorage; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get a pending mission by its path | ||||
| @@ -392,7 +381,7 @@ public class DownloadManager { | ||||
|  | ||||
|         synchronized (this) { | ||||
|             for (DownloadMission mission : mMissionsPending) { | ||||
|                 if (mission.running || !mission.canDownload()) continue; | ||||
|                 if (mission.running || mission.isCorrupt()) continue; | ||||
|  | ||||
|                 flag = true; | ||||
|                 mission.start(); | ||||
| @@ -482,7 +471,7 @@ public class DownloadManager { | ||||
|         int paused = 0; | ||||
|         synchronized (this) { | ||||
|             for (DownloadMission mission : mMissionsPending) { | ||||
|                 if (!mission.canDownload() || mission.isPsRunning()) continue; | ||||
|                 if (mission.isCorrupt() || mission.isPsRunning()) continue; | ||||
|  | ||||
|                 if (mission.running && isMetered) { | ||||
|                     paused++; | ||||
| @@ -542,6 +531,20 @@ public class DownloadManager { | ||||
|         return MissionState.None; | ||||
|     } | ||||
|  | ||||
|     private static boolean isDirectoryAvailable(File directory) { | ||||
|         return directory != null && directory.canWrite(); | ||||
|     } | ||||
|  | ||||
|     static File pickAvailableCacheDir(@NonNull Context ctx) { | ||||
|         if (isDirectoryAvailable(ctx.getExternalCacheDir())) | ||||
|             return ctx.getExternalCacheDir(); | ||||
|         else if (isDirectoryAvailable(ctx.getCacheDir())) | ||||
|             return ctx.getCacheDir(); | ||||
|  | ||||
|         // this never should happen | ||||
|         return ctx.getDir("tmp", Context.MODE_PRIVATE); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private StoredDirectoryHelper getMainStorage(@NonNull String tag) { | ||||
|         if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; | ||||
| @@ -656,7 +659,7 @@ public class DownloadManager { | ||||
|  | ||||
|             synchronized (DownloadManager.this) { | ||||
|                 for (DownloadMission mission : mMissionsPending) { | ||||
|                     if (hidden.contains(mission) || mission.canDownload()) | ||||
|                     if (hidden.contains(mission) || mission.isCorrupt()) | ||||
|                         continue; | ||||
|  | ||||
|                     if (mission.running) | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| @@ -40,6 +41,7 @@ import org.schabi.newpipe.player.helper.LockManager; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.net.URI; | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| @@ -65,14 +67,15 @@ public class DownloadManagerService extends Service { | ||||
|     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; | ||||
|  | ||||
|     private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; | ||||
|     private static final String EXTRA_PATH = "DownloadManagerService.extra.path"; | ||||
|     private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; | ||||
|     private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; | ||||
|     private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; | ||||
|     private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; | ||||
|     private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; | ||||
|     private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; | ||||
|     private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; | ||||
|     private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; | ||||
|     private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; | ||||
|     private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; | ||||
|  | ||||
|     private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; | ||||
|     private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; | ||||
| @@ -136,7 +139,9 @@ public class DownloadManagerService extends Service { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mManager = new DownloadManager(this, mHandler); | ||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|  | ||||
|         mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage()); | ||||
|  | ||||
|         Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) | ||||
|                 .setAction(Intent.ACTION_MAIN); | ||||
| @@ -182,7 +187,6 @@ public class DownloadManagerService extends Service { | ||||
|             registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); | ||||
|         } | ||||
|  | ||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); | ||||
|  | ||||
|         handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); | ||||
| @@ -190,8 +194,6 @@ public class DownloadManagerService extends Service { | ||||
|         handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); | ||||
|  | ||||
|         mLock = new LockManager(this); | ||||
|  | ||||
|         setupStorageAPI(true); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -347,11 +349,12 @@ public class DownloadManagerService extends Service { | ||||
|         } else if (key.equals(getString(R.string.downloads_queue_limit))) { | ||||
|             mManager.mPrefQueueLimit = prefs.getBoolean(key, true); | ||||
|         } else if (key.equals(getString(R.string.downloads_storage_api))) { | ||||
|             setupStorageAPI(false); | ||||
|             mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); | ||||
|             mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); | ||||
|         } else if (key.equals(getString(R.string.download_path_video_key))) { | ||||
|             loadMainStorage(key, DownloadManager.TAG_VIDEO, false); | ||||
|             mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO); | ||||
|         } else if (key.equals(getString(R.string.download_path_audio_key))) { | ||||
|             loadMainStorage(key, DownloadManager.TAG_AUDIO, false); | ||||
|             mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -387,36 +390,46 @@ public class DownloadManagerService extends Service { | ||||
|         Intent intent = new Intent(context, DownloadManagerService.class); | ||||
|         intent.setAction(Intent.ACTION_RUN); | ||||
|         intent.putExtra(EXTRA_URLS, urls); | ||||
|         intent.putExtra(EXTRA_PATH, storage.getUri()); | ||||
|         intent.putExtra(EXTRA_KIND, kind); | ||||
|         intent.putExtra(EXTRA_THREADS, threads); | ||||
|         intent.putExtra(EXTRA_SOURCE, source); | ||||
|         intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); | ||||
|         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); | ||||
|         intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); | ||||
|         intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); | ||||
|  | ||||
|         intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); | ||||
|         intent.putExtra(EXTRA_PATH, storage.getUri()); | ||||
|         intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); | ||||
|  | ||||
|         context.startService(intent); | ||||
|     } | ||||
|  | ||||
|     public void startMission(Intent intent) { | ||||
|     private void startMission(Intent intent) { | ||||
|         String[] urls = intent.getStringArrayExtra(EXTRA_URLS); | ||||
|         Uri path = intent.getParcelableExtra(EXTRA_PATH); | ||||
|         Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH); | ||||
|         int threads = intent.getIntExtra(EXTRA_THREADS, 1); | ||||
|         char kind = intent.getCharExtra(EXTRA_KIND, '?'); | ||||
|         String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); | ||||
|         String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); | ||||
|         String source = intent.getStringExtra(EXTRA_SOURCE); | ||||
|         long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); | ||||
|         String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); | ||||
|         String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); | ||||
|  | ||||
|         StoredFileHelper storage; | ||||
|         try { | ||||
|             storage = new StoredFileHelper(this, path, tag); | ||||
|             storage = new StoredFileHelper(this, parentPath, path, tag); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException(e);// this never should happen | ||||
|         } | ||||
|  | ||||
|         final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); | ||||
|         Postprocessing ps; | ||||
|         if (psName == null) | ||||
|             ps = null; | ||||
|         else | ||||
|             ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this)); | ||||
|  | ||||
|         final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); | ||||
|         mission.threadCount = threads; | ||||
|         mission.source = source; | ||||
|         mission.nearLength = nearLength; | ||||
| @@ -525,60 +538,63 @@ public class DownloadManagerService extends Service { | ||||
|         mLockAcquired = acquire; | ||||
|     } | ||||
|  | ||||
|     private void setupStorageAPI(boolean acquire) { | ||||
|         loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); | ||||
|         loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); | ||||
|     private StoredDirectoryHelper getVideoStorage() { | ||||
|         return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); | ||||
|     } | ||||
|  | ||||
|     void loadMainStorage(String prefKey, String tag, boolean acquire) { | ||||
|     private StoredDirectoryHelper getAudioStorage() { | ||||
|         return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) { | ||||
|         String path = mPrefs.getString(prefKey, null); | ||||
|  | ||||
|         final String JAVA_IO = getString(R.string.downloads_storage_api_default); | ||||
|         boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); | ||||
|  | ||||
|         final String defaultPath; | ||||
|         if (tag.equals(DownloadManager.TAG_VIDEO)) | ||||
|             defaultPath = Environment.DIRECTORY_MOVIES; | ||||
|         else// if (tag.equals(DownloadManager.TAG_AUDIO)) | ||||
|             defaultPath = Environment.DIRECTORY_MUSIC; | ||||
|  | ||||
|         StoredDirectoryHelper mainStorage; | ||||
|         if (path == null || path.isEmpty()) { | ||||
|             mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; | ||||
|         } else { | ||||
|  | ||||
|             if (path.charAt(0) == File.separatorChar) { | ||||
|                 Log.i(TAG, "Migrating old save path: " + path); | ||||
|  | ||||
|                 useJavaIO = true; | ||||
|                 path = Uri.fromFile(new File(path)).toString(); | ||||
|  | ||||
|                 mPrefs.edit().putString(prefKey, path).apply(); | ||||
|             } | ||||
|  | ||||
|             if (useJavaIO) { | ||||
|                 mainStorage = new StoredDirectoryHelper(path, tag); | ||||
|             } else { | ||||
|  | ||||
|                 // tree api is not available in older versions | ||||
|                 if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|                     mainStorage = null; | ||||
|                 } else { | ||||
|                     try { | ||||
|                         mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); | ||||
|                         if (acquire) mainStorage.acquirePermissions(); | ||||
|                     } catch (IOException e) { | ||||
|                         Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); | ||||
|                         mainStorage = null; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         switch (tag) { | ||||
|             case DownloadManager.TAG_VIDEO: | ||||
|                 defaultPath = Environment.DIRECTORY_MOVIES; | ||||
|                 break; | ||||
|             case DownloadManager.TAG_AUDIO: | ||||
|                 defaultPath = Environment.DIRECTORY_MUSIC; | ||||
|                 break; | ||||
|             default: | ||||
|                 return null; | ||||
|         } | ||||
|  | ||||
|         if (tag.equals(DownloadManager.TAG_VIDEO)) | ||||
|             mManager.mMainStorageVideo = mainStorage; | ||||
|         else// if (tag.equals(DownloadManager.TAG_AUDIO)) | ||||
|             mManager.mMainStorageAudio = mainStorage; | ||||
|         if (path == null || path.isEmpty()) { | ||||
|             return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null; | ||||
|         } | ||||
|  | ||||
|         if (path.charAt(0) == File.separatorChar) { | ||||
|             Log.i(TAG, "Migrating old save path: " + path); | ||||
|  | ||||
|             useJavaIO = true; | ||||
|             path = Uri.fromFile(new File(path)).toString(); | ||||
|  | ||||
|             mPrefs.edit().putString(prefKey, path).apply(); | ||||
|         } | ||||
|  | ||||
|         boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; | ||||
|         if (useJavaIO || override) { | ||||
|             return new StoredDirectoryHelper(URI.create(path), tag); | ||||
|         } | ||||
|  | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             return null;// SAF Directory API is not available in older versions | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             return new StoredDirectoryHelper(this, Uri.parse(path), tag); | ||||
|         } catch (Exception e) { | ||||
|             Log.e(TAG, "Failed to load the storage of " + tag + " from " + path, e); | ||||
|             Toast.makeText(this, R.string.no_available_dir, Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -41,7 +41,9 @@ import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.report.UserAction; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.net.URI; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
|  | ||||
| @@ -346,7 +348,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|             uri = FileProvider.getUriForFile( | ||||
|                     mContext, | ||||
|                     BuildConfig.APPLICATION_ID + ".provider", | ||||
|                     mission.storage.getIOFile() | ||||
|                     new File(URI.create(mission.storage.getUri().toString())) | ||||
|             ); | ||||
|         } else { | ||||
|             uri = mission.storage.getUri(); | ||||
| @@ -384,10 +386,18 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|     } | ||||
|  | ||||
|     private static String resolveMimeType(@NonNull Mission mission) { | ||||
|         String mimeType; | ||||
|  | ||||
|         if (!mission.storage.isInvalid()) { | ||||
|             mimeType = mission.storage.getType(); | ||||
|             if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME)) | ||||
|                 return mimeType; | ||||
|         } | ||||
|  | ||||
|         String ext = Utility.getFileExt(mission.storage.getName()); | ||||
|         if (ext == null) return DEFAULT_MIME_TYPE; | ||||
|  | ||||
|         String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||
|         mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||
|  | ||||
|         return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; | ||||
|     } | ||||
| @@ -476,6 +486,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|                 return; | ||||
|             case ERROR_PROGRESS_LOST: | ||||
|                 msg = R.string.error_progress_lost; | ||||
|                 break; | ||||
|             default: | ||||
|                 if (mission.errCode >= 100 && mission.errCode < 600) { | ||||
|                     msgEx = "HTTP " + mission.errCode; | ||||
| @@ -554,7 +565,12 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|                     return true; | ||||
|                 case R.id.retry: | ||||
|                     if (mission.hasInvalidStorage()) { | ||||
|                         mRecover.tryRecover(mission); | ||||
|                         mDownloadManager.tryRecover(mission); | ||||
|                         if (mission.storage.isInvalid()) | ||||
|                             mRecover.tryRecover(mission); | ||||
|                         else | ||||
|                             recoverMission(mission); | ||||
|  | ||||
|                         return true; | ||||
|                     } | ||||
|                     mission.psContinue(true); | ||||
| @@ -672,13 +688,12 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|         if (mDeleter != null) mDeleter.resume(); | ||||
|     } | ||||
|  | ||||
|     public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { | ||||
|     public void recoverMission(DownloadMission mission) { | ||||
|         for (ViewHolderItem h : mPendingDownloadsItems) { | ||||
|             if (mission != h.item.mission) continue; | ||||
|  | ||||
|             mission.changeStorage(newStorage); | ||||
|             mission.errCode = DownloadMission.ERROR_NOTHING; | ||||
|             mission.errObject = null; | ||||
|             mission.resetState(true, false, DownloadMission.ERROR_NOTHING); | ||||
|  | ||||
|             h.status.setText(UNDEFINED_PROGRESS); | ||||
|             h.state = -1; | ||||
| @@ -822,9 +837,9 @@ public class MissionAdapter extends Adapter<ViewHolder> { | ||||
|  | ||||
|             if (mission != null) { | ||||
|                 if (mission.hasInvalidStorage()) { | ||||
|                     retry.setEnabled(true); | ||||
|                     delete.setEnabled(true); | ||||
|                     showError.setEnabled(true); | ||||
|                     retry.setVisible(true); | ||||
|                     delete.setVisible(true); | ||||
|                     showError.setVisible(true); | ||||
|                 } else if (mission.isPsRunning()) { | ||||
|                     switch (mission.errCode) { | ||||
|                         case ERROR_INSUFFICIENT_STORAGE: | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.os.IBinder; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v7.widget.GridLayoutManager; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| @@ -66,14 +67,7 @@ public class MissionsFragment extends Fragment { | ||||
|             mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); | ||||
|             mAdapter.deleterLoad(getView()); | ||||
|  | ||||
|             mAdapter.setRecover(mission -> | ||||
|                     StoredFileHelper.requestSafWithFileCreation( | ||||
|                             MissionsFragment.this, | ||||
|                             REQUEST_DOWNLOAD_PATH_SAF, | ||||
|                             mission.storage.getName(), | ||||
|                             mission.storage.getType() | ||||
|                     ) | ||||
|             ); | ||||
|             mAdapter.setRecover(MissionsFragment.this::recoverMission); | ||||
|  | ||||
|             setAdapterButtons(); | ||||
|  | ||||
| @@ -92,7 +86,7 @@ public class MissionsFragment extends Fragment { | ||||
|     }; | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         View v = inflater.inflate(R.layout.missions, container, false); | ||||
|  | ||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||
| @@ -239,8 +233,18 @@ public class MissionsFragment extends Fragment { | ||||
|         mAdapter.setMasterButtons(mStart, mPause); | ||||
|     } | ||||
|  | ||||
|     private void recoverMission(@NonNull DownloadMission mission) { | ||||
|         unsafeMissionTarget = mission; | ||||
|         StoredFileHelper.requestSafWithFileCreation( | ||||
|                 MissionsFragment.this, | ||||
|                 REQUEST_DOWNLOAD_PATH_SAF, | ||||
|                 mission.storage.getName(), | ||||
|                 mission.storage.getType() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|     public void onSaveInstanceState(@NonNull Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|  | ||||
|         if (mAdapter != null) { | ||||
| @@ -285,8 +289,9 @@ public class MissionsFragment extends Fragment { | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); | ||||
|             mAdapter.recoverMission(unsafeMissionTarget, storage); | ||||
|             String tag = unsafeMissionTarget.storage.getTag(); | ||||
|             unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag); | ||||
|             mAdapter.recoverMission(unsafeMissionTarget); | ||||
|         } catch (IOException e) { | ||||
|             Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.util.Log; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| @@ -81,6 +82,7 @@ public class Utility { | ||||
|             objectInputStream = new ObjectInputStream(new FileInputStream(file)); | ||||
|             object = (T) objectInputStream.readObject(); | ||||
|         } catch (Exception e) { | ||||
|             Log.e("Utility", "Failed to deserialize the object", e); | ||||
|             object = null; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -442,8 +442,8 @@ abrir en modo popup</string> | ||||
|     <!-- message dialog about download error --> | ||||
|     <string name="show_error">Mostrar error</string> | ||||
|     <string name="label_code">Codigo</string> | ||||
|     <string name="error_file_creation">No se puede crear la carpeta de destino</string> | ||||
|     <string name="error_path_creation">No se puede crear el archivo</string> | ||||
|     <string name="error_file_creation">No se puede crear el archivo</string> | ||||
|     <string name="error_path_creation">No se puede crear la carpeta de destino</string> | ||||
|     <string name="error_permission_denied">Permiso denegado por el sistema</string> | ||||
|     <string name="error_ssl_exception">Fallo la conexión segura</string> | ||||
|     <string name="error_unknown_host">No se pudo encontrar el servidor</string> | ||||
|   | ||||
| @@ -176,13 +176,17 @@ | ||||
|     </string-array> | ||||
|  | ||||
|     <!-- FileName Downloads  --> | ||||
|     <string name="settings_file_charset_key" translatable="false">file_rename</string> | ||||
|     <string name="settings_file_charset_key" translatable="false">file_rename_charset</string> | ||||
|     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> | ||||
|     <string name="settings_file_replacement_character_default_value" translatable="false">_</string> | ||||
|  | ||||
|  | ||||
|     <string name="charset_letters_and_digits_value" translatable="false">CHARSET_LETTERS_AND_DIGITS</string> | ||||
|     <string name="charset_most_special_value" translatable="false">CHARSET_MOST_SPECIAL</string> | ||||
|  | ||||
|     <string-array name="settings_filename_charset" translatable="false"> | ||||
|         <item>@string/charset_letters_and_digits_value</item> | ||||
|         <item>@string/charset_most_special_characters_value</item> | ||||
|         <item>@string/charset_most_special_value</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="settings_filename_charset_name" translatable="false"> | ||||
| @@ -190,7 +194,7 @@ | ||||
|         <item>@string/charset_most_special_characters</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string name="default_file_charset_value" translatable="false">@string/charset_most_special_characters_value</string> | ||||
|     <string name="default_file_charset_value" translatable="false">@string/charset_most_special_value</string> | ||||
|  | ||||
|     <string name="downloads_maximum_retry" translatable="false">downloads_max_retry</string> | ||||
|     <string name="downloads_maximum_retry_default" translatable="false">3</string> | ||||
|   | ||||
| @@ -305,8 +305,7 @@ | ||||
|     <string name="settings_file_charset_title">Allowed characters in filenames</string> | ||||
|     <string name="settings_file_replacement_character_summary">Invalid characters are replaced with this value</string> | ||||
|     <string name="settings_file_replacement_character_title">Replacement character</string> | ||||
|     <string name="charset_letters_and_digits_value" translatable="false">[^\\w\\d]+</string> | ||||
|     <string name="charset_most_special_characters_value" translatable="false">[\\n\\r|\\?*<":>/']+</string> | ||||
|  | ||||
|     <string name="charset_letters_and_digits">Letters and digits</string> | ||||
|     <string name="charset_most_special_characters">Most special characters</string> | ||||
|     <string name="toast_no_player">No app installed to play this file</string> | ||||
|   | ||||
| @@ -5,12 +5,6 @@ | ||||
|     android:title="@string/settings_category_downloads_title"> | ||||
|  | ||||
|  | ||||
|     <Preference | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:key="saf_test" | ||||
|         android:summary="Realiza una prueba del Storage Access Framework de Android" | ||||
|         android:title="Probar SAF"/> | ||||
|  | ||||
|     <ListPreference | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:defaultValue="@string/downloads_storage_api_default" | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/db.dia
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/db.dia
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user
	 kapodamy
					kapodamy