diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb15cddce..6eb9f514f 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 af118387c..784be8d0b 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.error.UserAction; 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 class App extends MultiDexApplication { 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 e7ae8d879..051e9b752 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 class DownloadDialog extends DialogFragment 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 class DownloadDialog extends DialogFragment // 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 @@ public class DownloadDialog extends DialogFragment 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 && (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 (!askForSavePath) { - Toast.makeText(context, getString(R.string.no_available_dir), - Toast.LENGTH_LONG).show(); - } - - if (NewPipeSettings.useStorageAccessFramework(context)) { - StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, - filename, mime); + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + startActivityForResult(StoredDirectoryHelper.getPicker(context), + REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER); } else { - 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); + startActivityForResult(StoredDirectoryHelper.getPicker(context), + REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER); } return; } + if (askForSavePath) { + final String startPath; + if (NewPipeSettings.useStorageAccessFramework(context)) { + startPath = null; + } else { + final File initialSavePath; + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); + } else { + initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); + } + 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 8a235fa8a..981433e83 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 f0675da1b..120b81681 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.annotation.StringRes; 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.exceptions.ExtractionException; 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 class SubscriptionsImportFragment extends BaseFragment { } 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 class SubscriptionsImportFragment extends BaseFragment { 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 5dfb1bfe5..cbf7bc95e 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.App; 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 class SubscriptionsExportService extends BaseImportExportService { 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 @@ public class SubscriptionsExportService extends BaseImportExportService { .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 class SubscriptionsExportService extends BaseImportExportService { } @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 class SubscriptionsExportService extends BaseImportExportService { }; } - 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 af94934b2..896775d49 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.NewPipe; 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.core.Notification; 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 class SubscriptionsImportService extends BaseImportExportService { 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 3fd44c4d5..e3193ba0f 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 @@ package org.schabi.newpipe.settings; 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.FilePickerActivityHelper; 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 class ContentSettingsFragment extends BasePreferenceFragment { 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 class ContentSettingsFragment extends BasePreferenceFragment { .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 class ContentSettingsFragment extends BasePreferenceFragment { @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 class ContentSettingsFragment extends BasePreferenceFragment { } 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 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } - 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 @@ public class ContentSettingsFragment extends BasePreferenceFragment { 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 1730a230e..6c70776ec 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 913512644..ab513940d 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.net.Uri; 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 class DownloadSettingsFragment extends BasePreferenceFragment { 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 class DownloadSettingsFragment extends BasePreferenceFragment { 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 class DownloadSettingsFragment extends BasePreferenceFragment { 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 01f51b0b3..0203f9107 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 @@ package org.schabi.newpipe.settings; 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 org.schabi.newpipe.R; 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 final class NewPipeSettings { 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 @@ public final class NewPipeSettings { } 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 68908fc92..02e2538c5 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 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; 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 000000000..4b871d1dc --- /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 000000000..23a2393a8 --- /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 46ec68d9e..9b4f79047 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 6ede163a3..20d8ce30c 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 @@ import java.io.File; 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 e2b766bb0..32964490e 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 final class ZipHelper { } /** - * 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 final class ZipHelper { } } - 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 0d6320b53..000000000 --- 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 5edc5f3ed..72193aa1b 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.NonNull; 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 @@ public class StoredDirectoryHelper { 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 ad64042f1..4a9c170eb 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.Context; 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 class StoredFileHelper implements Serializable { 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 @@ public class StoredFileHelper implements Serializable { 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 c83eec819..bfb7926dd 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 @@ public class MissionsFragment extends Fragment { 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 4fb4019bd..31e03fad6 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 1bc0e8404..53f9b2c89 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"> -