From 1e09a1768e0263b5f0b5d774d4d8173c22b0e398 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 5 Jun 2021 23:52:39 +0200 Subject: [PATCH 01/15] Revert all commits related to ContentSettingsFragment Revert "Annotate methode parameters as NonNull" This reverts commit 004907d306f84bbe800b624e9af650f73989652b. Revert "Commit path immediately when import backup" This reverts commit 05eb0d0fbe7ffc13d6b9a431d0825c6ce6c8dc79. Revert "Set ImportExportDataPath only on successful import" This reverts commit f13a1b04e64d01d2337662b724ddff4b9ede54fc. Revert "Set ImportExportDataPath only on successful export" This reverts commit fd4408e572f5c5aacf5c74ace91ef7c1ed46c474. Revert "Invert if condition in ContentSettingsFragment.setImportExportDataPath for better readability" This reverts commit 92ab9cae27620337d21e4f634b918a919aa5d830. Revert "Move ContentSettingsFragment.isValidPath to helpers and add unit test for it." This reverts commit fa2b11b7685d989d1e28e62f65ff1f80d890a9d1. Revert "Save backup import/export location for feature import/exports" This reverts commit 82f43ac6a64c1754e023aed6c5cd7a117f1a99dc. Remove FilePathHelperTest file --- .../settings/ContentSettingsFragment.java | 88 ++++--------------- .../schabi/newpipe/util/FilePathUtils.java | 22 ----- app/src/main/res/values/settings_keys.xml | 1 - .../newpipe/util/FilePathHelperTest.java | 56 ------------ 4 files changed, 16 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/util/FilePathUtils.java delete mode 100644 app/src/test/java/org/schabi/newpipe/util/FilePathHelperTest.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 9af3666a6e5..3fd44c4d521 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.settings; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -26,7 +25,6 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; -import org.schabi.newpipe.util.FilePathUtils; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ZipHelper; @@ -43,8 +41,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private ContentSettingsManager manager; - private String importExportDataPathKey; - private String thumbnailLoadToggleKey; private String youtubeRestrictedModeEnabledKey; @@ -60,7 +56,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro addPreferencesFromResource(R.xml.content_settings); - importExportDataPathKey = getString(R.string.import_export_data_path); final Preference importDataPreference = findPreference(getString(R.string.import_data)); importDataPreference.setOnPreferenceClickListener(p -> { final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) @@ -68,10 +63,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); - final String path = defaultPreferences.getString(importExportDataPathKey, ""); - if (FilePathUtils.isValidDirectoryPath(path)) { - i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path); - } startActivityForResult(i, REQUEST_IMPORT_PATH); return true; }); @@ -83,10 +74,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); - final String path = defaultPreferences.getString(importExportDataPathKey, ""); - if (FilePathUtils.isValidDirectoryPath(path)) { - i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path); - } startActivityForResult(i, REQUEST_EXPORT_PATH); return true; }); @@ -177,15 +164,15 @@ public void onActivityResult(final int requestCode, final int resultCode, if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data.getData() != null) { - final File file = Utils.getFileForUri(data.getData()); - + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); if (requestCode == REQUEST_EXPORT_PATH) { - exportDatabase(file); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); } else { final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setMessage(R.string.override_current_data) .setPositiveButton(getString(R.string.finish), - (d, id) -> importDatabase(file)) + (d, id) -> importDatabase(path)) .setNegativeButton(android.R.string.cancel, (d, id) -> d.cancel()); builder.create().show(); @@ -193,34 +180,26 @@ public void onActivityResult(final int requestCode, final int resultCode, } } - private void exportDatabase(@NonNull final File folder) { + private void exportDatabase(final String path) { try { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - final String path = folder.getAbsolutePath() + "/NewPipeData-" - + sdf.format(new Date()) + ".zip"; - //checkpoint before export NewPipeDatabase.checkpoint(); final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(requireContext()); + .getDefaultSharedPreferences(requireContext()); manager.exportDatabase(preferences, path); - setImportExportDataPath(folder, false); - Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e); } } - private void importDatabase(@NonNull final File file) { - final String filePath = file.getAbsolutePath(); - + private void importDatabase(final String filePath) { // check if file is supported if (!ZipHelper.isValidZipFile(filePath)) { Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); + .show(); return; } @@ -231,7 +210,7 @@ private void importDatabase(@NonNull final File file) { if (!manager.extractDb(filePath)) { Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) - .show(); + .show(); } //If settings file exist, ask if it should be imported. @@ -241,58 +220,23 @@ private void importDatabase(@NonNull final File file) { alert.setNegativeButton(android.R.string.no, (dialog, which) -> { dialog.dismiss(); - finishImport(file); + // restart app to properly load db + System.exit(0); }); alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { dialog.dismiss(); manager.loadSharedPreferences(PreferenceManager - .getDefaultSharedPreferences(requireContext())); - finishImport(file); + .getDefaultSharedPreferences(requireContext())); + // restart app to properly load db + System.exit(0); }); alert.show(); } else { - finishImport(file); + // restart app to properly load db + System.exit(0); } } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e); } } - - /** - * Save import path and restart system. - * - * @param file The file of the created backup - */ - private void finishImport(@NonNull final File file) { - if (file.getParentFile() != null) { - //immediately because app is about to exit - setImportExportDataPath(file.getParentFile(), true); - } - - // restart app to properly load db - System.exit(0); - } - - @SuppressLint("ApplySharedPref") - private void setImportExportDataPath(@NonNull final File file, final boolean immediately) { - final String directoryPath; - if (file.isDirectory()) { - directoryPath = file.getAbsolutePath(); - } else { - final File parentFile = file.getParentFile(); - if (parentFile != null) { - directoryPath = parentFile.getAbsolutePath(); - } else { - directoryPath = ""; - } - } - final SharedPreferences.Editor editor = defaultPreferences - .edit() - .putString(importExportDataPathKey, directoryPath); - if (immediately) { - editor.commit(); - } else { - editor.apply(); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePathUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilePathUtils.java deleted file mode 100644 index 4162e563af5..00000000000 --- a/app/src/main/java/org/schabi/newpipe/util/FilePathUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.schabi.newpipe.util; - -import java.io.File; - -public final class FilePathUtils { - private FilePathUtils() { } - - - /** - * Check that the path is a valid directory path and it exists. - * - * @param path full path of directory, - * @return is path valid or not - */ - public static boolean isValidDirectoryPath(final String path) { - if (path == null || path.isEmpty()) { - return false; - } - final File file = new File(path); - return file.exists() && file.isDirectory(); - } -} diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index c23e81fbe22..fd6cc725140 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -265,7 +265,6 @@ feed_use_dedicated_fetch_method - import_export_data_path import_data export_data diff --git a/app/src/test/java/org/schabi/newpipe/util/FilePathHelperTest.java b/app/src/test/java/org/schabi/newpipe/util/FilePathHelperTest.java deleted file mode 100644 index 3c9f127202a..00000000000 --- a/app/src/test/java/org/schabi/newpipe/util/FilePathHelperTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.schabi.newpipe.util; - -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class FilePathHelperTest { - - private Path dir; - - @Before - public void setUp() throws IOException { - dir = Files.createTempDirectory("dir1"); - } - - @Test - public void testIsValidDirectoryPathWithEmptyString() { - assertFalse(FilePathUtils.isValidDirectoryPath("")); - } - - @Test - public void testIsValidDirectoryPathWithNullString() { - assertFalse(FilePathUtils.isValidDirectoryPath(null)); - } - - @Test - public void testIsValidDirectoryPathWithValidPath() { - assertTrue(FilePathUtils.isValidDirectoryPath(dir.toAbsolutePath().toString())); - } - - @Test - public void testIsValidDirectoryPathWithDeepValidDirectory() throws IOException { - final File subDir = Files.createDirectory(dir.resolve("subdir")).toFile(); - assertTrue(FilePathUtils.isValidDirectoryPath(subDir.getAbsolutePath())); - } - - @Test - public void testIsValidDirectoryPathWithNotExistDirectory() { - assertFalse(FilePathUtils.isValidDirectoryPath(dir.resolve("not-exists-subdir"). - toFile().getAbsolutePath())); - } - - @Test - public void testIsValidDirectoryPathWithFile() throws IOException { - final File tempFile = Files.createFile(dir.resolve("simple_file")).toFile(); - assertFalse(FilePathUtils.isValidDirectoryPath(tempFile.getAbsolutePath())); - } - -} From 0f75024e033ec98184c46c7c3bdde237ec67fe2e Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 13 Jun 2020 17:29:57 +0200 Subject: [PATCH 02/15] Support SAF properly --- app/src/main/AndroidManifest.xml | 1 - app/src/main/java/org/schabi/newpipe/App.java | 4 +- .../newpipe/download/DownloadDialog.java | 124 ++++++++++++------ .../subscription/SubscriptionFragment.kt | 29 ++-- .../SubscriptionsImportFragment.java | 11 +- .../services/SubscriptionsExportService.java | 33 ++--- .../services/SubscriptionsImportService.java | 18 +-- .../settings/ContentSettingsFragment.java | 81 +++++++----- .../settings/ContentSettingsManager.kt | 16 ++- .../settings/DownloadSettingsFragment.java | 37 +++--- .../newpipe/settings/NewPipeSettings.java | 50 ++++--- .../newpipe/settings/SettingsActivity.java | 6 - .../newpipe/streams/io/SharpInputStream.java | 48 +++++++ .../newpipe/streams/io/SharpOutputStream.java | 42 ++++++ .../newpipe/streams/io/SharpStream.java | 3 +- .../util/FilePickerActivityHelper.java | 20 --- .../org/schabi/newpipe/util/ZipHelper.java | 21 +-- .../us/shandian/giga/io/SharpInputStream.java | 61 --------- .../giga/io/StoredDirectoryHelper.java | 17 +++ .../us/shandian/giga/io/StoredFileHelper.java | 95 +++++++++++--- .../giga/ui/fragment/MissionsFragment.java | 28 ++-- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/download_settings.xml | 2 - 23 files changed, 444 insertions(+), 304 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java delete mode 100644 app/src/main/java/us/shandian/giga/io/SharpInputStream.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb15cddcea0..6eb9f514f9b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,6 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:logo="@mipmap/ic_launcher" - android:requestLegacyExternalStorage="true" android:theme="@style/OpeningTheme" android:resizeableActivity="true" tools:ignore="AllowBackup"> diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index af118387caa..784be8d0b49 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -27,7 +27,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.ktx.ExceptionUtils; -import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; @@ -91,7 +91,7 @@ public void onCreate() { app = this; // Initialize settings first because others inits can use its values - SettingsActivity.initSettings(this); + NewPipeSettings.initSettings(this); NewPipe.init(getDownloader(), Localization.getPreferredLocalization(this), diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index e7ae8d8790b..051e9b7524f 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -83,6 +83,8 @@ public class DownloadDialog extends DialogFragment private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230; + private static final int REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER = 0x789E; + private static final int REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER = 0x789F; @State StreamInfo currentInfo; @@ -116,6 +118,10 @@ public class DownloadDialog extends DialogFragment private SharedPreferences prefs; + // Variables for file name and MIME type when picking new folder because it's not set yet + private String filenameTmp; + private String mimeTmp; + public static DownloadDialog newInstance(final StreamInfo info) { final DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); @@ -374,12 +380,16 @@ public void onSaveInstanceState(@NonNull final Bundle outState) { public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) { - if (data.getData() == null) { - showFailedDialog(R.string.general_error); - return; - } + if (resultCode != Activity.RESULT_OK) { + return; + } + if (data.getData() == null) { + showFailedDialog(R.string.general_error); + return; + } + + if (requestCode == REQUEST_DOWNLOAD_SAVE_AS) { if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) { final File file = Utils.getFileForUri(data.getData()); checkSelectedDownload(null, Uri.fromFile(file), file.getName(), @@ -396,6 +406,37 @@ public void onActivityResult(final int requestCode, final int resultCode, final // check if the selected file was previously used checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType()); + } else if (requestCode == REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER + || requestCode == REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER) { + Uri uri = data.getData(); + if (FilePickerActivityHelper.isOwnFileUri(context, uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)); + } else { + context.grantUriPermission(context.getPackageName(), uri, + StoredDirectoryHelper.PERMISSION_FLAGS); + } + + final String key; + final String tag; + if (requestCode == REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER) { + key = getString(R.string.download_path_audio_key); + tag = DownloadManager.TAG_AUDIO; + } else { + key = getString(R.string.download_path_video_key); + tag = DownloadManager.TAG_VIDEO; + } + + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(key, uri.toString()).apply(); + + try { + final StoredDirectoryHelper mainStorage + = new StoredDirectoryHelper(context, uri, tag); + checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), + filenameTmp, mimeTmp); + } catch (final IOException e) { + showFailedDialog(R.string.general_error); + } } } @@ -603,84 +644,89 @@ private void showFailedDialog(@StringRes final int msg) { private void prepareSelectedDownload() { final StoredDirectoryHelper mainStorage; final MediaFormat format; - final String mime; final String selectedMediaType; // first, build the filename and get the output folder (if possible) // later, run a very very very large file checking logic - String filename = getNameEditText().concat("."); + filenameTmp = getNameEditText().concat("."); switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedMediaType = getString(R.string.last_download_type_audio_key); mainStorage = mainStorageAudio; format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); - switch (format) { - case WEBMA_OPUS: - mime = "audio/ogg"; - filename += "opus"; - break; - default: - mime = format.mimeType; - filename += format.suffix; - break; + if (format == MediaFormat.WEBMA_OPUS) { + mimeTmp = "audio/ogg"; + filenameTmp += "opus"; + } else { + mimeTmp = format.mimeType; + filenameTmp += format.suffix; } break; case R.id.video_button: selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); - mime = format.mimeType; - filename += format.suffix; + mimeTmp = format.mimeType; + filenameTmp += format.suffix; break; case R.id.subtitle_button: selectedMediaType = getString(R.string.last_download_type_subtitle_key); mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); - mime = format.mimeType; - filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; + mimeTmp = format.mimeType; + filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; break; default: throw new RuntimeException("No stream selected"); } - if (mainStorage == null || askForSavePath) { - // This part is called if with SAF preferred: - // * older android version running - // * save path not defined (via download settings) - // * the user checked the "ask where to download" option - - if (!askForSavePath) { - Toast.makeText(context, getString(R.string.no_available_dir), - Toast.LENGTH_LONG).show(); + if (!askForSavePath && (mainStorage == null || (mainStorage.isDirect() + == NewPipeSettings.useStorageAccessFramework(context)))) { + // Pick new download folder if one of: + // - Download folder is not set + // - Download folder uses SAF while SAF is disabled + // - Download folder doesn't use SAF while SAF is enabled + Toast.makeText(context, getString(R.string.no_dir_yet), + Toast.LENGTH_LONG).show(); + + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + startActivityForResult(StoredDirectoryHelper.getPicker(context), + REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER); + } else { + startActivityForResult(StoredDirectoryHelper.getPicker(context), + REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER); } + return; + } + + if (askForSavePath) { + final String startPath; if (NewPipeSettings.useStorageAccessFramework(context)) { - StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, - filename, mime); + startPath = null; } else { - File initialSavePath; + final File initialSavePath; if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); } - - initialSavePath = new File(initialSavePath, filename); - startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context, - initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS); + startPath = initialSavePath.getAbsolutePath(); } + startActivityForResult(StoredFileHelper.getNewPicker(context, startPath, + filenameTmp), REQUEST_DOWNLOAD_SAVE_AS); + return; } // check for existing file with the same name - checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); + checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); // remember the last media type downloaded by the user - prefs.edit() - .putString(getString(R.string.last_used_download_type), selectedMediaType) + prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType) .apply(); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 8a235fa8abc..981433e83f7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -7,8 +7,8 @@ import android.content.DialogInterface import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration +import android.net.Uri import android.os.Bundle -import android.os.Environment import android.os.Parcelable import android.view.LayoutInflater import android.view.Menu @@ -52,7 +52,6 @@ import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION -import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE @@ -62,7 +61,7 @@ import org.schabi.newpipe.util.FilePickerActivityHelper import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ShareUtils -import java.io.File +import us.shandian.giga.io.StoredFileHelper import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -188,15 +187,14 @@ class SubscriptionFragment : BaseStateFragment() { } private fun onImportPreviousSelected() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE) + startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_CODE) } private fun onExportSelected() { val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val exportName = "newpipe_subscriptions_$date.json" - val exportFile = File(Environment.getExternalStorageDirectory(), exportName) - startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE) + startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName), REQUEST_EXPORT_CODE) } private fun openReorderDialog() { @@ -207,23 +205,20 @@ class SubscriptionFragment : BaseStateFragment() { super.onActivityResult(requestCode, resultCode, data) if (data != null && data.data != null && resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_EXPORT_CODE) { - val exportFile = Utils.getFileForUri(data.data!!) - val parentFile = exportFile.parentFile!! - if (!parentFile.canWrite() || !parentFile.canRead()) { - Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() - } else { - activity.startService( - Intent(activity, SubscriptionsExportService::class.java) - .putExtra(KEY_FILE_PATH, exportFile.absolutePath) - ) + var uri = data.data!! + if (FilePickerActivityHelper.isOwnFileUri(activity, uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)) } + activity.startService( + Intent(activity, SubscriptionsExportService::class.java) + .putExtra(SubscriptionsExportService.KEY_FILE_PATH, uri) + ) } else if (requestCode == REQUEST_IMPORT_CODE) { - val path = Utils.getFileForUri(data.data!!).absolutePath ImportConfirmationDialog.show( this, Intent(activity, SubscriptionsImportService::class.java) .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) - .putExtra(KEY_VALUE, path) + .putExtra(KEY_VALUE, data.data) ) } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index f0675da1b79..120b8168147 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -18,8 +18,6 @@ import androidx.appcompat.app.ActionBar; import androidx.core.text.util.LinkifyCompat; -import com.nononsenseapps.filepicker.Utils; - import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorActivity; @@ -30,13 +28,13 @@ import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ServiceHelper; import java.util.Collections; import java.util.List; import icepick.State; +import us.shandian.giga.io.StoredFileHelper; import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL; import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; @@ -175,8 +173,7 @@ public void onImportUrl(final String value) { } public void onImportFile() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), - REQUEST_IMPORT_FILE_CODE); + startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_FILE_CODE); } @Override @@ -188,10 +185,10 @@ public void onActivityResult(final int requestCode, final int resultCode, final if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) { - final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) - .putExtra(KEY_MODE, INPUT_STREAM_MODE).putExtra(KEY_VALUE, path) + .putExtra(KEY_MODE, INPUT_STREAM_MODE) + .putExtra(KEY_VALUE, data.getData()) .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 5dfb1bfe5b9..cbf7bc95ea1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -20,7 +20,7 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; -import android.text.TextUtils; +import android.net.Uri; import android.util.Log; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -31,16 +31,17 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; +import org.schabi.newpipe.streams.io.SharpOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; +import us.shandian.giga.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; @@ -55,8 +56,8 @@ public class SubscriptionsExportService extends BaseImportExportService { + ".services.SubscriptionsExportService.EXPORT_COMPLETE"; private Subscription subscription; - private File outFile; - private FileOutputStream outputStream; + private StoredFileHelper outFile; + private OutputStream outputStream; @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { @@ -64,18 +65,18 @@ public int onStartCommand(final Intent intent, final int flags, final int startI return START_NOT_STICKY; } - final String path = intent.getStringExtra(KEY_FILE_PATH); - if (TextUtils.isEmpty(path)) { + final Uri path = intent.getParcelableExtra(KEY_FILE_PATH); + if (path == null) { stopAndReportError(new IllegalStateException( - "Exporting to a file, but the path is empty or null"), + "Exporting to a file, but the path is null"), "Exporting subscriptions"); return START_NOT_STICKY; } try { - outFile = new File(path); - outputStream = new FileOutputStream(outFile); - } catch (final FileNotFoundException e) { + outFile = new StoredFileHelper(this, path, "application/json"); + outputStream = new SharpOutputStream(outFile.getStream()); + } catch (final IOException e) { handleError(e); return START_NOT_STICKY; } @@ -122,8 +123,8 @@ private void startExport() { .subscribe(getSubscriber()); } - private Subscriber getSubscriber() { - return new Subscriber() { + private Subscriber getSubscriber() { + return new Subscriber() { @Override public void onSubscribe(final Subscription s) { subscription = s; @@ -131,7 +132,7 @@ public void onSubscribe(final Subscription s) { } @Override - public void onNext(final File file) { + public void onNext(final StoredFileHelper file) { if (DEBUG) { Log.d(TAG, "startExport() success: file = " + file); } @@ -153,7 +154,7 @@ public void onComplete() { }; } - private Function, File> exportToFile() { + private Function, StoredFileHelper> exportToFile() { return subscriptionItems -> { ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener); return outFile; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index af94934b251..896775d49cc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -20,6 +20,7 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -36,12 +37,10 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.ktx.ExceptionUtils; +import org.schabi.newpipe.streams.io.SharpInputStream; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -53,8 +52,10 @@ import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; +import us.shandian.giga.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; +import static us.shandian.giga.io.StoredFileHelper.DEFAULT_MIME; public class SubscriptionsImportService extends BaseImportExportService { public static final int CHANNEL_URL_MODE = 0; @@ -101,17 +102,18 @@ public int onStartCommand(final Intent intent, final int flags, final int startI if (currentMode == CHANNEL_URL_MODE) { channelUrl = intent.getStringExtra(KEY_VALUE); } else { - final String filePath = intent.getStringExtra(KEY_VALUE); - if (TextUtils.isEmpty(filePath)) { + final Uri uri = intent.getParcelableExtra(KEY_VALUE); + if (uri == null) { stopAndReportError(new IllegalStateException( - "Importing from input stream, but file path is empty or null"), + "Importing from input stream, but file path is null"), "Importing subscriptions"); return START_NOT_STICKY; } try { - inputStream = new FileInputStream(new File(filePath)); - } catch (final FileNotFoundException e) { + inputStream = new SharpInputStream( + new StoredFileHelper(this, uri, DEFAULT_MIME).getStream()); + } catch (final IOException e) { handleError(e); return START_NOT_STICKY; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 3fd44c4d521..e3193ba0fdf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -2,13 +2,15 @@ import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.preference.Preference; @@ -29,10 +31,14 @@ import org.schabi.newpipe.util.ZipHelper; import java.io.File; +import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; + import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class ContentSettingsFragment extends BasePreferenceFragment { @@ -57,24 +63,15 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro addPreferencesFromResource(R.xml.content_settings); final Preference importDataPreference = findPreference(getString(R.string.import_data)); - importDataPreference.setOnPreferenceClickListener(p -> { - final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_FILE); - startActivityForResult(i, REQUEST_IMPORT_PATH); + importDataPreference.setOnPreferenceClickListener((Preference p) -> { + startActivityForResult(StoredFileHelper.getPicker(getContext()), REQUEST_IMPORT_PATH); return true; }); final Preference exportDataPreference = findPreference(getString(R.string.export_data)); - exportDataPreference.setOnPreferenceClickListener(p -> { - final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_DIR); - startActivityForResult(i, REQUEST_EXPORT_PATH); + exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { + startActivityForResult(StoredDirectoryHelper.getPicker(getContext()), + REQUEST_EXPORT_PATH); return true; }); @@ -89,7 +86,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key)); - clearCookiePref.setOnPreferenceClickListener(preference -> { defaultPreferences.edit() .putString(getString(R.string.recaptcha_cookies_key), "").apply(); @@ -152,7 +148,7 @@ public void onDestroy() { @Override public void onActivityResult(final int requestCode, final int resultCode, - @NonNull final Intent data) { + @Nullable final Intent data) { assureCorrectAppLanguage(getContext()); super.onActivityResult(requestCode, resultCode, data); if (DEBUG) { @@ -163,31 +159,44 @@ public void onActivityResult(final int requestCode, final int resultCode, } if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) - && resultCode == Activity.RESULT_OK && data.getData() != null) { - final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); - if (requestCode == REQUEST_EXPORT_PATH) { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); - } else { - final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); - builder.setMessage(R.string.override_current_data) - .setPositiveButton(getString(R.string.finish), - (d, id) -> importDatabase(path)) - .setNegativeButton(android.R.string.cancel, - (d, id) -> d.cancel()); - builder.create().show(); + && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { + try { + Uri uri = data.getData(); + if (FilePickerActivityHelper.isOwnFileUri(requireContext(), uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)); + } + if (requestCode == REQUEST_EXPORT_PATH) { + final StoredDirectoryHelper directory + = new StoredDirectoryHelper(requireContext(), uri, null); + final SimpleDateFormat sdf + = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + exportDatabase(directory.createFile("NewPipeData-" + + sdf.format(new Date()) + ".zip", "application/zip")); + } else { + final StoredFileHelper file = new StoredFileHelper(getContext(), uri, + StoredFileHelper.DEFAULT_MIME); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); + builder.setMessage(R.string.override_current_data) + .setPositiveButton(R.string.finish, + (DialogInterface d, int id) -> importDatabase(file)) + .setNegativeButton(R.string.cancel, + (DialogInterface d, int id) -> d.cancel()); + builder.create().show(); + } + } catch (final IOException e) { + e.printStackTrace(); } } } - private void exportDatabase(final String path) { + private void exportDatabase(final StoredFileHelper file) { try { //checkpoint before export NewPipeDatabase.checkpoint(); final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(requireContext()); - manager.exportDatabase(preferences, path); + manager.exportDatabase(preferences, file); Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); } catch (final Exception e) { @@ -195,9 +204,9 @@ private void exportDatabase(final String path) { } } - private void importDatabase(final String filePath) { + private void importDatabase(final StoredFileHelper file) { // check if file is supported - if (!ZipHelper.isValidZipFile(filePath)) { + if (!ZipHelper.isValidZipFile(file)) { Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) .show(); return; @@ -208,13 +217,13 @@ private void importDatabase(final String filePath) { throw new Exception("Could not create databases dir"); } - if (!manager.extractDb(filePath)) { + if (!manager.extractDb(file)) { Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) .show(); } //If settings file exist, ask if it should be imported. - if (manager.extractSettings(filePath)) { + if (manager.extractSettings(file)) { final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()); alert.setTitle(R.string.import_settings); diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt index 1730a230e04..6c70776ec88 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt @@ -1,7 +1,9 @@ package org.schabi.newpipe.settings import android.content.SharedPreferences +import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.util.ZipHelper +import us.shandian.giga.io.StoredFileHelper import java.io.BufferedOutputStream import java.io.FileInputStream import java.io.FileOutputStream @@ -17,8 +19,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { * It also creates the file. */ @Throws(Exception::class) - fun exportDatabase(preferences: SharedPreferences, outputPath: String) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(outputPath))) + fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) { + file.create() + ZipOutputStream(BufferedOutputStream(SharpOutputStream(file.stream))) .use { outZip -> ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db") @@ -48,8 +51,8 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir() } - fun extractDb(filePath: String): Boolean { - val success = ZipHelper.extractFileFromZip(filePath, fileLocator.db.path, "newpipe.db") + fun extractDb(file: StoredFileHelper): Boolean { + val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db") if (success) { fileLocator.dbJournal.delete() fileLocator.dbWal.delete() @@ -59,9 +62,8 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { return success } - fun extractSettings(filePath: String): Boolean { - return ZipHelper - .extractFileFromZip(filePath, fileLocator.settings.path, "newpipe.settings") + fun extractSettings(file: StoredFileHelper): Boolean { + return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings") } fun loadSharedPreferences(preferences: SharedPreferences) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 91351264466..ab513940dcb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -8,11 +8,11 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; -import android.widget.Toast; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.nononsenseapps.filepicker.Utils; @@ -57,6 +57,14 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro prefPathAudio = findPreference(downloadPathAudioPreference); prefStorageAsk = findPreference(downloadStorageAsk); + final SwitchPreference prefUseSaf = findPreference(storageUseSafPreference); + prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); + prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + prefUseSaf.setEnabled(false); + } + updatePreferencesSummary(); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); @@ -177,8 +185,14 @@ public boolean onPreferenceTreeClick(final Preference preference) { final int request; if (key.equals(storageUseSafPreference)) { - Toast.makeText(getContext(), R.string.download_choose_new_path, - Toast.LENGTH_LONG).show(); + if (!NewPipeSettings.useStorageAccessFramework(ctx)) { + NewPipeSettings.saveDefaultVideoDownloadDirectory(ctx); + NewPipeSettings.saveDefaultAudioDownloadDirectory(ctx); + } else { + defaultPreferences.edit().putString(downloadPathVideoPreference, null) + .putString(downloadPathAudioPreference, null).apply(); + } + updatePreferencesSummary(); return true; } else if (key.equals(downloadPathVideoPreference)) { request = REQUEST_DOWNLOAD_VIDEO_PATH; @@ -188,22 +202,7 @@ public boolean onPreferenceTreeClick(final Preference preference) { return super.onPreferenceTreeClick(preference); } - final Intent i; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && NewPipeSettings.useStorageAccessFramework(ctx)) { - i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | StoredDirectoryHelper.PERMISSION_FLAGS); - } else { - i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_DIR); - } - - startActivityForResult(i, request); + startActivityForResult(StoredDirectoryHelper.getPicker(ctx), request); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 01f51b0b3a3..0203f91073b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.Environment; import androidx.annotation.NonNull; @@ -12,6 +13,8 @@ import java.io.File; import java.util.Set; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + /* * Created by k3b on 07.01.2016. * @@ -65,32 +68,36 @@ public static void initSettings(final Context context) { PreferenceManager.setDefaultValues(context, R.xml.update_settings, true); PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); - getVideoDownloadFolder(context); - getAudioDownloadFolder(context); + saveDefaultVideoDownloadDirectory(context); + saveDefaultAudioDownloadDirectory(context); SettingMigrations.initMigrations(context, isFirstRun); } - private static void getVideoDownloadFolder(final Context context) { - getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); + static void saveDefaultVideoDownloadDirectory(final Context context) { + saveDefaultDirectory(context, R.string.download_path_video_key, + Environment.DIRECTORY_MOVIES); } - private static void getAudioDownloadFolder(final Context context) { - getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); + static void saveDefaultAudioDownloadDirectory(final Context context) { + saveDefaultDirectory(context, R.string.download_path_audio_key, + Environment.DIRECTORY_MUSIC); } - private static void getDir(final Context context, final int keyID, - final String defaultDirectoryName) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(keyID); - final String downloadPath = prefs.getString(key, null); - if ((downloadPath != null) && (!downloadPath.isEmpty())) { - return; - } + private static void saveDefaultDirectory(final Context context, final int keyID, + final String defaultDirectoryName) { + if (!useStorageAccessFramework(context)) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String key = context.getString(keyID); + final String downloadPath = prefs.getString(key, null); + if (!isNullOrEmpty(downloadPath)) { + return; + } - final SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); - spEditor.apply(); + final SharedPreferences.Editor spEditor = prefs.edit(); + spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); + spEditor.apply(); + } } @NonNull @@ -103,10 +110,15 @@ private static String getNewPipeChildFolderPathForDir(final File dir) { } public static boolean useStorageAccessFramework(final Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return true; + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return false; + } + final String key = context.getString(R.string.storage_use_saf); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(key, false); + return prefs.getBoolean(key, true); } - } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 68908fc9248..02e2538c59a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.settings; -import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -41,11 +40,6 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { - - public static void initSettings(final Context context) { - NewPipeSettings.initSettings(context); - } - @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java new file mode 100644 index 00000000000..4b871d1dc13 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java @@ -0,0 +1,48 @@ +package org.schabi.newpipe.streams.io; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +public class SharpInputStream extends InputStream { + private final SharpStream stream; + + public SharpInputStream(final SharpStream stream) throws IOException { + if (!stream.canRead()) { + throw new IOException("SharpStream is not readable"); + } + this.stream = stream; + } + + @Override + public int read() throws IOException { + return stream.read(); + } + + @Override + public int read(@NonNull final byte[] b) throws IOException { + return stream.read(b); + } + + @Override + public int read(@NonNull final byte[] b, final int off, final int len) throws IOException { + return stream.read(b, off, len); + } + + @Override + public long skip(final long n) throws IOException { + return stream.skip(n); + } + + @Override + public int available() { + final long res = stream.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; + } + + @Override + public void close() { + stream.close(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java new file mode 100644 index 00000000000..23a2393a8dd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java @@ -0,0 +1,42 @@ +package org.schabi.newpipe.streams.io; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.OutputStream; + +public class SharpOutputStream extends OutputStream { + private final SharpStream stream; + + public SharpOutputStream(final SharpStream stream) throws IOException { + if (!stream.canWrite()) { + throw new IOException("SharpStream is not writable"); + } + this.stream = stream; + } + + @Override + public void write(final int b) throws IOException { + stream.write((byte) b); + } + + @Override + public void write(@NonNull final byte[] b) throws IOException { + stream.write(b); + } + + @Override + public void write(@NonNull final byte[] b, final int off, final int len) throws IOException { + stream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + stream.flush(); + } + + @Override + public void close() { + stream.close(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 46ec68d9e53..9b4f79047f4 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.streams.io; import java.io.Closeable; +import java.io.Flushable; import java.io.IOException; /** * Based on C#'s Stream class. */ -public abstract class SharpStream implements Closeable { +public abstract class SharpStream implements Closeable, Flushable { public abstract int read() throws IOException; public abstract int read(byte[] buffer) throws IOException; diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 6ede163a3b8..20d8ce30c3d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.util; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Environment; @@ -28,25 +27,6 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { private CustomFilePickerFragment currentFragment; - public static Intent chooseSingleFile(@NonNull final Context context) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); - } - - public static Intent chooseFileToSave(@NonNull final Context context, - @Nullable final String startPath) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) - .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_NEW_FILE); - } - public static boolean isOwnFileUri(@NonNull final Context context, @NonNull final Uri uri) { if (uri.getAuthority() == null) { return false; diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index e2b766bb03e..32964490e4c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -1,15 +1,18 @@ package org.schabi.newpipe.util; +import org.schabi.newpipe.streams.io.SharpInputStream; + import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import us.shandian.giga.io.StoredFileHelper; + /** * Created by Christian Schabesberger on 28.01.18. * Copyright 2018 Christian Schabesberger @@ -59,24 +62,23 @@ public static void addFileToZip(final ZipOutputStream outZip, final String file, } /** - * This will extract data from Zipfiles. + * This will extract data from ZipInputStream. * Caution this will override the original file. * - * @param filePath The path of the zip + * @param zipFile The zip file * @param file The path of the file on the disk where the data should be extracted to. * @param name The path of the file inside the zip. * @return will return true if the file was found within the zip file * @throws Exception */ - public static boolean extractFileFromZip(final String filePath, final String file, + public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file, final String name) throws Exception { try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream( - new FileInputStream(filePath)))) { + new SharpInputStream(zipFile.getStream())))) { final byte[] data = new byte[BUFFER_SIZE]; - boolean found = false; - ZipEntry ze; + while ((ze = inZip.getNextEntry()) != null) { if (ze.getName().equals(name)) { found = true; @@ -102,8 +104,9 @@ public static boolean extractFileFromZip(final String filePath, final String fil } } - public static boolean isValidZipFile(final String filePath) { - try (ZipFile ignored = new ZipFile(filePath)) { + public static boolean isValidZipFile(final StoredFileHelper file) { + try (ZipInputStream ignored = new ZipInputStream(new BufferedInputStream( + new SharpInputStream(file.getStream())))) { return true; } catch (final IOException ioe) { return false; diff --git a/app/src/main/java/us/shandian/giga/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java deleted file mode 100644 index 0d6320b53e4..00000000000 --- a/app/src/main/java/us/shandian/giga/io/SharpInputStream.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package us.shandian.giga.io; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Wrapper for the classic {@link java.io.InputStream} - * - * @author kapodamy - */ -public class SharpInputStream extends InputStream { - - private final SharpStream base; - - public SharpInputStream(SharpStream base) throws IOException { - if (!base.canRead()) { - throw new IOException("The provided stream is not readable"); - } - this.base = base; - } - - @Override - public int read() throws IOException { - return base.read(); - } - - @Override - public int read(@NonNull byte[] bytes) throws IOException { - return base.read(bytes); - } - - @Override - public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { - return base.read(bytes, i, i1); - } - - @Override - public long skip(long l) throws IOException { - return base.skip(l); - } - - @Override - public int available() { - long res = base.available(); - return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; - } - - @Override - public void close() { - base.close(); - } -} diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index 5edc5f3eda4..72193aa1ba8 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -13,6 +13,9 @@ import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; +import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.util.FilePickerActivityHelper; + import java.io.File; import java.io.IOException; import java.net.URI; @@ -287,4 +290,18 @@ static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tr return null; } + public static Intent getPicker(final Context ctx) { + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + return new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_DIR); + } + } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index ad64042f179..4a9c170eb72 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -6,14 +6,18 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.provider.DocumentsContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; -import androidx.fragment.app.Fragment; +import com.nononsenseapps.filepicker.Utils; + +import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.streams.io.SharpStream; +import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; @@ -37,6 +41,19 @@ public class StoredFileHelper implements Serializable { private String srcName; private String srcType; + public StoredFileHelper(final Context context, final Uri uri, final String mime) { + if (FilePickerActivityHelper.isOwnFileUri(context, uri)) { + ioFile = Utils.getFileForUri(uri); + source = Uri.fromFile(ioFile).toString(); + } else { + docFile = DocumentFile.fromSingleUri(context, uri); + source = uri.toString(); + } + + this.context = context; + this.srcType = mime; + } + public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) { this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods @@ -139,22 +156,6 @@ public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Co return instance; } - public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) { - // SAF notes: - // ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files - // ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict - - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType(mime) - .putExtra(Intent.EXTRA_TITLE, filename) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS) - .putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks - - who.startActivityForResult(intent, requestCode); - } - - public SharpStream getStream() throws IOException { invalid(); @@ -383,4 +384,64 @@ private boolean stringMismatch(String str1, String str2) { return !str1.equals(str2); } + + public static Intent getPicker(final Context ctx) { + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + return new Intent(Intent.ACTION_OPEN_DOCUMENT) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .setType("*/*") + .addCategory(Intent.CATEGORY_OPENABLE) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + return new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_FILE); + } + } + + public static Intent getNewPicker(@NonNull final Context ctx, @Nullable final String startPath, + @Nullable final String filename) { + final Intent i; + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + i = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .setType("*/*") + .addCategory(Intent.CATEGORY_OPENABLE) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + + if (startPath != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + i.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(startPath)); + } + if (filename != null) { + i.putExtra(Intent.EXTRA_TITLE, filename); + } + } else { + i = new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_NEW_FILE); + + if (startPath != null || filename != null) { + File fullStartPath; + if (startPath == null) { + fullStartPath = Environment.getExternalStorageDirectory(); + } else { + fullStartPath = new File(startPath); + } + if (filename != null) { + fullStartPath = new File(fullStartPath, filename); + } + i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, + fullStartPath.getAbsolutePath()); + } + } + return i; + } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index c83eec819af..bfb7926dda1 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -242,27 +242,21 @@ private void setAdapterButtons() { private void recoverMission(@NonNull DownloadMission mission) { unsafeMissionTarget = mission; + final String startPath; if (NewPipeSettings.useStorageAccessFramework(mContext)) { - StoredFileHelper.requestSafWithFileCreation( - MissionsFragment.this, - REQUEST_DOWNLOAD_SAVE_AS, - mission.storage.getName(), - mission.storage.getType() - ); - + startPath = null; } else { - File initialSavePath; - if (DownloadManager.TAG_VIDEO.equals(mission.storage.getType())) - initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); - else + final File initialSavePath; + if (DownloadManager.TAG_AUDIO.equals(mission.storage.getType())) { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); - - initialSavePath = new File(initialSavePath, mission.storage.getName()); - startActivityForResult( - FilePickerActivityHelper.chooseFileToSave(mContext, initialSavePath.getAbsolutePath()), - REQUEST_DOWNLOAD_SAVE_AS - ); + } else { + initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); + } + startPath = initialSavePath.getAbsolutePath(); } + + startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath, + mission.storage.getName()), REQUEST_DOWNLOAD_SAVE_AS); } @Override diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb4019bd63..31e03fad66b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -362,6 +362,7 @@ Please waitâ€Ĥ Copied to clipboard Please define a download folder later in settings + No download folder set yet, choose the default download folder now This permission is needed to\nopen in popup mode 1 item deleted. diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 1bc0e84047e..53f9b2c895d 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -3,7 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_category_downloads_title"> - Date: Sun, 14 Jun 2020 19:37:45 +0200 Subject: [PATCH 03/15] Move Stored(File|Directory)Helper into NewPipe --- .../newpipe/download/DownloadDialog.java | 4 +- .../subscription/SubscriptionFragment.kt | 2 +- .../SubscriptionsImportFragment.java | 2 +- .../services/SubscriptionsExportService.java | 2 +- .../services/SubscriptionsImportService.java | 4 +- .../settings/ContentSettingsFragment.java | 4 +- .../settings/DownloadSettingsFragment.java | 2 +- .../streams}/io/StoredDirectoryHelper.java | 171 +++++++------ .../newpipe/streams}/io/StoredFileHelper.java | 230 +++++++++++------- .../org/schabi/newpipe/util/ZipHelper.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 2 +- .../java/us/shandian/giga/get/Mission.java | 12 +- .../giga/get/sqlite/FinishedMissionStore.java | 2 +- .../giga/service/DownloadManager.java | 4 +- .../giga/service/DownloadManagerService.java | 4 +- .../giga/ui/adapter/MissionAdapter.java | 2 +- .../giga/ui/fragment/MissionsFragment.java | 2 +- .../java/us/shandian/giga/util/Utility.java | 2 +- 18 files changed, 269 insertions(+), 184 deletions(-) rename app/src/main/java/{us/shandian/giga => org/schabi/newpipe/streams}/io/StoredDirectoryHelper.java (60%) rename app/src/main/java/{us/shandian/giga => org/schabi/newpipe/streams}/io/StoredFileHelper.java (65%) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 051e9b7524f..4cb597b6e96 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -49,6 +49,8 @@ import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; @@ -68,8 +70,6 @@ import icepick.State; import io.reactivex.rxjava3.disposables.CompositeDisposable; import us.shandian.giga.get.MissionRecoveryInfo; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 981433e83f7..87b1de1e781 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -57,11 +57,11 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE +import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.FilePickerActivityHelper import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ShareUtils -import us.shandian.giga.io.StoredFileHelper import java.text.SimpleDateFormat import java.util.Date import java.util.Locale diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 120b8168147..27786bd4404 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -34,7 +34,7 @@ import java.util.List; import icepick.State; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL; import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index cbf7bc95ea1..06310359706 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -32,6 +32,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.streams.io.SharpOutputStream; +import org.schabi.newpipe.streams.io.StoredFileHelper; import java.io.IOException; import java.io.OutputStream; @@ -41,7 +42,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; -import us.shandian.giga.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 896775d49cc..a843ad77c4a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -38,6 +38,7 @@ import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.streams.io.SharpInputStream; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; @@ -52,10 +53,9 @@ import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; -import us.shandian.giga.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; -import static us.shandian.giga.io.StoredFileHelper.DEFAULT_MIME; +import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME; public class SubscriptionsImportService extends BaseImportExportService { public static final int CHANNEL_URL_MODE = 0; diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index e3193ba0fdf..6e020e368b8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -36,8 +36,8 @@ import java.util.Date; import java.util.Locale; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index ab513940dcb..14883092e8a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -26,7 +26,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import us.shandian.giga.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java similarity index 60% rename from app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java rename to app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index 72193aa1ba8..5bd03e6243d 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -1,6 +1,5 @@ -package us.shandian.giga.io; +package org.schabi.newpipe.streams.io; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -24,10 +23,11 @@ import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; - +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class StoredDirectoryHelper { - public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; private File ioTree; private DocumentFile docTree; @@ -36,7 +36,8 @@ public class StoredDirectoryHelper { private final String tag; - public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { + public StoredDirectoryHelper(@NonNull final Context context, @NonNull final Uri path, + final String tag) throws IOException { this.tag = tag; if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { @@ -48,51 +49,49 @@ public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String try { this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS); - } catch (Exception e) { + } catch (final Exception e) { throw new IOException(e); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { throw new IOException("Storage Access Framework with Directory API is not available"); + } this.docTree = DocumentFile.fromTreeUri(context, path); - if (this.docTree == null) + if (this.docTree == null) { throw new IOException("Failed to create the tree from Uri"); + } } - @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredDirectoryHelper(@NonNull URI location, String tag) { - ioTree = new File(location); - this.tag = tag; - } - - public StoredFileHelper createFile(String filename, String mime) { + public StoredFileHelper createFile(final String filename, final String mime) { return createFile(filename, mime, false); } - public StoredFileHelper createUniqueFile(String name, String mime) { - ArrayList matches = new ArrayList<>(); - String[] filename = splitFilename(name); - String lcFilename = filename[0].toLowerCase(); + public StoredFileHelper createUniqueFile(final String name, final String mime) { + final ArrayList matches = new ArrayList<>(); + final String[] filename = splitFilename(name); + final String lcFilename = filename[0].toLowerCase(); if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - for (File file : ioTree.listFiles()) + for (final 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()) - ); + final Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( + docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())); - String[] projection = {COLUMN_DISPLAY_NAME}; - String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; - ContentResolver cr = context.getContentResolver(); + final String[] projection = new String[]{COLUMN_DISPLAY_NAME}; + final String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; + final ContentResolver cr = context.getContentResolver(); - try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) { + try (Cursor cursor = cr.query(docTreeChildren, projection, selection, + new String[]{lcFilename}, null)) { if (cursor != null) { - while (cursor.moveToNext()) + while (cursor.moveToNext()) { addIfStartWith(matches, lcFilename, cursor.getString(0)); + } } } } @@ -102,7 +101,7 @@ public StoredFileHelper createUniqueFile(String name, String mime) { } else { // check if the filename is in use String lcName = name.toLowerCase(); - for (String testName : matches) { + for (final String testName : matches) { if (testName.equals(lcName)) { lcName = null; break; @@ -110,28 +109,34 @@ public StoredFileHelper createUniqueFile(String name, String mime) { } // check if not in use - if (lcName != null) return createFile(name, mime, true); + 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) + 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); + return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, + false); } - private StoredFileHelper createFile(String filename, String mime, boolean safe) { - StoredFileHelper storage; + private StoredFileHelper createFile(final String filename, final String mime, + final boolean safe) { + final StoredFileHelper storage; try { - if (docTree == null) + if (docTree == null) { storage = new StoredFileHelper(ioTree, filename, mime); - else + } else { storage = new StoredFileHelper(context, docTree, filename, mime, safe); - } catch (IOException e) { + } + } catch (final IOException e) { return null; } @@ -149,7 +154,7 @@ public boolean exists() { } /** - * Indicates whatever if is possible access using the {@code java.io} API + * Indicates whether it's using the {@code java.io} API. * * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework */ @@ -172,7 +177,9 @@ public boolean mkdirs() { return ioTree.exists() || ioTree.mkdirs(); } - if (docTree.exists()) return true; + if (docTree.exists()) { + return true; + } try { DocumentFile parent; @@ -180,14 +187,18 @@ public boolean mkdirs() { while (true) { parent = docTree.getParentFile(); - if (parent == null || child == null) break; - if (parent.exists()) return true; + if (parent == null || child == null) { + break; + } + if (parent.exists()) { + return true; + } parent.createDirectory(child); - child = parent.getName();// for the next iteration + child = parent.getName(); // for the next iteration } - } catch (Exception e) { + } catch (final Exception ignored) { // no more parent directories or unsupported by the storage provider } @@ -198,13 +209,13 @@ public String getTag() { return tag; } - public Uri findFile(String filename) { + public Uri findFile(final String filename) { if (docTree == null) { - File res = new File(ioTree, filename); + final File res = new File(ioTree, filename); return res.exists() ? Uri.fromFile(res) : null; } - DocumentFile res = findFileSAFHelper(context, docTree, filename); + final DocumentFile res = findFileSAFHelper(context, docTree, filename); return res == null ? null : res.getUri(); } @@ -218,72 +229,82 @@ public String toString() { return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString(); } - //////////////////// // Utils /////////////////// - private static void addIfStartWith(ArrayList list, @NonNull String base, String str) { - if (str == null || str.isEmpty()) return; - str = str.toLowerCase(); - if (str.startsWith(base)) list.add(str); + private static void addIfStartWith(final ArrayList list, @NonNull final String base, + final String str) { + if (isNullOrEmpty(str)) { + return; + } + final String lowerStr = str.toLowerCase(); + if (lowerStr.startsWith(base)) { + list.add(lowerStr); + } } - private static String[] splitFilename(@NonNull String filename) { - int dotIndex = filename.lastIndexOf('.'); + private static String[] splitFilename(@NonNull final String filename) { + final int dotIndex = filename.lastIndexOf('.'); - if (dotIndex < 0 || (dotIndex == filename.length() - 1)) + 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) { + private static String makeFileName(final String name, final int idx, final String ext) { return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); } /** - * Fast (but not enough) file/directory finder under the storage access framework + * 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 DocumentFile} contain the reference, otherwise, null */ - static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) { + static DocumentFile findFileSAFHelper(@Nullable final Context context, final DocumentFile tree, + final String filename) { if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return tree.findFile(filename);// warning: this is very slow + return tree.findFile(filename); // warning: this is very slow } - if (!tree.canRead()) return null;// missing read permission + 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 ?%"; + final String selection = COLUMN_DISPLAY_NAME + " = ?"; + //final 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(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(tree.getUri(), + DocumentsContract.getDocumentId(tree.getUri())); + final String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; + final ContentResolver contentResolver = context.getContentResolver(); - filename = filename.toLowerCase(); + final String lowerFilename = filename.toLowerCase(); - try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) { - if (cursor == null) return null; + try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, + new String[]{lowerFilename}, null)) { + if (cursor == null) { + return null; + } while (cursor.moveToNext()) { - if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename)) + if (cursor.isNull(name) + || !cursor.getString(name).toLowerCase().startsWith(lowerFilename)) { continue; + } - return DocumentFile.fromSingleUri( - context, DocumentsContract.buildDocumentUriUsingTree( - tree.getUri(), cursor.getString(documentId) - ) - ); + return DocumentFile.fromSingleUri(context, + DocumentsContract.buildDocumentUriUsingTree(tree.getUri(), + cursor.getString(documentId))); } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java similarity index 65% rename from app/src/main/java/us/shandian/giga/io/StoredFileHelper.java rename to app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index 4a9c170eb72..d2b69424771 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -1,4 +1,4 @@ -package us.shandian.giga.io; +package org.schabi.newpipe.streams.io; import android.annotation.TargetApi; import android.content.ContentResolver; @@ -16,7 +16,6 @@ import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.settings.NewPipeSettings; -import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; @@ -24,6 +23,9 @@ import java.io.Serializable; import java.net.URI; +import us.shandian.giga.io.FileStream; +import us.shandian.giga.io.FileStreamSAF; + public class StoredFileHelper implements Serializable { private static final long serialVersionUID = 0L; public static final String DEFAULT_MIME = "application/octet-stream"; @@ -54,27 +56,34 @@ public StoredFileHelper(final Context context, final Uri uri, final String mime) this.srcType = mime; } - public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) { - this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods + public StoredFileHelper(@Nullable final Uri parent, final String filename, final String mime, + final 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(); + if (parent != null) { + this.sourceTree = parent.toString(); + } this.tag = tag; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException { + StoredFileHelper(@Nullable final Context context, final DocumentFile tree, + final String filename, final String mime, final boolean safe) + throws IOException { this.docTree = tree; this.context = context; - DocumentFile res; + final DocumentFile res; 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"); + if (res == null) { + throw new IOException("Cannot create the file"); + } } else { res = createSAF(context, mime, filename); } @@ -88,15 +97,19 @@ public StoredFileHelper(@Nullable Uri parent, String filename, String mime, Stri this.srcType = this.docFile.getType(); } - StoredFileHelper(File location, String filename, String mime) throws IOException { + StoredFileHelper(final File location, final String filename, final String mime) + throws IOException { this.ioFile = new File(location, filename); if (this.ioFile.exists()) { - if (!this.ioFile.isFile() && !this.ioFile.delete()) - throw new IOException("The filename is already in use by non-file entity and cannot overwrite it"); + if (!this.ioFile.isFile() && !this.ioFile.delete()) { + throw new IOException("The filename is already in use by non-file entity " + + "and cannot overwrite it"); + } } else { - if (!this.ioFile.createNewFile()) + if (!this.ioFile.createNewFile()) { throw new IOException("Cannot create the file"); + } } this.source = Uri.fromFile(this.ioFile).toString(); @@ -107,16 +120,20 @@ public StoredFileHelper(@Nullable Uri parent, String filename, String mime, Stri } @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException { + public StoredFileHelper(final Context context, @Nullable final Uri parent, + @NonNull final Uri path, final String tag) throws IOException { this.tag = tag; this.source = path.toString(); - if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) { + 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); + final DocumentFile file = DocumentFile.fromSingleUri(context, path); - if (file == null) throw new RuntimeException("SAF not available"); + if (file == null) { + throw new RuntimeException("SAF not available"); + } this.context = context; @@ -130,8 +147,9 @@ public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path } if (parent != null) { - if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) + if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) { this.docTree = DocumentFile.fromTreeUri(context, parent); + } this.sourceTree = parent.toString(); } @@ -141,37 +159,45 @@ public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path } - public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { - Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree); + public static StoredFileHelper deserialize(@NonNull final StoredFileHelper storage, + final Context context) throws IOException { + final Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree); - if (storage.isInvalid()) + if (storage.isInvalid()) { return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag); + } - StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag); + final StoredFileHelper instance = new StoredFileHelper(context, treeUri, + Uri.parse(storage.source), storage.tag); // 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; + if (instance.srcName == null) { + instance.srcName = storage.srcName; + } + if (instance.srcType == null) { + instance.srcType = storage.srcType; + } return instance; } public SharpStream getStream() throws IOException { - invalid(); + assertValid(); - if (docFile == null) + if (docFile == null) { return new FileStream(ioFile); - else + } else { return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); + } } /** - * Indicates whatever if is possible access using the {@code java.io} API + * Indicates whether it's using the {@code java.io} API. * * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework */ public boolean isDirect() { - invalid(); + assertValid(); return docFile == null; } @@ -181,19 +207,19 @@ public boolean isInvalid() { } public Uri getUri() { - invalid(); + assertValid(); return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); } public Uri getParentUri() { - invalid(); + assertValid(); return sourceTree == null ? null : Uri.parse(sourceTree); } public void truncate() throws IOException { - invalid(); + assertValid(); try (SharpStream fs = getStream()) { fs.setLength(0); @@ -201,16 +227,20 @@ public void truncate() throws IOException { } public boolean delete() { - if (source == null) return true; - if (docFile == null) return ioFile.delete(); - + if (source == null) { + return true; + } + if (docFile == null) { + return ioFile.delete(); + } - boolean res = docFile.delete(); + final boolean res = docFile.delete(); try { - int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + final int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags); - } catch (Exception ex) { + } catch (final Exception ex) { // nothing to do } @@ -218,31 +248,35 @@ public boolean delete() { } public long length() { - invalid(); + assertValid(); return docFile == null ? ioFile.length() : docFile.length(); } public boolean canWrite() { - if (source == null) return false; + if (source == null) { + return false; + } return docFile == null ? ioFile.canWrite() : docFile.canWrite(); } public String getName() { - if (source == null) + if (source == null) { return srcName; - else if (docFile == null) + } else if (docFile == null) { return ioFile.getName(); + } - String name = docFile.getName(); + final String name = docFile.getName(); return name == null ? srcName : name; } public String getType() { - if (source == null || docFile == null) + if (source == null || docFile == null) { return srcType; + } - String type = docFile.getType(); + final String type = docFile.getType(); return type == null ? srcType : type; } @@ -251,34 +285,41 @@ public String getTag() { } public boolean existsAsFile() { - if (source == null) return false; + if (source == null) { + return false; + } // WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow - boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); - boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ÂżdocFile.isVirtual() means is no-physical? + final boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); + // ÂżdocFile.isVirtual() means is no-physical? + final boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile(); return exists && isFile; } public boolean create() { - invalid(); - boolean result; + assertValid(); + final boolean result; if (docFile == null) { try { result = ioFile.createNewFile(); - } catch (IOException e) { + } catch (final IOException e) { return false; } } else if (docTree == null) { result = false; } else { - if (!docTree.canRead() || !docTree.canWrite()) return false; + if (!docTree.canRead() || !docTree.canWrite()) { + return false; + } try { docFile = createSAF(context, srcType, srcName); - if (docFile.getName() == null) return false; + if (docFile.getName() == null) { + return false; + } result = true; - } catch (IOException e) { + } catch (final IOException e) { return false; } } @@ -293,7 +334,9 @@ public boolean create() { } public void invalidate() { - if (source == null) return; + if (source == null) { + return; + } srcName = getName(); srcType = getType(); @@ -306,81 +349,102 @@ public void invalidate() { context = null; } - public boolean equals(StoredFileHelper storage) { - if (this == storage) return true; + public boolean equals(final StoredFileHelper storage) { + 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))) + if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree))) { return false; + } if (this.isInvalid() || storage.isInvalid()) { - if (this.srcName == null || storage.srcName == null || this.srcType == null || storage.srcType == null) return false; - return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType); + if (this.srcName == null || storage.srcName == null || this.srcType == null + || storage.srcType == null) { + return false; + } + + return this.srcName.equalsIgnoreCase(storage.srcName) + && this.srcType.equalsIgnoreCase(storage.srcType); } - if (this.isDirect() != storage.isDirect()) return false; + if (this.isDirect() != storage.isDirect()) { + return false; + } - if (this.isDirect()) + if (this.isDirect()) { return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); + } - return DocumentsContract.getDocumentId( - this.docFile.getUri() - ).equalsIgnoreCase(DocumentsContract.getDocumentId( - storage.docFile.getUri() - )); + return DocumentsContract.getDocumentId(this.docFile.getUri()) + .equalsIgnoreCase(DocumentsContract.getDocumentId(storage.docFile.getUri())); } @NonNull @Override public String toString() { - if (source == null) + if (source == null) { return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag; - else - return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag; + } else { + return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + + " tag=" + tag; + } } - private void invalid() { - if (source == null) + private void assertValid() { + 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); + context.getContentResolver().takePersistableUriPermission(docFile.getUri(), + StoredDirectoryHelper.PERMISSION_FLAGS); + } catch (final Exception e) { + if (docFile.getName() == null) { + throw new IOException(e); + } } } @NonNull - private DocumentFile createSAF(@Nullable Context context, String mime, String filename) - throws IOException { - DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename); + private DocumentFile createSAF(@Nullable final Context ctx, final String mime, + final String filename) throws IOException { + DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(ctx, docTree, filename); if (res != null && res.exists() && res.isDirectory()) { - if (!res.delete()) + 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"); + if (res == null) { + throw new IOException("Cannot create the file"); + } } return res; } - private String getLowerCase(String str) { + private String getLowerCase(final 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; + private boolean stringMismatch(final String str1, final String str2) { + if (str1 == null && str2 == null) { + return false; + } + if ((str1 == null) != (str2 == null)) { + return true; + } return !str1.equals(str2); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index 32964490e4c..bc08e6197fb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -11,7 +11,7 @@ import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; /** * Created by Christian Schabesberger on 28.01.18. diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 2b3faa3e050..628058f5502 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -26,7 +26,7 @@ import javax.net.ssl.SSLException; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.util.Utility; diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ecb0eaebd5f..77b9c1e3397 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -5,7 +5,7 @@ import java.io.Serializable; import java.util.Calendar; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; public abstract class Mission implements Serializable { private static final long serialVersionUID = 1L;// last bump: 27 march 2019 @@ -25,6 +25,10 @@ public abstract class Mission implements Serializable { */ public long timestamp; + public long getTimestamp() { + return timestamp; + } + /** * pre-defined content type */ @@ -35,10 +39,6 @@ public abstract class Mission implements Serializable { */ public StoredFileHelper storage; - public long getTimestamp() { - return timestamp; - } - /** * Delete the downloaded file * @@ -57,7 +57,7 @@ public boolean delete() { @NonNull @Override public String toString() { - Calendar calendar = Calendar.getInstance(); + final Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timestamp); return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 15c45c6fd31..704385212ab 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -17,7 +17,7 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; /** * SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 8359fce9aa8..283517e014a 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -19,8 +19,8 @@ import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.sqlite.FinishedMissionStore; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 568c3497add..52c28828d5d 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -47,8 +47,8 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.MissionRecoveryInfo; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager.NetworkState; diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 41a254b4976..45ee290f6f9 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -61,7 +61,7 @@ import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.MissionRecoveryInfo; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index bfb7926dda1..ac3579b797f 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -36,7 +36,7 @@ import java.io.IOException; import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index ab584f0e6c7..9e6787d5d8a 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -29,7 +29,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Locale; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; public class Utility { From cb4e6159c43e3b8d18b0119aa68ff580e4f82c60 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Mon, 15 Jun 2020 16:26:52 +0200 Subject: [PATCH 04/15] Use file picker for export DB --- .../settings/ContentSettingsFragment.java | 47 ++++++++----------- .../settings/ContentSettingsManager.kt | 2 +- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 6e020e368b8..e564073b83d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -36,7 +36,6 @@ import java.util.Date; import java.util.Locale; -import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; @@ -70,8 +69,9 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro final Preference exportDataPreference = findPreference(getString(R.string.export_data)); exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { - startActivityForResult(StoredDirectoryHelper.getPicker(getContext()), - REQUEST_EXPORT_PATH); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + startActivityForResult(StoredFileHelper.getNewPicker(getContext(), null, + "NewPipeData-" + sdf.format(new Date()) + ".zip"), REQUEST_EXPORT_PATH); return true; }); @@ -160,31 +160,22 @@ public void onActivityResult(final int requestCode, final int resultCode, if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - try { - Uri uri = data.getData(); - if (FilePickerActivityHelper.isOwnFileUri(requireContext(), uri)) { - uri = Uri.fromFile(Utils.getFileForUri(uri)); - } - if (requestCode == REQUEST_EXPORT_PATH) { - final StoredDirectoryHelper directory - = new StoredDirectoryHelper(requireContext(), uri, null); - final SimpleDateFormat sdf - = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - exportDatabase(directory.createFile("NewPipeData-" - + sdf.format(new Date()) + ".zip", "application/zip")); - } else { - final StoredFileHelper file = new StoredFileHelper(getContext(), uri, - StoredFileHelper.DEFAULT_MIME); - final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); - builder.setMessage(R.string.override_current_data) - .setPositiveButton(R.string.finish, - (DialogInterface d, int id) -> importDatabase(file)) - .setNegativeButton(R.string.cancel, - (DialogInterface d, int id) -> d.cancel()); - builder.create().show(); - } - } catch (final IOException e) { - e.printStackTrace(); + Uri uri = data.getData(); + if (FilePickerActivityHelper.isOwnFileUri(requireActivity(), uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)); + } + final StoredFileHelper file = new StoredFileHelper(getContext(), uri, + "application/zip"); + if (requestCode == REQUEST_EXPORT_PATH) { + exportDatabase(file); + } else { + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); + builder.setMessage(R.string.override_current_data) + .setPositiveButton(R.string.finish, + (DialogInterface d, int id) -> importDatabase(file)) + .setNegativeButton(R.string.cancel, + (DialogInterface d, int id) -> d.cancel()); + builder.create().show(); } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt index 6c70776ec88..cb4c1479659 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt @@ -2,8 +2,8 @@ package org.schabi.newpipe.settings import android.content.SharedPreferences import org.schabi.newpipe.streams.io.SharpOutputStream +import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper -import us.shandian.giga.io.StoredFileHelper import java.io.BufferedOutputStream import java.io.FileInputStream import java.io.FileOutputStream From febb21a01de48923450cb3b04886e283ba4c9bd1 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Thu, 30 Jul 2020 16:01:08 +0200 Subject: [PATCH 05/15] Fix non-SAF actions --- .../local/subscription/SubscriptionFragment.kt | 9 +-------- .../settings/ContentSettingsFragment.java | 16 ++++------------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 87b1de1e781..4f3128aaa29 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -7,7 +7,6 @@ import android.content.DialogInterface import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration -import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -21,7 +20,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager -import com.nononsenseapps.filepicker.Utils import com.xwray.groupie.Group import com.xwray.groupie.GroupAdapter import com.xwray.groupie.Item @@ -58,7 +56,6 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE import org.schabi.newpipe.streams.io.StoredFileHelper -import org.schabi.newpipe.util.FilePickerActivityHelper import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ShareUtils @@ -205,13 +202,9 @@ class SubscriptionFragment : BaseStateFragment() { super.onActivityResult(requestCode, resultCode, data) if (data != null && data.data != null && resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_EXPORT_CODE) { - var uri = data.data!! - if (FilePickerActivityHelper.isOwnFileUri(activity, uri)) { - uri = Uri.fromFile(Utils.getFileForUri(uri)) - } activity.startService( Intent(activity, SubscriptionsExportService::class.java) - .putExtra(SubscriptionsExportService.KEY_FILE_PATH, uri) + .putExtra(SubscriptionsExportService.KEY_FILE_PATH, data.data) ) } else if (requestCode == REQUEST_IMPORT_CODE) { ImportConfirmationDialog.show( diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index e564073b83d..c9b36e2a711 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -5,7 +5,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; @@ -16,7 +15,6 @@ import androidx.preference.Preference; import androidx.preference.PreferenceManager; -import com.nononsenseapps.filepicker.Utils; import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.DownloaderImpl; @@ -27,17 +25,14 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; -import org.schabi.newpipe.util.FilePickerActivityHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.ZipHelper; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -import org.schabi.newpipe.streams.io.StoredFileHelper; - import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class ContentSettingsFragment extends BasePreferenceFragment { @@ -147,7 +142,8 @@ public void onDestroy() { } @Override - public void onActivityResult(final int requestCode, final int resultCode, + public void onActivityResult(final int requestCode, + final int resultCode, @Nullable final Intent data) { assureCorrectAppLanguage(getContext()); super.onActivityResult(requestCode, resultCode, data); @@ -160,11 +156,7 @@ public void onActivityResult(final int requestCode, final int resultCode, if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - Uri uri = data.getData(); - if (FilePickerActivityHelper.isOwnFileUri(requireActivity(), uri)) { - uri = Uri.fromFile(Utils.getFileForUri(uri)); - } - final StoredFileHelper file = new StoredFileHelper(getContext(), uri, + final StoredFileHelper file = new StoredFileHelper(getContext(), data.getData(), "application/zip"); if (requestCode == REQUEST_EXPORT_PATH) { exportDatabase(file); From 7efe62ee80fa4f889f20b11a6110a3b363274206 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 2 Aug 2020 11:23:40 +0200 Subject: [PATCH 06/15] Only ask for storage permissions when not using SAF --- .../main/java/org/schabi/newpipe/util/PermissionHelper.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 03400bdbb0b..c64631b7266 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat; import org.schabi.newpipe.R; +import org.schabi.newpipe.settings.NewPipeSettings; public final class PermissionHelper { public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778; @@ -26,6 +27,10 @@ public final class PermissionHelper { private PermissionHelper() { } public static boolean checkStoragePermissions(final Activity activity, final int requestCode) { + if (NewPipeSettings.useStorageAccessFramework(activity)) { + return true; // Storage permissions are not needed for SAF + } + if (!checkReadStoragePermissions(activity, requestCode)) { return false; } From 1e8b3826dc7f1574655954bad1390c999fd0dc71 Mon Sep 17 00:00:00 2001 From: Stypox Date: Thu, 14 Jan 2021 21:40:16 +0100 Subject: [PATCH 07/15] Add setting migration to promote using SAF --- .../settings/DownloadSettingsFragment.java | 7 ++++--- .../newpipe/settings/SettingMigrations.java | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 14883092e8a..77b9f3083bd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -9,10 +9,11 @@ import android.os.Bundle; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; import com.nononsenseapps.filepicker.Utils; @@ -57,7 +58,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro prefPathAudio = findPreference(downloadPathAudioPreference); prefStorageAsk = findPreference(downloadStorageAsk); - final SwitchPreference prefUseSaf = findPreference(storageUseSafPreference); + final SwitchPreferenceCompat prefUseSaf = findPreference(storageUseSafPreference); prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q @@ -84,7 +85,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro } @Override - public void onAttach(final Context context) { + public void onAttach(@NonNull final Context context) { super.onAttach(context); ctx = context; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java index c5974642831..d6aade168f0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.util.Log; import androidx.preference.PreferenceManager; @@ -18,7 +19,7 @@ public final class SettingMigrations { /** * Version number for preferences. Must be incremented every time a migration is necessary. */ - public static final int VERSION = 2; + public static final int VERSION = 3; private static SharedPreferences sp; public static final Migration MIGRATION_0_1 = new Migration(0, 1) { @@ -54,6 +55,20 @@ protected void migrate(final Context context) { } }; + public static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + protected void migrate(final Context context) { + // Storage Access Framework implementation was improved in #5415, allowing the modern + // and standard way to access folders and files to be used consistently everywhere. + // We reset the setting to its default value, i.e. "use SAF", since now there are no + // more issues with SAF and users should use that one instead of the old + // NoNonsenseFilePicker. SAF does not work on KitKat and below, though, so the setting + // is set to false in that case. + sp.edit().putBoolean(context.getString(R.string.storage_use_saf), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP).apply(); + } + }; + /** * List of all implemented migrations. *

@@ -62,7 +77,8 @@ protected void migrate(final Context context) { */ private static final Migration[] SETTING_MIGRATIONS = { MIGRATION_0_1, - MIGRATION_1_2 + MIGRATION_1_2, + MIGRATION_2_3 }; From 6a0c5a874cd87a39fd33bf96251813884845074b Mon Sep 17 00:00:00 2001 From: Stypox Date: Thu, 14 Jan 2021 22:04:40 +0100 Subject: [PATCH 08/15] Fix ContentSettingsManager tests --- .../settings/ContentSettingsManagerTest.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt index 3c59c29b35f..2311b41ce78 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt @@ -17,6 +17,8 @@ import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify import org.mockito.Mockito.withSettings import org.mockito.junit.MockitoJUnitRunner +import org.schabi.newpipe.streams.io.StoredFileHelper +import us.shandian.giga.io.FileStream import java.io.File import java.io.ObjectInputStream import java.nio.file.Files @@ -30,10 +32,12 @@ class ContentSettingsManagerTest { } private lateinit var fileLocator: NewPipeFileLocator + private lateinit var storedFileHelper: StoredFileHelper @Before fun setupFileLocator() { fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly()) + storedFileHelper = Mockito.mock(StoredFileHelper::class.java, withSettings().stubOnly()) } @Test @@ -44,11 +48,13 @@ class ContentSettingsManagerTest { `when`(fileLocator.settings).thenReturn(newpipeSettings) val expectedPreferences = mapOf("such pref" to "much wow") - val sharedPreferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) + val sharedPreferences = + Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) `when`(sharedPreferences.all).thenReturn(expectedPreferences) val output = File.createTempFile("newpipe_", "") - ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, output.absolutePath) + `when`(storedFileHelper.stream).thenReturn(FileStream(output)) + ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper) val zipFile = ZipFile(output) val entries = zipFile.entries().toList() @@ -117,7 +123,8 @@ class ContentSettingsManagerTest { `when`(fileLocator.dbWal).thenReturn(dbWal) val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) - val success = ContentSettingsManager(fileLocator).extractDb(zip.path) + `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) + val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper) assertTrue(success) assertFalse(dbJournal.exists()) @@ -135,7 +142,8 @@ class ContentSettingsManagerTest { `when`(fileLocator.db).thenReturn(db) val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) - val success = ContentSettingsManager(fileLocator).extractDb(emptyZip.path) + `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) + val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper) assertFalse(success) assertTrue(dbJournal.exists()) @@ -150,7 +158,8 @@ class ContentSettingsManagerTest { `when`(fileLocator.settings).thenReturn(settings) val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) - val contains = ContentSettingsManager(fileLocator).extractSettings(zip.path) + `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) + val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper) assertTrue(contains) } @@ -161,7 +170,8 @@ class ContentSettingsManagerTest { `when`(fileLocator.settings).thenReturn(settings) val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) - val contains = ContentSettingsManager(fileLocator).extractSettings(emptyZip.path) + `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) + val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper) assertFalse(contains) } From bcb1cf660369511fd79a0abb6cd3310bc00169b4 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 16 Jan 2021 14:10:24 +0100 Subject: [PATCH 09/15] Improve SAF switch descriptions in settings --- .../newpipe/settings/DownloadSettingsFragment.java | 10 ++++++---- app/src/main/res/values/strings.xml | 10 ++++++---- app/src/main/res/xml/download_settings.xml | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 77b9f3083bd..a882acf3ba0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -64,15 +64,17 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { prefUseSaf.setEnabled(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29); + } else { + prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_19); + } + prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice); } updatePreferencesSummary(); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary); - } - if (hasInvalidPath(downloadPathVideoPreference) || hasInvalidPath(downloadPathAudioPreference)) { updatePreferencesSummary(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31e03fad66b..4be0519c858 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -641,10 +641,12 @@ Start downloads Pause downloads Ask where to download - You will be asked where to save each download - You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card - Use SAF - The \'Storage Access Framework\' allows downloads to an external SD card.\nSome devices are incompatible + You will be asked where to save each download.\nEnable the system folder picker (SAF) if you want to download to an external SD card + You will be asked where to save each download + Use system folder picker (SAF) + The \'Storage Access Framework\' allows downloads to an external SD card + The \'Storage Access Framework\' is not supported on Android KitKat and below + Starting from Android 10 only \'Storage Access Framework\' is supported Choose an instance App language System default diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 53f9b2c895d..0912f546ffb 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -3,10 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_category_downloads_title"> - From eea43d5a73ac1c2d52c5d6ac64818f971006e3bf Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 16 Jan 2021 14:35:15 +0100 Subject: [PATCH 10/15] Add comments to SharpStreams --- .../org/schabi/newpipe/streams/io/SharpInputStream.java | 4 ++++ .../org/schabi/newpipe/streams/io/SharpOutputStream.java | 4 ++++ .../java/org/schabi/newpipe/streams/io/SharpStream.java | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java index 4b871d1dc13..956e9865cef 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java @@ -5,6 +5,10 @@ import java.io.IOException; import java.io.InputStream; +/** + * Simply wraps a readable {@link SharpStream} allowing it to be used with built-in Java stuff that + * supports {@link InputStream}. + */ public class SharpInputStream extends InputStream { private final SharpStream stream; diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java index 23a2393a8dd..76e3943123b 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java @@ -5,6 +5,10 @@ import java.io.IOException; import java.io.OutputStream; +/** + * Simply wraps a writable {@link SharpStream} allowing it to be used with built-in Java stuff that + * supports {@link OutputStream}. + */ public class SharpOutputStream extends OutputStream { private final SharpStream stream; diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 9b4f79047f4..849c7c05104 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -5,7 +5,14 @@ import java.io.IOException; /** - * Based on C#'s Stream class. + * Based on C#'s Stream class. SharpStream is a wrapper around the 2 different APIs for SAF + * ({@link us.shandian.giga.io.FileStreamSAF}) and non-SAF ({@link us.shandian.giga.io.FileStream}). + * It has both input and output like in C#, while in Java those are usually different classes. + * {@link SharpInputStream} and {@link SharpOutputStream} are simple classes that wrap + * {@link SharpStream} and extend respectively {@link java.io.InputStream} and + * {@link java.io.OutputStream}, since unfortunately a class can only extend one class, so that a + * sharp stream can be used with built-in Java stuff that supports {@link java.io.InputStream} + * or {@link java.io.OutputStream}. */ public abstract class SharpStream implements Closeable, Flushable { public abstract int read() throws IOException; From 114dc8ffa08d25c130e27af25ed24ad5f84e58cc Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 16 Jan 2021 15:15:16 +0100 Subject: [PATCH 11/15] Pass mime type so that SAF treats file extension correctly --- .../java/org/schabi/newpipe/download/DownloadDialog.java | 2 +- .../newpipe/local/subscription/SubscriptionFragment.kt | 2 +- .../schabi/newpipe/settings/ContentSettingsFragment.java | 3 ++- .../org/schabi/newpipe/streams/io/StoredFileHelper.java | 8 +++++--- .../us/shandian/giga/ui/fragment/MissionsFragment.java | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 4cb597b6e96..d3b57631fdd 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -717,7 +717,7 @@ private void prepareSelectedDownload() { } startActivityForResult(StoredFileHelper.getNewPicker(context, startPath, - filenameTmp), REQUEST_DOWNLOAD_SAVE_AS); + filenameTmp, mimeTmp), REQUEST_DOWNLOAD_SAVE_AS); return; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 4f3128aaa29..d6c08a269ab 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -191,7 +191,7 @@ class SubscriptionFragment : BaseStateFragment() { val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val exportName = "newpipe_subscriptions_$date.json" - startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName), REQUEST_EXPORT_CODE) + startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName, "application/json"), REQUEST_EXPORT_CODE) } private fun openReorderDialog() { diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index c9b36e2a711..45325dc6cae 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -66,7 +66,8 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); startActivityForResult(StoredFileHelper.getNewPicker(getContext(), null, - "NewPipeData-" + sdf.format(new Date()) + ".zip"), REQUEST_EXPORT_PATH); + "NewPipeData-" + sdf.format(new Date()) + ".zip", "application/zip"), + REQUEST_EXPORT_PATH); return true; }); diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index d2b69424771..893114dc15b 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -467,13 +467,15 @@ public static Intent getPicker(final Context ctx) { } } - public static Intent getNewPicker(@NonNull final Context ctx, @Nullable final String startPath, - @Nullable final String filename) { + public static Intent getNewPicker(@NonNull final Context ctx, + @Nullable final String startPath, + @Nullable final String filename, + @NonNull final String mimeType) { final Intent i; if (NewPipeSettings.useStorageAccessFramework(ctx)) { i = new Intent(Intent.ACTION_CREATE_DOCUMENT) .putExtra("android.content.extra.SHOW_ADVANCED", true) - .setType("*/*") + .setType(mimeType) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index ac3579b797f..c56587c55d0 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -256,7 +256,7 @@ private void recoverMission(@NonNull DownloadMission mission) { } startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath, - mission.storage.getName()), REQUEST_DOWNLOAD_SAVE_AS); + mission.storage.getName(), mission.storage.getType()), REQUEST_DOWNLOAD_SAVE_AS); } @Override From b78ac7d2e9a2a480a927ab9af661bb0857b365df Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 16 Jan 2021 15:46:50 +0100 Subject: [PATCH 12/15] Fix strange behaviour when app loses access to saf download folder --- .../java/org/schabi/newpipe/download/DownloadDialog.java | 7 +++++-- .../schabi/newpipe/streams/io/StoredDirectoryHelper.java | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index d3b57631fdd..72dc2fdde80 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -682,12 +682,15 @@ private void prepareSelectedDownload() { throw new RuntimeException("No stream selected"); } - if (!askForSavePath && (mainStorage == null || (mainStorage.isDirect() - == NewPipeSettings.useStorageAccessFramework(context)))) { + if (!askForSavePath + && (mainStorage == null + || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context) + || mainStorage.isInvalidSafStorage())) { // Pick new download folder if one of: // - Download folder is not set // - Download folder uses SAF while SAF is disabled // - Download folder doesn't use SAF while SAF is enabled + // - Download folder uses SAF but the user manually revoked access to it Toast.makeText(context, getString(R.string.no_dir_yet), Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index 5bd03e6243d..feca89f0272 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -223,6 +223,15 @@ public boolean canWrite() { return docTree == null ? ioTree.canWrite() : docTree.canWrite(); } + /** + * @return {@code false} if the storage is direct, or the SAF storage is valid; {@code true} if + * SAF access to this SAF storage is denied (e.g. the user clicked on {@code Android settings -> + * Apps & notifications -> NewPipe -> Storage & cache -> Clear access}); + */ + public boolean isInvalidSafStorage() { + return docTree != null && docTree.getName() == null; + } + @NonNull @Override public String toString() { From 21b8df03759e555d3daeae1d7ac8a61a867403ae Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 17 Mar 2021 20:28:15 +0100 Subject: [PATCH 13/15] Check if file really exists before asking user to overwrite it --- .../newpipe/download/DownloadDialog.java | 15 ++++++------- .../newpipe/streams/io/StoredFileHelper.java | 9 ++++---- .../giga/service/DownloadManager.java | 22 ++++++++++++++++--- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 72dc2fdde80..d9a897d7360 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -757,15 +757,14 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, return; } - // check if is our file + // get state of potential mission referring to the same file final MissionState state = downloadManager.checkForExistingMission(storage); - @StringRes - final int msgBtn; - @StringRes - final int msgBody; + @StringRes final int msgBtn; + @StringRes final int msgBody; + // this switch checks if there is already a mission referring to the same file switch (state) { - case Finished: + case Finished: // there is already a finished mission msgBtn = R.string.overwrite; msgBody = R.string.overwrite_finished_warning; break; @@ -777,7 +776,7 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, msgBtn = R.string.generate_unique_name; msgBody = R.string.download_already_running; break; - case None: + case None: // there is no mission referring to the same file if (mainStorage == null) { // This part is called if: // * using SAF on older android version @@ -812,7 +811,7 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, msgBody = R.string.overwrite_unrelated_warning; break; default: - return; + return; // unreachable } final AlertDialog.Builder askDialog = new AlertDialog.Builder(context) diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index 893114dc15b..0fcad29589d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -290,11 +290,10 @@ public boolean existsAsFile() { } // WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow - final boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); - // ÂżdocFile.isVirtual() means is no-physical? - final boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile(); - - return exists && isFile; + // docFile.isVirtual() means it is non-physical? + return docFile == null + ? (ioFile.exists() && ioFile.isFile()) + : (docFile.exists() && docFile.isFile()); } public boolean create() { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 283517e014a..a2811e72eb4 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -106,7 +106,8 @@ private static boolean testDir(@Nullable File dir) { } /** - * Loads finished missions from the data source + * Loads finished missions from the data source and forgets finished missions whose file does + * not exist anymore. */ private ArrayList loadFinishedMissions() { ArrayList finishedMissions = mFinishedMissionStore.loadFinishedMissions(); @@ -331,14 +332,29 @@ private DownloadMission getPendingMission(StoredFileHelper storage) { } /** - * Get a finished mission by its path + * Get the index into {@link #mMissionsFinished} of a finished mission by its path, return + * {@code -1} if there is no such mission. This function also checks if the matched mission's + * file exists, and, if it does not, the related mission is forgotten about (like in {@link + * #loadFinishedMissions()}) and {@code -1} is returned. * - * @param storage where the file possible is stored + * @param storage where the file would be stored * @return the mission index or -1 if no such mission exists */ private int getFinishedMissionIndex(StoredFileHelper storage) { for (int i = 0; i < mMissionsFinished.size(); i++) { if (mMissionsFinished.get(i).storage.equals(storage)) { + // If the file does not exist the mission is not valid anymore. Also checking if + // length == 0 since the file picker may create an empty file before yielding it, + // but that does not mean the file really belonged to a previous mission. + if (!storage.existsAsFile() || storage.length() == 0) { + if (DEBUG) { + Log.d(TAG, "matched downloaded file removed: " + storage.getName()); + } + + mFinishedMissionStore.deleteMission(mMissionsFinished.get(i)); + mMissionsFinished.remove(i); + return -1; // finished mission whose associated file was removed + } return i; } } From 5ffc667bea5a6c6b5ceeb42fd6db16055a6ac790 Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 17 Mar 2021 22:07:13 +0100 Subject: [PATCH 14/15] Remove misplaced comment --- .../main/java/org/schabi/newpipe/download/DownloadDialog.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index d9a897d7360..1c3c86f4d0e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -159,10 +159,6 @@ public void setVideoStreams(final List videoStreams) { setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); } - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - public void setVideoStreams(final StreamSizeWrapper wvs) { this.wrappedVideoStreams = wvs; } From 2a99e0e435fac1b68c7880f3d1ce3da19f1a4e43 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 6 Jun 2021 22:15:32 +0200 Subject: [PATCH 15/15] Reimplement storing backup import/export path #6319 and #6402 were reverted before adding SAF changes, and have been readded at the end of SAF changes --- .../newpipe/download/DownloadDialog.java | 10 +-- .../subscription/SubscriptionFragment.kt | 8 +- .../settings/BasePreferenceFragment.java | 11 +++ .../settings/ContentSettingsFragment.java | 81 ++++++++++++++----- .../newpipe/streams/io/StoredFileHelper.java | 78 +++++++++++++----- .../giga/ui/fragment/MissionsFragment.java | 13 ++- app/src/main/res/values/settings_keys.xml | 1 + 7 files changed, 151 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 1c3c86f4d0e..c055f8f2383 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -702,9 +702,9 @@ private void prepareSelectedDownload() { } if (askForSavePath) { - final String startPath; + final Uri initialPath; if (NewPipeSettings.useStorageAccessFramework(context)) { - startPath = null; + initialPath = null; } else { final File initialSavePath; if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { @@ -712,11 +712,11 @@ private void prepareSelectedDownload() { } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); } - startPath = initialSavePath.getAbsolutePath(); + initialPath = Uri.parse(initialSavePath.getAbsolutePath()); } - startActivityForResult(StoredFileHelper.getNewPicker(context, startPath, - filenameTmp, mimeTmp), REQUEST_DOWNLOAD_SAVE_AS); + startActivityForResult(StoredFileHelper.getNewPicker(context, + filenameTmp, mimeTmp, initialPath), REQUEST_DOWNLOAD_SAVE_AS); return; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index d6c08a269ab..095c8dbc772 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -191,7 +191,10 @@ class SubscriptionFragment : BaseStateFragment() { val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val exportName = "newpipe_subscriptions_$date.json" - startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName, "application/json"), REQUEST_EXPORT_CODE) + startActivityForResult( + StoredFileHelper.getNewPicker(activity, exportName, "application/json", null), + REQUEST_EXPORT_CODE + ) } private fun openReorderDialog() { @@ -283,7 +286,8 @@ class SubscriptionFragment : BaseStateFragment() { private fun showLongTapDialog(selectedItem: ChannelInfoItem) { val commands = arrayOf( - getString(R.string.share), getString(R.string.open_in_browser), + getString(R.string.share), + getString(R.string.open_in_browser), getString(R.string.unsubscribe) ) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index b64543d2732..8b2bd9c9adf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -6,12 +6,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.util.ThemeHelper; +import java.util.Objects; + public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final boolean DEBUG = MainActivity.DEBUG; @@ -37,4 +41,11 @@ public void onResume() { super.onResume(); ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); } + + @NonNull + public final Preference requirePreference(@StringRes final int resId) { + final Preference preference = findPreference(getString(resId)); + Objects.requireNonNull(preference); + return preference; + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 45325dc6cae..4ef5b8b2517 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -5,6 +5,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; @@ -32,18 +33,25 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Objects; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class ContentSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_EXPORT_PATH = 30945; + private static final String ZIP_MIME_TYPE = "application/zip"; + private static final SimpleDateFormat EXPORT_DATE_FORMAT + = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); private ContentSettingsManager manager; + private String importExportDataPathKey; private String thumbnailLoadToggleKey; private String youtubeRestrictedModeEnabledKey; + @Nullable private Uri lastImportExportDataUri = null; private Localization initialSelectedLocalization; private ContentCountry initialSelectedContentCountry; private String initialLanguage; @@ -51,29 +59,35 @@ public class ContentSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { final File homeDir = ContextCompat.getDataDir(requireContext()); + Objects.requireNonNull(homeDir); manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); manager.deleteSettingsFile(); + importExportDataPathKey = getString(R.string.import_export_data_path); + thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); + youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); + addPreferencesFromResource(R.xml.content_settings); - final Preference importDataPreference = findPreference(getString(R.string.import_data)); + final Preference importDataPreference = requirePreference(R.string.import_data); importDataPreference.setOnPreferenceClickListener((Preference p) -> { - startActivityForResult(StoredFileHelper.getPicker(getContext()), REQUEST_IMPORT_PATH); + startActivityForResult( + StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()), + REQUEST_IMPORT_PATH); return true; }); - final Preference exportDataPreference = findPreference(getString(R.string.export_data)); + final Preference exportDataPreference = requirePreference(R.string.export_data); exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - startActivityForResult(StoredFileHelper.getNewPicker(getContext(), null, - "NewPipeData-" + sdf.format(new Date()) + ".zip", "application/zip"), + + startActivityForResult( + StoredFileHelper.getNewPicker(requireContext(), + "NewPipeData-" + EXPORT_DATE_FORMAT.format(new Date()) + ".zip", + ZIP_MIME_TYPE, getImportExportDataUri()), REQUEST_EXPORT_PATH); return true; }); - thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); - youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); - initialSelectedLocalization = org.schabi.newpipe.util.Localization .getPreferredLocalization(requireContext()); initialSelectedContentCountry = org.schabi.newpipe.util.Localization @@ -81,7 +95,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro initialLanguage = PreferenceManager .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); - final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key)); + final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key); clearCookiePref.setOnPreferenceClickListener(preference -> { defaultPreferences.edit() .putString(getString(R.string.recaptcha_cookies_key), "").apply(); @@ -157,8 +171,11 @@ public void onActivityResult(final int requestCode, if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - final StoredFileHelper file = new StoredFileHelper(getContext(), data.getData(), - "application/zip"); + + lastImportExportDataUri = data.getData(); // will be saved only on success + + final StoredFileHelper file + = new StoredFileHelper(getContext(), data.getData(), ZIP_MIME_TYPE); if (requestCode == REQUEST_EXPORT_PATH) { exportDatabase(file); } else { @@ -182,6 +199,7 @@ private void exportDatabase(final StoredFileHelper file) { .getDefaultSharedPreferences(requireContext()); manager.exportDatabase(preferences, file); + saveLastImportExportDataUri(false); // save export path only on success Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e); @@ -206,30 +224,55 @@ private void importDatabase(final StoredFileHelper file) { .show(); } - //If settings file exist, ask if it should be imported. + // if settings file exist, ask if it should be imported. if (manager.extractSettings(file)) { final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()); alert.setTitle(R.string.import_settings); alert.setNegativeButton(android.R.string.no, (dialog, which) -> { dialog.dismiss(); - // restart app to properly load db - System.exit(0); + finishImport(); }); alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { dialog.dismiss(); manager.loadSharedPreferences(PreferenceManager .getDefaultSharedPreferences(requireContext())); - // restart app to properly load db - System.exit(0); + finishImport(); }); alert.show(); } else { - // restart app to properly load db - System.exit(0); + finishImport(); } } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e); } } + + /** + * Save import path and restart system. + */ + private void finishImport() { + // save import path only on success; save immediately because app is about to exit + saveLastImportExportDataUri(true); + // restart app to properly load db + System.exit(0); + } + + private Uri getImportExportDataUri() { + final String path = defaultPreferences.getString(importExportDataPathKey, null); + return isBlank(path) ? null : Uri.parse(path); + } + + private void saveLastImportExportDataUri(final boolean immediately) { + if (lastImportExportDataUri != null) { + final SharedPreferences.Editor editor = defaultPreferences.edit() + .putString(importExportDataPathKey, lastImportExportDataUri.toString()); + if (immediately) { + // noinspection ApplySharedPref + editor.commit(); // app about to be restarted, commit immediately + } else { + editor.apply(); + } + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index 0fcad29589d..c86164ed2e5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -448,7 +448,7 @@ private boolean stringMismatch(final String str1, final String str2) { return !str1.equals(str2); } - public static Intent getPicker(final Context ctx) { + public static Intent getPicker(@NonNull final Context ctx) { if (NewPipeSettings.useStorageAccessFramework(ctx)) { return new Intent(Intent.ACTION_OPEN_DOCUMENT) .putExtra("android.content.extra.SHOW_ADVANCED", true) @@ -466,10 +466,14 @@ public static Intent getPicker(final Context ctx) { } } + public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) { + return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null); + } + public static Intent getNewPicker(@NonNull final Context ctx, - @Nullable final String startPath, @Nullable final String filename, - @NonNull final String mimeType) { + @NonNull final String mimeType, + @Nullable final Uri initialPath) { final Intent i; if (NewPipeSettings.useStorageAccessFramework(ctx)) { i = new Intent(Intent.ACTION_CREATE_DOCUMENT) @@ -478,10 +482,6 @@ public static Intent getNewPicker(@NonNull final Context ctx, .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); - - if (startPath != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - i.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(startPath)); - } if (filename != null) { i.putExtra(Intent.EXTRA_TITLE, filename); } @@ -492,21 +492,63 @@ public static Intent getNewPicker(@NonNull final Context ctx, .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE); + } + return applyInitialPathToPickerIntent(ctx, i, initialPath, filename); + } - if (startPath != null || filename != null) { - File fullStartPath; - if (startPath == null) { - fullStartPath = Environment.getExternalStorageDirectory(); - } else { - fullStartPath = new File(startPath); + private static Intent applyInitialPathToPickerIntent(@NonNull final Context ctx, + @NonNull final Intent intent, + @Nullable final Uri initialPath, + @Nullable final String filename) { + + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + if (initialPath == null) { + return intent; // nothing to do, no initial path provided + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialPath); + } else { + return intent; // can't set initial path on API < 26 + } + + } else { + if (initialPath == null && filename == null) { + return intent; // nothing to do, no initial path and no file name provided + } + + File file; + if (initialPath == null) { + // The only way to set the previewed filename in non-SAF FilePicker is to set a + // starting path ending with that filename. So when the initialPath is null but + // filename isn't just default to the external storage directory. + file = Environment.getExternalStorageDirectory(); + } else { + try { + file = Utils.getFileForUri(initialPath); + } catch (final Throwable ignored) { + // getFileForUri() can't decode paths to 'storage', fallback to this + file = new File(initialPath.toString()); } - if (filename != null) { - fullStartPath = new File(fullStartPath, filename); + } + + // remove any filename at the end of the path (get the parent directory in that case) + if (!file.exists() || !file.isDirectory()) { + file = file.getParentFile(); + if (file == null || !file.exists()) { + // default to the external storage directory in case of an invalid path + file = Environment.getExternalStorageDirectory(); } - i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, - fullStartPath.getAbsolutePath()); + // else: file is surely a directory } + + if (filename != null) { + // append a filename so that the non-SAF FilePicker shows it as the preview + file = new File(file, filename); + } + + return intent + .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, file.getAbsolutePath()); } - return i; } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index c56587c55d0..793d147b595 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -29,14 +29,13 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.FilePickerActivityHelper; -import org.schabi.newpipe.util.ThemeHelper; import java.io.File; import java.io.IOException; import us.shandian.giga.get.DownloadMission; -import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; @@ -242,9 +241,9 @@ private void setAdapterButtons() { private void recoverMission(@NonNull DownloadMission mission) { unsafeMissionTarget = mission; - final String startPath; + final Uri initialPath; if (NewPipeSettings.useStorageAccessFramework(mContext)) { - startPath = null; + initialPath = null; } else { final File initialSavePath; if (DownloadManager.TAG_AUDIO.equals(mission.storage.getType())) { @@ -252,11 +251,11 @@ private void recoverMission(@NonNull DownloadMission mission) { } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); } - startPath = initialSavePath.getAbsolutePath(); + initialPath = Uri.parse(initialSavePath.getAbsolutePath()); } - startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath, - mission.storage.getName(), mission.storage.getType()), REQUEST_DOWNLOAD_SAVE_AS); + startActivityForResult(StoredFileHelper.getNewPicker(mContext, mission.storage.getName(), + mission.storage.getType(), initialPath), REQUEST_DOWNLOAD_SAVE_AS); } @Override diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fd6cc725140..c23e81fbe22 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -265,6 +265,7 @@ feed_use_dedicated_fetch_method + import_export_data_path import_data export_data