Support SAF properly
This commit is contained in:
parent
1e09a1768e
commit
0f75024e03
23 changed files with 451 additions and 311 deletions
|
@ -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">
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<SubscriptionState>() {
|
|||
}
|
||||
|
||||
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<SubscriptionState>() {
|
|||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<File> getSubscriber() {
|
||||
return new Subscriber<File>() {
|
||||
private Subscriber<StoredFileHelper> getSubscriber() {
|
||||
return new Subscriber<StoredFileHelper>() {
|
||||
@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<List<SubscriptionItem>, File> exportToFile() {
|
||||
private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
|
||||
return subscriptionItems -> {
|
||||
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
|
||||
return outFile;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <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.
|
||||
*
|
||||
* @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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -362,6 +362,7 @@
|
|||
<string name="msg_wait">Please wait…</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_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="one_item_deleted">1 item deleted.</string>
|
||||
<!-- Checksum types -->
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/settings_category_downloads_title">
|
||||
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/downloads_storage_ask"
|
||||
|
@ -12,7 +11,6 @@
|
|||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="@string/storage_use_saf"
|
||||
android:summary="@string/downloads_storage_use_saf_summary"
|
||||
android:title="@string/downloads_storage_use_saf_title"
|
||||
|
|
Loading…
Reference in a new issue