1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-02-24 06:50:07 +00:00

Support SAF properly

This commit is contained in:
wb9688 2020-06-13 17:29:57 +02:00 committed by Stypox
parent 1e09a1768e
commit 0f75024e03
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
23 changed files with 451 additions and 311 deletions

View File

@ -22,7 +22,6 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:logo="@mipmap/ic_launcher" android:logo="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:theme="@style/OpeningTheme" android:theme="@style/OpeningTheme"
android:resizeableActivity="true" android:resizeableActivity="true"
tools:ignore="AllowBackup"> tools:ignore="AllowBackup">

View File

@ -27,7 +27,7 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils; 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.Localization;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -91,7 +91,7 @@ public class App extends MultiDexApplication {
app = this; app = this;
// Initialize settings first because others inits can use its values // Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this); NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(), NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this), Localization.getPreferredLocalization(this),

View File

@ -83,6 +83,8 @@ public class DownloadDialog extends DialogFragment
private static final String TAG = "DialogFragment"; private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG; private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230; 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 @State
StreamInfo currentInfo; StreamInfo currentInfo;
@ -116,6 +118,10 @@ public class DownloadDialog extends DialogFragment
private SharedPreferences prefs; 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) { public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog(); final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info); dialog.setInfo(info);
@ -374,12 +380,16 @@ public class DownloadDialog extends DialogFragment
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) { if (resultCode != Activity.RESULT_OK) {
return;
}
if (data.getData() == null) { if (data.getData() == null) {
showFailedDialog(R.string.general_error); showFailedDialog(R.string.general_error);
return; return;
} }
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS) {
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) { if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
final File file = Utils.getFileForUri(data.getData()); final File file = Utils.getFileForUri(data.getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
@ -396,6 +406,37 @@ public class DownloadDialog extends DialogFragment
// check if the selected file was previously used // check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(), checkSelectedDownload(null, data.getData(), docFile.getName(),
docFile.getType()); 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() { private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage; final StoredDirectoryHelper mainStorage;
final MediaFormat format; final MediaFormat format;
final String mime;
final String selectedMediaType; final String selectedMediaType;
// first, build the filename and get the output folder (if possible) // first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic // later, run a very very very large file checking logic
String filename = getNameEditText().concat("."); filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key); selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio; mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
switch (format) { if (format == MediaFormat.WEBMA_OPUS) {
case WEBMA_OPUS: mimeTmp = "audio/ogg";
mime = "audio/ogg"; filenameTmp += "opus";
filename += "opus"; } else {
break; mimeTmp = format.mimeType;
default: filenameTmp += format.suffix;
mime = format.mimeType;
filename += format.suffix;
break;
} }
break; break;
case R.id.video_button: case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key); selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo; mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mime = format.mimeType; mimeTmp = format.mimeType;
filename += format.suffix; filenameTmp += format.suffix;
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key); selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType; mimeTmp = format.mimeType;
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
break; break;
default: default:
throw new RuntimeException("No stream selected"); throw new RuntimeException("No stream selected");
} }
if (mainStorage == null || askForSavePath) { if (!askForSavePath && (mainStorage == null || (mainStorage.isDirect()
// This part is called if with SAF preferred: == NewPipeSettings.useStorageAccessFramework(context)))) {
// * older android version running // Pick new download folder if one of:
// * save path not defined (via download settings) // - Download folder is not set
// * the user checked the "ask where to download" option // - Download folder uses SAF while SAF is disabled
// - Download folder doesn't use SAF while SAF is enabled
if (!askForSavePath) { Toast.makeText(context, getString(R.string.no_dir_yet),
Toast.makeText(context, getString(R.string.no_available_dir),
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
}
if (NewPipeSettings.useStorageAccessFramework(context)) {
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
filename, mime);
} else {
File initialSavePath;
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); startActivityForResult(StoredDirectoryHelper.getPicker(context),
REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER);
} else { } else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); startActivityForResult(StoredDirectoryHelper.getPicker(context),
} REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER);
initialSavePath = new File(initialSavePath, filename);
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
} }
return; 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 // 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 // remember the last media type downloaded by the user
prefs.edit() prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
.putString(getString(R.string.last_used_download_type), selectedMediaType)
.apply(); .apply();
} }

View File

@ -7,8 +7,8 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu 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.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService 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.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 org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE 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.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils import org.schabi.newpipe.util.ShareUtils
import java.io.File import us.shandian.giga.io.StoredFileHelper
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -188,15 +187,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
private fun onImportPreviousSelected() { private fun onImportPreviousSelected() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE) startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_CODE)
} }
private fun onExportSelected() { private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json" 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() { private fun openReorderDialog() {
@ -207,23 +205,20 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) { if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_EXPORT_CODE) { if (requestCode == REQUEST_EXPORT_CODE) {
val exportFile = Utils.getFileForUri(data.data!!) var uri = data.data!!
val parentFile = exportFile.parentFile!! if (FilePickerActivityHelper.isOwnFileUri(activity, uri)) {
if (!parentFile.canWrite() || !parentFile.canRead()) { uri = Uri.fromFile(Utils.getFileForUri(uri))
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() }
} else {
activity.startService( activity.startService(
Intent(activity, SubscriptionsExportService::class.java) Intent(activity, SubscriptionsExportService::class.java)
.putExtra(KEY_FILE_PATH, exportFile.absolutePath) .putExtra(SubscriptionsExportService.KEY_FILE_PATH, uri)
) )
}
} else if (requestCode == REQUEST_IMPORT_CODE) { } else if (requestCode == REQUEST_IMPORT_CODE) {
val path = Utils.getFileForUri(data.data!!).absolutePath
ImportConfirmationDialog.show( ImportConfirmationDialog.show(
this, this,
Intent(activity, SubscriptionsImportService::class.java) Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, path) .putExtra(KEY_VALUE, data.data)
) )
} }
} }

View File

@ -18,8 +18,6 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.core.text.util.LinkifyCompat; import androidx.core.text.util.LinkifyCompat;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity; 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.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import icepick.State; 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.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
@ -175,8 +173,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
} }
public void onImportFile() { public void onImportFile() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_FILE_CODE);
REQUEST_IMPORT_FILE_CODE);
} }
@Override @Override
@ -188,10 +185,10 @@ public class SubscriptionsImportFragment extends BaseFragment {
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE
&& data.getData() != null) { && data.getData() != null) {
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
ImportConfirmationDialog.show(this, ImportConfirmationDialog.show(this,
new Intent(activity, SubscriptionsImportService.class) 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)); .putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
} }
} }

View File

@ -20,7 +20,7 @@
package org.schabi.newpipe.local.subscription.services; package org.schabi.newpipe.local.subscription.services;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils; import android.net.Uri;
import android.util.Log; import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -31,16 +31,17 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.streams.io.SharpOutputStream;
import java.io.File; import java.io.IOException;
import java.io.FileNotFoundException; import java.io.OutputStream;
import java.io.FileOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import us.shandian.giga.io.StoredFileHelper;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
@ -55,8 +56,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE"; + ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription; private Subscription subscription;
private File outFile; private StoredFileHelper outFile;
private FileOutputStream outputStream; private OutputStream outputStream;
@Override @Override
public int onStartCommand(final Intent intent, final int flags, final int startId) { 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; return START_NOT_STICKY;
} }
final String path = intent.getStringExtra(KEY_FILE_PATH); final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
if (TextUtils.isEmpty(path)) { if (path == null) {
stopAndReportError(new IllegalStateException( 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"); "Exporting subscriptions");
return START_NOT_STICKY; return START_NOT_STICKY;
} }
try { try {
outFile = new File(path); outFile = new StoredFileHelper(this, path, "application/json");
outputStream = new FileOutputStream(outFile); outputStream = new SharpOutputStream(outFile.getStream());
} catch (final FileNotFoundException e) { } catch (final IOException e) {
handleError(e); handleError(e);
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -122,8 +123,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
.subscribe(getSubscriber()); .subscribe(getSubscriber());
} }
private Subscriber<File> getSubscriber() { private Subscriber<StoredFileHelper> getSubscriber() {
return new Subscriber<File>() { return new Subscriber<StoredFileHelper>() {
@Override @Override
public void onSubscribe(final Subscription s) { public void onSubscribe(final Subscription s) {
subscription = s; subscription = s;
@ -131,7 +132,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
} }
@Override @Override
public void onNext(final File file) { public void onNext(final StoredFileHelper file) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "startExport() success: file = " + file); Log.d(TAG, "startExport() success: file = " + file);
} }
@ -153,7 +154,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
}; };
} }
private Function<List<SubscriptionItem>, File> exportToFile() { private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
return subscriptionItems -> { return subscriptionItems -> {
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener); ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
return outFile; return outFile;

View File

@ -20,6 +20,7 @@
package org.schabi.newpipe.local.subscription.services; package org.schabi.newpipe.local.subscription.services;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; 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.channel.ChannelInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.streams.io.SharpInputStream;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; 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.Consumer;
import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import us.shandian.giga.io.StoredFileHelper;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
import static us.shandian.giga.io.StoredFileHelper.DEFAULT_MIME;
public class SubscriptionsImportService extends BaseImportExportService { public class SubscriptionsImportService extends BaseImportExportService {
public static final int CHANNEL_URL_MODE = 0; public static final int CHANNEL_URL_MODE = 0;
@ -101,17 +102,18 @@ public class SubscriptionsImportService extends BaseImportExportService {
if (currentMode == CHANNEL_URL_MODE) { if (currentMode == CHANNEL_URL_MODE) {
channelUrl = intent.getStringExtra(KEY_VALUE); channelUrl = intent.getStringExtra(KEY_VALUE);
} else { } else {
final String filePath = intent.getStringExtra(KEY_VALUE); final Uri uri = intent.getParcelableExtra(KEY_VALUE);
if (TextUtils.isEmpty(filePath)) { if (uri == null) {
stopAndReportError(new IllegalStateException( 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"); "Importing subscriptions");
return START_NOT_STICKY; return START_NOT_STICKY;
} }
try { try {
inputStream = new FileInputStream(new File(filePath)); inputStream = new SharpInputStream(
} catch (final FileNotFoundException e) { new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
} catch (final IOException e) {
handleError(e); handleError(e);
return START_NOT_STICKY; return START_NOT_STICKY;
} }

View File

@ -2,13 +2,15 @@ package org.schabi.newpipe.settings;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.Preference; import androidx.preference.Preference;
@ -29,10 +31,14 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class ContentSettingsFragment extends BasePreferenceFragment { public class ContentSettingsFragment extends BasePreferenceFragment {
@ -57,24 +63,15 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResource(R.xml.content_settings); addPreferencesFromResource(R.xml.content_settings);
final Preference importDataPreference = findPreference(getString(R.string.import_data)); final Preference importDataPreference = findPreference(getString(R.string.import_data));
importDataPreference.setOnPreferenceClickListener(p -> { importDataPreference.setOnPreferenceClickListener((Preference p) -> {
final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) startActivityForResult(StoredFileHelper.getPicker(getContext()), REQUEST_IMPORT_PATH);
.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);
return true; return true;
}); });
final Preference exportDataPreference = findPreference(getString(R.string.export_data)); final Preference exportDataPreference = findPreference(getString(R.string.export_data));
exportDataPreference.setOnPreferenceClickListener(p -> { exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) startActivityForResult(StoredDirectoryHelper.getPicker(getContext()),
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) REQUEST_EXPORT_PATH);
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_DIR);
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true; return true;
}); });
@ -89,7 +86,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key)); final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key));
clearCookiePref.setOnPreferenceClickListener(preference -> { clearCookiePref.setOnPreferenceClickListener(preference -> {
defaultPreferences.edit() defaultPreferences.edit()
.putString(getString(R.string.recaptcha_cookies_key), "").apply(); .putString(getString(R.string.recaptcha_cookies_key), "").apply();
@ -152,7 +148,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
@Override @Override
public void onActivityResult(final int requestCode, final int resultCode, public void onActivityResult(final int requestCode, final int resultCode,
@NonNull final Intent data) { @Nullable final Intent data) {
assureCorrectAppLanguage(getContext()); assureCorrectAppLanguage(getContext());
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) { if (DEBUG) {
@ -163,31 +159,44 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
} }
if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH)
&& resultCode == Activity.RESULT_OK && data.getData() != null) { && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); try {
Uri uri = data.getData();
if (FilePickerActivityHelper.isOwnFileUri(requireContext(), uri)) {
uri = Uri.fromFile(Utils.getFileForUri(uri));
}
if (requestCode == REQUEST_EXPORT_PATH) { if (requestCode == REQUEST_EXPORT_PATH) {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); final StoredDirectoryHelper directory
exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); = 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 { } else {
final StoredFileHelper file = new StoredFileHelper(getContext(), uri,
StoredFileHelper.DEFAULT_MIME);
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setMessage(R.string.override_current_data) builder.setMessage(R.string.override_current_data)
.setPositiveButton(getString(R.string.finish), .setPositiveButton(R.string.finish,
(d, id) -> importDatabase(path)) (DialogInterface d, int id) -> importDatabase(file))
.setNegativeButton(android.R.string.cancel, .setNegativeButton(R.string.cancel,
(d, id) -> d.cancel()); (DialogInterface d, int id) -> d.cancel());
builder.create().show(); builder.create().show();
} }
} catch (final IOException e) {
e.printStackTrace();
}
} }
} }
private void exportDatabase(final String path) { private void exportDatabase(final StoredFileHelper file) {
try { try {
//checkpoint before export //checkpoint before export
NewPipeDatabase.checkpoint(); NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext()); .getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, path); manager.exportDatabase(preferences, file);
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) { } 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 // 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) Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show(); .show();
return; return;
@ -208,13 +217,13 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
throw new Exception("Could not create databases dir"); 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) 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. //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()); final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
alert.setTitle(R.string.import_settings); alert.setTitle(R.string.import_settings);

View File

@ -1,7 +1,9 @@
package org.schabi.newpipe.settings package org.schabi.newpipe.settings
import android.content.SharedPreferences import android.content.SharedPreferences
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.util.ZipHelper import org.schabi.newpipe.util.ZipHelper
import us.shandian.giga.io.StoredFileHelper
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
@ -17,8 +19,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
* It also creates the file. * It also creates the file.
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, outputPath: String) { fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputPath))) file.create()
ZipOutputStream(BufferedOutputStream(SharpOutputStream(file.stream)))
.use { outZip -> .use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db") 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() return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
} }
fun extractDb(filePath: String): Boolean { fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(filePath, fileLocator.db.path, "newpipe.db") val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
if (success) { if (success) {
fileLocator.dbJournal.delete() fileLocator.dbJournal.delete()
fileLocator.dbWal.delete() fileLocator.dbWal.delete()
@ -59,9 +62,8 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
return success return success
} }
fun extractSettings(filePath: String): Boolean { fun extractSettings(file: StoredFileHelper): Boolean {
return ZipHelper return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
.extractFileFromZip(filePath, fileLocator.settings.path, "newpipe.settings")
} }
fun loadSharedPreferences(preferences: SharedPreferences) { fun loadSharedPreferences(preferences: SharedPreferences) {

View File

@ -8,11 +8,11 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.SwitchPreference;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
@ -57,6 +57,14 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
prefPathAudio = findPreference(downloadPathAudioPreference); prefPathAudio = findPreference(downloadPathAudioPreference);
prefStorageAsk = findPreference(downloadStorageAsk); 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(); updatePreferencesSummary();
updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false));
@ -177,8 +185,14 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
final int request; final int request;
if (key.equals(storageUseSafPreference)) { if (key.equals(storageUseSafPreference)) {
Toast.makeText(getContext(), R.string.download_choose_new_path, if (!NewPipeSettings.useStorageAccessFramework(ctx)) {
Toast.LENGTH_LONG).show(); NewPipeSettings.saveDefaultVideoDownloadDirectory(ctx);
NewPipeSettings.saveDefaultAudioDownloadDirectory(ctx);
} else {
defaultPreferences.edit().putString(downloadPathVideoPreference, null)
.putString(downloadPathAudioPreference, null).apply();
}
updatePreferencesSummary();
return true; return true;
} else if (key.equals(downloadPathVideoPreference)) { } else if (key.equals(downloadPathVideoPreference)) {
request = REQUEST_DOWNLOAD_VIDEO_PATH; request = REQUEST_DOWNLOAD_VIDEO_PATH;
@ -188,22 +202,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
return super.onPreferenceTreeClick(preference); return super.onPreferenceTreeClick(preference);
} }
final Intent i; startActivityForResult(StoredDirectoryHelper.getPicker(ctx), request);
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);
return true; return true;
} }

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.settings;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -12,6 +13,8 @@ import org.schabi.newpipe.R;
import java.io.File; import java.io.File;
import java.util.Set; import java.util.Set;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/* /*
* Created by k3b on 07.01.2016. * Created by k3b on 07.01.2016.
* *
@ -65,26 +68,29 @@ public final class NewPipeSettings {
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true); PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
getVideoDownloadFolder(context); saveDefaultVideoDownloadDirectory(context);
getAudioDownloadFolder(context); saveDefaultAudioDownloadDirectory(context);
SettingMigrations.initMigrations(context, isFirstRun); SettingMigrations.initMigrations(context, isFirstRun);
} }
private static void getVideoDownloadFolder(final Context context) { static void saveDefaultVideoDownloadDirectory(final Context context) {
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); saveDefaultDirectory(context, R.string.download_path_video_key,
Environment.DIRECTORY_MOVIES);
} }
private static void getAudioDownloadFolder(final Context context) { static void saveDefaultAudioDownloadDirectory(final Context context) {
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); saveDefaultDirectory(context, R.string.download_path_audio_key,
Environment.DIRECTORY_MUSIC);
} }
private static void getDir(final Context context, final int keyID, private static void saveDefaultDirectory(final Context context, final int keyID,
final String defaultDirectoryName) { final String defaultDirectoryName) {
if (!useStorageAccessFramework(context)) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID); final String key = context.getString(keyID);
final String downloadPath = prefs.getString(key, null); final String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) { if (!isNullOrEmpty(downloadPath)) {
return; return;
} }
@ -92,6 +98,7 @@ public final class NewPipeSettings {
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply(); spEditor.apply();
} }
}
@NonNull @NonNull
public static File getDir(final String defaultDirectoryName) { public static File getDir(final String defaultDirectoryName) {
@ -103,10 +110,15 @@ public final class NewPipeSettings {
} }
public static boolean useStorageAccessFramework(final Context context) { 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 String key = context.getString(R.string.storage_use_saf);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(key, false); return prefs.getBoolean(key, true);
} }
} }

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -41,11 +40,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class SettingsActivity extends AppCompatActivity public class SettingsActivity extends AppCompatActivity
implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
public static void initSettings(final Context context) {
NewPipeSettings.initSettings(context);
}
@Override @Override
protected void onCreate(final Bundle savedInstanceBundle) { protected void onCreate(final Bundle savedInstanceBundle) {
setTheme(ThemeHelper.getSettingsThemeStyle(this)); setTheme(ThemeHelper.getSettingsThemeStyle(this));

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,12 +1,13 @@
package org.schabi.newpipe.streams.io; package org.schabi.newpipe.streams.io;
import java.io.Closeable; import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException; import java.io.IOException;
/** /**
* Based on C#'s Stream class. * 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() throws IOException;
public abstract int read(byte[] buffer) throws IOException; public abstract int read(byte[] buffer) throws IOException;

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
@ -28,25 +27,6 @@ import java.io.File;
public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity {
private CustomFilePickerFragment currentFragment; 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) { public static boolean isOwnFileUri(@NonNull final Context context, @NonNull final Uri uri) {
if (uri.getAuthority() == null) { if (uri.getAuthority() == null) {
return false; return false;

View File

@ -1,15 +1,18 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import org.schabi.newpipe.streams.io.SharpInputStream;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import us.shandian.giga.io.StoredFileHelper;
/** /**
* Created by Christian Schabesberger on 28.01.18. * Created by Christian Schabesberger on 28.01.18.
* Copyright 2018 Christian Schabesberger <chris.schabesberger@mailbox.org> * Copyright 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
@ -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. * 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 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. * @param name The path of the file inside the zip.
* @return will return true if the file was found within the zip file * @return will return true if the file was found within the zip file
* @throws Exception * @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 { final String name) throws Exception {
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream( try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
new FileInputStream(filePath)))) { new SharpInputStream(zipFile.getStream())))) {
final byte[] data = new byte[BUFFER_SIZE]; final byte[] data = new byte[BUFFER_SIZE];
boolean found = false; boolean found = false;
ZipEntry ze; ZipEntry ze;
while ((ze = inZip.getNextEntry()) != null) { while ((ze = inZip.getNextEntry()) != null) {
if (ze.getName().equals(name)) { if (ze.getName().equals(name)) {
found = true; found = true;
@ -102,8 +104,9 @@ public final class ZipHelper {
} }
} }
public static boolean isValidZipFile(final String filePath) { public static boolean isValidZipFile(final StoredFileHelper file) {
try (ZipFile ignored = new ZipFile(filePath)) { try (ZipInputStream ignored = new ZipInputStream(new BufferedInputStream(
new SharpInputStream(file.getStream())))) {
return true; return true;
} catch (final IOException ioe) { } catch (final IOException ioe) {
return false; return false;

View File

@ -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();
}
}

View File

@ -13,6 +13,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -287,4 +290,18 @@ public class StoredDirectoryHelper {
return null; 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);
}
}
} }

View File

@ -6,14 +6,18 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile; 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.streams.io.SharpStream;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -37,6 +41,19 @@ public class StoredFileHelper implements Serializable {
private String srcName; private String srcName;
private String srcType; 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) { public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
@ -139,22 +156,6 @@ public class StoredFileHelper implements Serializable {
return instance; 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 { public SharpStream getStream() throws IOException {
invalid(); invalid();
@ -383,4 +384,64 @@ public class StoredFileHelper implements Serializable {
return !str1.equals(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;
}
} }

View File

@ -242,27 +242,21 @@ public class MissionsFragment extends Fragment {
private void recoverMission(@NonNull DownloadMission mission) { private void recoverMission(@NonNull DownloadMission mission) {
unsafeMissionTarget = mission; unsafeMissionTarget = mission;
final String startPath;
if (NewPipeSettings.useStorageAccessFramework(mContext)) { if (NewPipeSettings.useStorageAccessFramework(mContext)) {
StoredFileHelper.requestSafWithFileCreation( startPath = null;
MissionsFragment.this,
REQUEST_DOWNLOAD_SAVE_AS,
mission.storage.getName(),
mission.storage.getType()
);
} else { } else {
File initialSavePath; final File initialSavePath;
if (DownloadManager.TAG_VIDEO.equals(mission.storage.getType())) if (DownloadManager.TAG_AUDIO.equals(mission.storage.getType())) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
else
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = new File(initialSavePath, mission.storage.getName()); initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
startActivityForResult(
FilePickerActivityHelper.chooseFileToSave(mContext, initialSavePath.getAbsolutePath()),
REQUEST_DOWNLOAD_SAVE_AS
);
} }
startPath = initialSavePath.getAbsolutePath();
}
startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath,
mission.storage.getName()), REQUEST_DOWNLOAD_SAVE_AS);
} }
@Override @Override

View File

@ -362,6 +362,7 @@
<string name="msg_wait">Please wait…</string> <string name="msg_wait">Please wait…</string>
<string name="msg_copied">Copied to clipboard</string> <string name="msg_copied">Copied to clipboard</string>
<string name="no_available_dir">Please define a download folder later in settings</string> <string name="no_available_dir">Please define a download folder later in settings</string>
<string name="no_dir_yet">No download folder set yet, choose the default download folder now</string>
<string name="msg_popup_permission">This permission is needed to\nopen in popup mode</string> <string name="msg_popup_permission">This permission is needed to\nopen in popup mode</string>
<string name="one_item_deleted">1 item deleted.</string> <string name="one_item_deleted">1 item deleted.</string>
<!-- Checksum types --> <!-- Checksum types -->

View File

@ -3,7 +3,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/settings_category_downloads_title"> android:title="@string/settings_category_downloads_title">
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="@string/downloads_storage_ask" android:key="@string/downloads_storage_ask"
@ -12,7 +11,6 @@
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/storage_use_saf" android:key="@string/storage_use_saf"
android:summary="@string/downloads_storage_use_saf_summary" android:summary="@string/downloads_storage_use_saf_summary"
android:title="@string/downloads_storage_use_saf_title" android:title="@string/downloads_storage_use_saf_title"