Merge pull request #5415 from Stypox/saf

Support SAF properly
This commit is contained in:
Tobi 2021-06-08 10:55:38 +02:00 committed by GitHub
commit c96bdfcb32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1193 additions and 951 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

@ -49,6 +49,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
@ -68,8 +70,6 @@ import icepick.Icepick;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
@ -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);
@ -153,10 +159,6 @@ public class DownloadDialog extends DialogFragment
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
} }
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) { public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs; this.wrappedVideoStreams = wvs;
} }
@ -374,12 +376,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 +402,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 +640,92 @@ 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
// This part is called if with SAF preferred: && (mainStorage == null
// * older android version running || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
// * save path not defined (via download settings) || mainStorage.isInvalidSafStorage())) {
// * the user checked the "ask where to download" option // Pick new download folder if one of:
// - Download folder is not set
if (!askForSavePath) { // - Download folder uses SAF while SAF is disabled
Toast.makeText(context, getString(R.string.no_available_dir), // - Download folder doesn't use SAF while SAF is enabled
// - Download folder uses SAF but the user manually revoked access to it
Toast.makeText(context, getString(R.string.no_dir_yet),
Toast.LENGTH_LONG).show(); 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 Uri initialPath;
if (NewPipeSettings.useStorageAccessFramework(context)) {
initialPath = 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);
}
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
startActivityForResult(StoredFileHelper.getNewPicker(context,
filenameTmp, mimeTmp, initialPath), 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();
} }
@ -708,15 +753,14 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
// check if is our file // get state of potential mission referring to the same file
final MissionState state = downloadManager.checkForExistingMission(storage); final MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes @StringRes final int msgBtn;
final int msgBtn; @StringRes final int msgBody;
@StringRes
final int msgBody;
// this switch checks if there is already a mission referring to the same file
switch (state) { switch (state) {
case Finished: case Finished: // there is already a finished mission
msgBtn = R.string.overwrite; msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_warning; msgBody = R.string.overwrite_finished_warning;
break; break;
@ -728,7 +772,7 @@ public class DownloadDialog extends DialogFragment
msgBtn = R.string.generate_unique_name; msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running; msgBody = R.string.download_already_running;
break; break;
case None: case None: // there is no mission referring to the same file
if (mainStorage == null) { if (mainStorage == null) {
// This part is called if: // This part is called if:
// * using SAF on older android version // * using SAF on older android version
@ -763,7 +807,7 @@ public class DownloadDialog extends DialogFragment
msgBody = R.string.overwrite_unrelated_warning; msgBody = R.string.overwrite_unrelated_warning;
break; break;
default: default:
return; return; // unreachable
} }
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context) final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)

View File

@ -8,7 +8,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
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
@ -21,7 +20,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.nononsenseapps.filepicker.Utils
import com.xwray.groupie.Group import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item import com.xwray.groupie.Item
@ -52,17 +50,15 @@ 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
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.util.FilePickerActivityHelper import org.schabi.newpipe.streams.io.StoredFileHelper
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 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 +184,17 @@ 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, exportName, "application/json", null),
REQUEST_EXPORT_CODE
)
} }
private fun openReorderDialog() { private fun openReorderDialog() {
@ -207,23 +205,16 @@ 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!!)
val parentFile = exportFile.parentFile!!
if (!parentFile.canWrite() || !parentFile.canRead()) {
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, data.data)
) )
}
} 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)
) )
} }
} }
@ -295,7 +286,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private fun showLongTapDialog(selectedItem: ChannelInfoItem) { private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf( val commands = arrayOf(
getString(R.string.share), getString(R.string.open_in_browser), getString(R.string.share),
getString(R.string.open_in_browser),
getString(R.string.unsubscribe) getString(R.string.unsubscribe)
) )

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 org.schabi.newpipe.streams.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,10 +31,11 @@ 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 org.schabi.newpipe.streams.io.StoredFileHelper;
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;
@ -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,11 @@ 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.streams.io.StoredFileHelper;
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;
@ -55,6 +55,7 @@ import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.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

@ -6,12 +6,16 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.Objects;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected final boolean DEBUG = MainActivity.DEBUG; protected final boolean DEBUG = MainActivity.DEBUG;
@ -37,4 +41,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
super.onResume(); super.onResume();
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
} }
@NonNull
public final Preference requirePreference(@StringRes final int resId) {
final Preference preference = findPreference(getString(resId));
Objects.requireNonNull(preference);
return preference;
}
} }

View File

@ -1,21 +1,21 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.annotation.SuppressLint;
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;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
@ -26,28 +26,32 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.util.FilePathUtils; import org.schabi.newpipe.streams.io.StoredFileHelper;
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.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class ContentSettingsFragment extends BasePreferenceFragment { public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_IMPORT_PATH = 8945;
private static final int REQUEST_EXPORT_PATH = 30945; private static final int REQUEST_EXPORT_PATH = 30945;
private static final String ZIP_MIME_TYPE = "application/zip";
private static final SimpleDateFormat EXPORT_DATE_FORMAT
= new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ContentSettingsManager manager; private ContentSettingsManager manager;
private String importExportDataPathKey; private String importExportDataPathKey;
private String thumbnailLoadToggleKey; private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey; private String youtubeRestrictedModeEnabledKey;
@Nullable private Uri lastImportExportDataUri = null;
private Localization initialSelectedLocalization; private Localization initialSelectedLocalization;
private ContentCountry initialSelectedContentCountry; private ContentCountry initialSelectedContentCountry;
private String initialLanguage; private String initialLanguage;
@ -55,45 +59,35 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
@Override @Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext()); final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
manager.deleteSettingsFile(); manager.deleteSettingsFile();
addPreferencesFromResource(R.xml.content_settings);
importExportDataPathKey = getString(R.string.import_export_data_path); importExportDataPathKey = getString(R.string.import_export_data_path);
final Preference importDataPreference = findPreference(getString(R.string.import_data));
importDataPreference.setOnPreferenceClickListener(p -> {
final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_FILE);
final String path = defaultPreferences.getString(importExportDataPathKey, "");
if (FilePathUtils.isValidDirectoryPath(path)) {
i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path);
}
startActivityForResult(i, REQUEST_IMPORT_PATH);
return true;
});
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);
final String path = defaultPreferences.getString(importExportDataPathKey, "");
if (FilePathUtils.isValidDirectoryPath(path)) {
i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path);
}
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true;
});
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResource(R.xml.content_settings);
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
startActivityForResult(
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()),
REQUEST_IMPORT_PATH);
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
startActivityForResult(
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + EXPORT_DATE_FORMAT.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()),
REQUEST_EXPORT_PATH);
return true;
});
initialSelectedLocalization = org.schabi.newpipe.util.Localization initialSelectedLocalization = org.schabi.newpipe.util.Localization
.getPreferredLocalization(requireContext()); .getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization initialSelectedContentCountry = org.schabi.newpipe.util.Localization
@ -101,8 +95,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
initialLanguage = PreferenceManager initialLanguage = PreferenceManager
.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 = requirePreference(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();
@ -164,8 +157,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
} }
@Override @Override
public void onActivityResult(final int requestCode, final int resultCode, public void onActivityResult(final int requestCode,
@NonNull final Intent data) { final int resultCode,
@Nullable final Intent data) {
assureCorrectAppLanguage(getContext()); assureCorrectAppLanguage(getContext());
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) { if (DEBUG) {
@ -176,49 +170,45 @@ 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 File file = Utils.getFileForUri(data.getData());
lastImportExportDataUri = data.getData(); // will be saved only on success
final StoredFileHelper file
= new StoredFileHelper(getContext(), data.getData(), ZIP_MIME_TYPE);
if (requestCode == REQUEST_EXPORT_PATH) { if (requestCode == REQUEST_EXPORT_PATH) {
exportDatabase(file); exportDatabase(file);
} else { } else {
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(file)) (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();
} }
} }
} }
private void exportDatabase(@NonNull final File folder) { private void exportDatabase(final StoredFileHelper file) {
try { try {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
final String path = folder.getAbsolutePath() + "/NewPipeData-"
+ sdf.format(new Date()) + ".zip";
//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);
setImportExportDataPath(folder, false);
saveLastImportExportDataUri(false); // save export path only on success
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) {
ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e); ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
} }
} }
private void importDatabase(@NonNull final File file) { private void importDatabase(final StoredFileHelper file) {
final String filePath = file.getAbsolutePath();
// 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;
@ -229,29 +219,29 @@ 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);
alert.setNegativeButton(android.R.string.no, (dialog, which) -> { alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
finishImport(file); finishImport();
}); });
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
manager.loadSharedPreferences(PreferenceManager manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext())); .getDefaultSharedPreferences(requireContext()));
finishImport(file); finishImport();
}); });
alert.show(); alert.show();
} else { } else {
finishImport(file); finishImport();
} }
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e); ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e);
@ -260,39 +250,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
/** /**
* Save import path and restart system. * Save import path and restart system.
*
* @param file The file of the created backup
*/ */
private void finishImport(@NonNull final File file) { private void finishImport() {
if (file.getParentFile() != null) { // save import path only on success; save immediately because app is about to exit
//immediately because app is about to exit saveLastImportExportDataUri(true);
setImportExportDataPath(file.getParentFile(), true);
}
// restart app to properly load db // restart app to properly load db
System.exit(0); System.exit(0);
} }
@SuppressLint("ApplySharedPref") private Uri getImportExportDataUri() {
private void setImportExportDataPath(@NonNull final File file, final boolean immediately) { final String path = defaultPreferences.getString(importExportDataPathKey, null);
final String directoryPath; return isBlank(path) ? null : Uri.parse(path);
if (file.isDirectory()) {
directoryPath = file.getAbsolutePath();
} else {
final File parentFile = file.getParentFile();
if (parentFile != null) {
directoryPath = parentFile.getAbsolutePath();
} else {
directoryPath = "";
} }
}
final SharedPreferences.Editor editor = defaultPreferences private void saveLastImportExportDataUri(final boolean immediately) {
.edit() if (lastImportExportDataUri != null) {
.putString(importExportDataPathKey, directoryPath); final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, lastImportExportDataUri.toString());
if (immediately) { if (immediately) {
editor.commit(); // noinspection ApplySharedPref
editor.commit(); // app about to be restarted, commit immediately
} else { } else {
editor.apply(); editor.apply();
} }
} }
}
} }

View File

@ -1,6 +1,8 @@
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.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper import org.schabi.newpipe.util.ZipHelper
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.FileInputStream import java.io.FileInputStream
@ -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,12 @@ 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.NonNull;
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.SwitchPreferenceCompat;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
@ -26,7 +27,7 @@ import java.net.URI;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import us.shandian.giga.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -57,13 +58,23 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
prefPathAudio = findPreference(downloadPathAudioPreference); prefPathAudio = findPreference(downloadPathAudioPreference);
prefStorageAsk = findPreference(downloadStorageAsk); prefStorageAsk = findPreference(downloadStorageAsk);
final SwitchPreferenceCompat prefUseSaf = findPreference(storageUseSafPreference);
prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
prefUseSaf.setEnabled(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29);
} else {
prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_19);
}
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice);
}
updatePreferencesSummary(); updatePreferencesSummary();
updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
}
if (hasInvalidPath(downloadPathVideoPreference) if (hasInvalidPath(downloadPathVideoPreference)
|| hasInvalidPath(downloadPathAudioPreference)) { || hasInvalidPath(downloadPathAudioPreference)) {
updatePreferencesSummary(); updatePreferencesSummary();
@ -76,7 +87,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
} }
@Override @Override
public void onAttach(final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
ctx = context; ctx = context;
} }
@ -177,8 +188,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 +205,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

@ -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.util.Log; import android.util.Log;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -18,7 +19,7 @@ public final class SettingMigrations {
/** /**
* Version number for preferences. Must be incremented every time a migration is necessary. * Version number for preferences. Must be incremented every time a migration is necessary.
*/ */
public static final int VERSION = 2; public static final int VERSION = 3;
private static SharedPreferences sp; private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) { public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@ -54,6 +55,20 @@ public final class SettingMigrations {
} }
}; };
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
protected void migrate(final Context context) {
// Storage Access Framework implementation was improved in #5415, allowing the modern
// and standard way to access folders and files to be used consistently everywhere.
// We reset the setting to its default value, i.e. "use SAF", since now there are no
// more issues with SAF and users should use that one instead of the old
// NoNonsenseFilePicker. SAF does not work on KitKat and below, though, so the setting
// is set to false in that case.
sp.edit().putBoolean(context.getString(R.string.storage_use_saf),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP).apply();
}
};
/** /**
* List of all implemented migrations. * List of all implemented migrations.
* <p> * <p>
@ -62,7 +77,8 @@ public final class SettingMigrations {
*/ */
private static final Migration[] SETTING_MIGRATIONS = { private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1, MIGRATION_0_1,
MIGRATION_1_2 MIGRATION_1_2,
MIGRATION_2_3
}; };

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,52 @@
package org.schabi.newpipe.streams.io;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
/**
* Simply wraps a readable {@link SharpStream} allowing it to be used with built-in Java stuff that
* supports {@link InputStream}.
*/
public class SharpInputStream extends InputStream {
private final SharpStream stream;
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,46 @@
package org.schabi.newpipe.streams.io;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.OutputStream;
/**
* Simply wraps a writable {@link SharpStream} allowing it to be used with built-in Java stuff that
* supports {@link OutputStream}.
*/
public class SharpOutputStream extends OutputStream {
private final SharpStream stream;
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,20 @@
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. SharpStream is a wrapper around the 2 different APIs for SAF
* ({@link us.shandian.giga.io.FileStreamSAF}) and non-SAF ({@link us.shandian.giga.io.FileStream}).
* It has both input and output like in C#, while in Java those are usually different classes.
* {@link SharpInputStream} and {@link SharpOutputStream} are simple classes that wrap
* {@link SharpStream} and extend respectively {@link java.io.InputStream} and
* {@link java.io.OutputStream}, since unfortunately a class can only extend one class, so that a
* sharp stream can be used with built-in Java stuff that supports {@link java.io.InputStream}
* or {@link java.io.OutputStream}.
*/ */
public abstract class SharpStream implements Closeable { 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,6 +1,5 @@
package us.shandian.giga.io; package org.schabi.newpipe.streams.io;
import android.annotation.TargetApi;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -13,6 +12,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;
@ -21,10 +23,11 @@ import java.util.Collections;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StoredDirectoryHelper { public class StoredDirectoryHelper {
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
private File ioTree; private File ioTree;
private DocumentFile docTree; private DocumentFile docTree;
@ -33,7 +36,8 @@ public class StoredDirectoryHelper {
private final String tag; private final String tag;
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { public StoredDirectoryHelper(@NonNull final Context context, @NonNull final Uri path,
final String tag) throws IOException {
this.tag = tag; this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
@ -45,61 +49,59 @@ public class StoredDirectoryHelper {
try { try {
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS); this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
} catch (Exception e) { } catch (final Exception e) {
throw new IOException(e); throw new IOException(e);
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new IOException("Storage Access Framework with Directory API is not available"); throw new IOException("Storage Access Framework with Directory API is not available");
}
this.docTree = DocumentFile.fromTreeUri(context, path); this.docTree = DocumentFile.fromTreeUri(context, path);
if (this.docTree == null) if (this.docTree == null) {
throw new IOException("Failed to create the tree from Uri"); throw new IOException("Failed to create the tree from Uri");
} }
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location);
this.tag = tag;
} }
public StoredFileHelper createFile(String filename, String mime) { public StoredFileHelper createFile(final String filename, final String mime) {
return createFile(filename, mime, false); return createFile(filename, mime, false);
} }
public StoredFileHelper createUniqueFile(String name, String mime) { public StoredFileHelper createUniqueFile(final String name, final String mime) {
ArrayList<String> matches = new ArrayList<>(); final ArrayList<String> matches = new ArrayList<>();
String[] filename = splitFilename(name); final String[] filename = splitFilename(name);
String lcFilename = filename[0].toLowerCase(); final String lcFilename = filename[0].toLowerCase();
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
for (File file : ioTree.listFiles()) for (final File file : ioTree.listFiles()) {
addIfStartWith(matches, lcFilename, file.getName()); addIfStartWith(matches, lcFilename, file.getName());
}
} else { } else {
// warning: SAF file listing is very slow // warning: SAF file listing is very slow
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( final Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()) docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()));
);
String[] projection = {COLUMN_DISPLAY_NAME}; final String[] projection = new String[]{COLUMN_DISPLAY_NAME};
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; final String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
ContentResolver cr = context.getContentResolver(); final ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) { try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
new String[]{lcFilename}, null)) {
if (cursor != null) { if (cursor != null) {
while (cursor.moveToNext()) while (cursor.moveToNext()) {
addIfStartWith(matches, lcFilename, cursor.getString(0)); addIfStartWith(matches, lcFilename, cursor.getString(0));
} }
} }
} }
}
if (matches.size() < 1) { if (matches.size() < 1) {
return createFile(name, mime, true); return createFile(name, mime, true);
} else { } else {
// check if the filename is in use // check if the filename is in use
String lcName = name.toLowerCase(); String lcName = name.toLowerCase();
for (String testName : matches) { for (final String testName : matches) {
if (testName.equals(lcName)) { if (testName.equals(lcName)) {
lcName = null; lcName = null;
break; break;
@ -107,28 +109,34 @@ public class StoredDirectoryHelper {
} }
// check if not in use // check if not in use
if (lcName != null) return createFile(name, mime, true); if (lcName != null) {
return createFile(name, mime, true);
}
} }
Collections.sort(matches, String::compareTo); Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) { for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) {
return createFile(makeFileName(filename[0], i, filename[1]), mime, true); return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
} }
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
} }
private StoredFileHelper createFile(String filename, String mime, boolean safe) { return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime,
StoredFileHelper storage; false);
}
private StoredFileHelper createFile(final String filename, final String mime,
final boolean safe) {
final StoredFileHelper storage;
try { try {
if (docTree == null) if (docTree == null) {
storage = new StoredFileHelper(ioTree, filename, mime); storage = new StoredFileHelper(ioTree, filename, mime);
else } else {
storage = new StoredFileHelper(context, docTree, filename, mime, safe); storage = new StoredFileHelper(context, docTree, filename, mime, safe);
} catch (IOException e) { }
} catch (final IOException e) {
return null; return null;
} }
@ -146,7 +154,7 @@ public class StoredDirectoryHelper {
} }
/** /**
* Indicates whatever if is possible access using the {@code java.io} API * Indicates whether it's using the {@code java.io} API.
* *
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/ */
@ -169,7 +177,9 @@ public class StoredDirectoryHelper {
return ioTree.exists() || ioTree.mkdirs(); return ioTree.exists() || ioTree.mkdirs();
} }
if (docTree.exists()) return true; if (docTree.exists()) {
return true;
}
try { try {
DocumentFile parent; DocumentFile parent;
@ -177,14 +187,18 @@ public class StoredDirectoryHelper {
while (true) { while (true) {
parent = docTree.getParentFile(); parent = docTree.getParentFile();
if (parent == null || child == null) break; if (parent == null || child == null) {
if (parent.exists()) return true; break;
}
if (parent.exists()) {
return true;
}
parent.createDirectory(child); parent.createDirectory(child);
child = parent.getName();// for the next iteration child = parent.getName(); // for the next iteration
} }
} catch (Exception e) { } catch (final Exception ignored) {
// no more parent directories or unsupported by the storage provider // no more parent directories or unsupported by the storage provider
} }
@ -195,13 +209,13 @@ public class StoredDirectoryHelper {
return tag; return tag;
} }
public Uri findFile(String filename) { public Uri findFile(final String filename) {
if (docTree == null) { if (docTree == null) {
File res = new File(ioTree, filename); final File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null; return res.exists() ? Uri.fromFile(res) : null;
} }
DocumentFile res = findFileSAFHelper(context, docTree, filename); final DocumentFile res = findFileSAFHelper(context, docTree, filename);
return res == null ? null : res.getUri(); return res == null ? null : res.getUri();
} }
@ -209,82 +223,115 @@ public class StoredDirectoryHelper {
return docTree == null ? ioTree.canWrite() : docTree.canWrite(); return docTree == null ? ioTree.canWrite() : docTree.canWrite();
} }
/**
* @return {@code false} if the storage is direct, or the SAF storage is valid; {@code true} if
* SAF access to this SAF storage is denied (e.g. the user clicked on {@code Android settings ->
* Apps & notifications -> NewPipe -> Storage & cache -> Clear access});
*/
public boolean isInvalidSafStorage() {
return docTree != null && docTree.getName() == null;
}
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString(); return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString();
} }
//////////////////// ////////////////////
// Utils // Utils
/////////////////// ///////////////////
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) { private static void addIfStartWith(final ArrayList<String> list, @NonNull final String base,
if (str == null || str.isEmpty()) return; final String str) {
str = str.toLowerCase(); if (isNullOrEmpty(str)) {
if (str.startsWith(base)) list.add(str); return;
}
final String lowerStr = str.toLowerCase();
if (lowerStr.startsWith(base)) {
list.add(lowerStr);
}
} }
private static String[] splitFilename(@NonNull String filename) { private static String[] splitFilename(@NonNull final String filename) {
int dotIndex = filename.lastIndexOf('.'); final int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
return new String[]{filename, ""}; return new String[]{filename, ""};
}
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)}; return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
} }
private static String makeFileName(String name, int idx, String ext) { private static String makeFileName(final String name, final int idx, final String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
} }
/** /**
* Fast (but not enough) file/directory finder under the storage access framework * Fast (but not enough) file/directory finder under the storage access framework.
* *
* @param context The context * @param context The context
* @param tree Directory where search * @param tree Directory where search
* @param filename Target filename * @param filename Target filename
* @return A {@link DocumentFile} contain the reference, otherwise, null * @return A {@link DocumentFile} contain the reference, otherwise, null
*/ */
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) { static DocumentFile findFileSAFHelper(@Nullable final Context context, final DocumentFile tree,
final String filename) {
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return tree.findFile(filename);// warning: this is very slow return tree.findFile(filename); // warning: this is very slow
} }
if (!tree.canRead()) return null;// missing read permission if (!tree.canRead()) {
return null; // missing read permission
}
final int name = 0; final int name = 0;
final int documentId = 1; final int documentId = 1;
// LOWER() SQL function is not supported // LOWER() SQL function is not supported
String selection = COLUMN_DISPLAY_NAME + " = ?"; final String selection = COLUMN_DISPLAY_NAME + " = ?";
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%"; //final String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(tree.getUri(),
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri()) DocumentsContract.getDocumentId(tree.getUri()));
); final String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; final ContentResolver contentResolver = context.getContentResolver();
ContentResolver contentResolver = context.getContentResolver();
filename = filename.toLowerCase(); final String lowerFilename = filename.toLowerCase();
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) { try (Cursor cursor = contentResolver.query(childrenUri, projection, selection,
if (cursor == null) return null; new String[]{lowerFilename}, null)) {
if (cursor == null) {
return null;
}
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename)) if (cursor.isNull(name)
|| !cursor.getString(name).toLowerCase().startsWith(lowerFilename)) {
continue; continue;
}
return DocumentFile.fromSingleUri( return DocumentFile.fromSingleUri(context,
context, DocumentsContract.buildDocumentUriUsingTree( DocumentsContract.buildDocumentUriUsingTree(tree.getUri(),
tree.getUri(), cursor.getString(documentId) cursor.getString(documentId)));
)
);
} }
} }
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

@ -0,0 +1,554 @@
package org.schabi.newpipe.streams.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
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 com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import us.shandian.giga.io.FileStream;
import us.shandian.giga.io.FileStreamSAF;
public class StoredFileHelper implements Serializable {
private static final long serialVersionUID = 0L;
public static final String DEFAULT_MIME = "application/octet-stream";
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient Context context;
protected String source;
private String sourceTree;
protected String tag;
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 final Uri parent, final String filename, final String mime,
final String tag) {
this.source = null; // this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) {
this.sourceTree = parent.toString();
}
this.tag = tag;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(@Nullable final Context context, final DocumentFile tree,
final String filename, final String mime, final boolean safe)
throws IOException {
this.docTree = tree;
this.context = context;
final DocumentFile res;
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) {
throw new IOException("Cannot create the file");
}
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
StoredFileHelper(final File location, final String filename, final String mime)
throws IOException {
this.ioFile = new File(location, filename);
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete()) {
throw new IOException("The filename is already in use by non-file entity "
+ "and cannot overwrite it");
}
} else {
if (!this.ioFile.createNewFile()) {
throw new IOException("Cannot create the file");
}
}
this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(final Context context, @Nullable final Uri parent,
@NonNull final Uri path, final String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
final DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) {
throw new RuntimeException("SAF not available");
}
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) {
this.docTree = DocumentFile.fromTreeUri(context, parent);
}
this.sourceTree = parent.toString();
}
this.srcName = getName();
this.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull final StoredFileHelper storage,
final Context context) throws IOException {
final Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid()) {
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
}
final StoredFileHelper instance = new StoredFileHelper(context, treeUri,
Uri.parse(storage.source), storage.tag);
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) {
instance.srcName = storage.srcName;
}
if (instance.srcType == null) {
instance.srcType = storage.srcType;
}
return instance;
}
public SharpStream getStream() throws IOException {
assertValid();
if (docFile == null) {
return new FileStream(ioFile);
} else {
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
}
}
/**
* Indicates whether it's using the {@code java.io} API.
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
assertValid();
return docFile == null;
}
public boolean isInvalid() {
return source == null;
}
public Uri getUri() {
assertValid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
}
public Uri getParentUri() {
assertValid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
assertValid();
try (SharpStream fs = getStream()) {
fs.setLength(0);
}
}
public boolean delete() {
if (source == null) {
return true;
}
if (docFile == null) {
return ioFile.delete();
}
final boolean res = docFile.delete();
try {
final int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (final Exception ex) {
// nothing to do
}
return res;
}
public long length() {
assertValid();
return docFile == null ? ioFile.length() : docFile.length();
}
public boolean canWrite() {
if (source == null) {
return false;
}
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public String getName() {
if (source == null) {
return srcName;
} else if (docFile == null) {
return ioFile.getName();
}
final String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null || docFile == null) {
return srcType;
}
final String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
return tag;
}
public boolean existsAsFile() {
if (source == null) {
return false;
}
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
// docFile.isVirtual() means it is non-physical?
return docFile == null
? (ioFile.exists() && ioFile.isFile())
: (docFile.exists() && docFile.isFile());
}
public boolean create() {
assertValid();
final boolean result;
if (docFile == null) {
try {
result = ioFile.createNewFile();
} catch (final IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) {
return false;
}
try {
docFile = createSAF(context, srcType, srcName);
if (docFile.getName() == null) {
return false;
}
result = true;
} catch (final IOException e) {
return false;
}
}
if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
return result;
}
public void invalidate() {
if (source == null) {
return;
}
srcName = getName();
srcType = getType();
source = null;
docTree = null;
docFile = null;
ioFile = null;
context = null;
}
public boolean equals(final StoredFileHelper storage) {
if (this == storage) {
return true;
}
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree))) {
return false;
}
if (this.isInvalid() || storage.isInvalid()) {
if (this.srcName == null || storage.srcName == null || this.srcType == null
|| storage.srcType == null) {
return false;
}
return this.srcName.equalsIgnoreCase(storage.srcName)
&& this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) {
return false;
}
if (this.isDirect()) {
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
}
return DocumentsContract.getDocumentId(this.docFile.getUri())
.equalsIgnoreCase(DocumentsContract.getDocumentId(storage.docFile.getUri()));
}
@NonNull
@Override
public String toString() {
if (source == null) {
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
} else {
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree)
+ " tag=" + tag;
}
}
private void assertValid() {
if (source == null) {
throw new IllegalStateException("In invalid state");
}
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(),
StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (final Exception e) {
if (docFile.getName() == null) {
throw new IOException(e);
}
}
}
@NonNull
private DocumentFile createSAF(@Nullable final Context ctx, final String mime,
final String filename) throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(ctx, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete()) {
throw new IOException("Directory with the same name found but cannot delete");
}
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) {
throw new IOException("Cannot create the file");
}
}
return res;
}
private String getLowerCase(final String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(final String str1, final String str2) {
if (str1 == null && str2 == null) {
return false;
}
if ((str1 == null) != (str2 == null)) {
return true;
}
return !str1.equals(str2);
}
public static Intent getPicker(@NonNull final Context ctx) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.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 getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) {
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null);
}
public static Intent getNewPicker(@NonNull final Context ctx,
@Nullable final String filename,
@NonNull final String mimeType,
@Nullable final Uri initialPath) {
final Intent i;
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
i = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.setType(mimeType)
.addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
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);
}
return applyInitialPathToPickerIntent(ctx, i, initialPath, filename);
}
private static Intent applyInitialPathToPickerIntent(@NonNull final Context ctx,
@NonNull final Intent intent,
@Nullable final Uri initialPath,
@Nullable final String filename) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
if (initialPath == null) {
return intent; // nothing to do, no initial path provided
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialPath);
} else {
return intent; // can't set initial path on API < 26
}
} else {
if (initialPath == null && filename == null) {
return intent; // nothing to do, no initial path and no file name provided
}
File file;
if (initialPath == null) {
// The only way to set the previewed filename in non-SAF FilePicker is to set a
// starting path ending with that filename. So when the initialPath is null but
// filename isn't just default to the external storage directory.
file = Environment.getExternalStorageDirectory();
} else {
try {
file = Utils.getFileForUri(initialPath);
} catch (final Throwable ignored) {
// getFileForUri() can't decode paths to 'storage', fallback to this
file = new File(initialPath.toString());
}
}
// remove any filename at the end of the path (get the parent directory in that case)
if (!file.exists() || !file.isDirectory()) {
file = file.getParentFile();
if (file == null || !file.exists()) {
// default to the external storage directory in case of an invalid path
file = Environment.getExternalStorageDirectory();
}
// else: file is surely a directory
}
if (filename != null) {
// append a filename so that the non-SAF FilePicker shows it as the preview
file = new File(file, filename);
}
return intent
.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, file.getAbsolutePath());
}
}
}

View File

@ -1,22 +0,0 @@
package org.schabi.newpipe.util;
import java.io.File;
public final class FilePathUtils {
private FilePathUtils() { }
/**
* Check that the path is a valid directory path and it exists.
*
* @param path full path of directory,
* @return is path valid or not
*/
public static boolean isValidDirectoryPath(final String path) {
if (path == null || path.isEmpty()) {
return false;
}
final File file = new File(path);
return file.exists() && file.isDirectory();
}
}

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

@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings;
public final class PermissionHelper { public final class PermissionHelper {
public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778; public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778;
@ -26,6 +27,10 @@ public final class PermissionHelper {
private PermissionHelper() { } private PermissionHelper() { }
public static boolean checkStoragePermissions(final Activity activity, final int requestCode) { public static boolean checkStoragePermissions(final Activity activity, final int requestCode) {
if (NewPipeSettings.useStorageAccessFramework(activity)) {
return true; // Storage permissions are not needed for SAF
}
if (!checkReadStoragePermissions(activity, requestCode)) { if (!checkReadStoragePermissions(activity, requestCode)) {
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 org.schabi.newpipe.streams.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

@ -26,7 +26,7 @@ import java.util.Objects;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;

View File

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
import java.io.Serializable; import java.io.Serializable;
import java.util.Calendar; import java.util.Calendar;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
public abstract class Mission implements Serializable { public abstract class Mission implements Serializable {
private static final long serialVersionUID = 1L;// last bump: 27 march 2019 private static final long serialVersionUID = 1L;// last bump: 27 march 2019
@ -25,6 +25,10 @@ public abstract class Mission implements Serializable {
*/ */
public long timestamp; public long timestamp;
public long getTimestamp() {
return timestamp;
}
/** /**
* pre-defined content type * pre-defined content type
*/ */
@ -35,10 +39,6 @@ public abstract class Mission implements Serializable {
*/ */
public StoredFileHelper storage; public StoredFileHelper storage;
public long getTimestamp() {
return timestamp;
}
/** /**
* Delete the downloaded file * Delete the downloaded file
* *
@ -57,7 +57,7 @@ public abstract class Mission implements Serializable {
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
Calendar calendar = Calendar.getInstance(); final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp); calendar.setTimeInMillis(timestamp);
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
} }

View File

@ -17,7 +17,7 @@ import java.util.Objects;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission; import us.shandian.giga.get.Mission;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
/** /**
* SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s * SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s

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

@ -1,386 +0,0 @@
package us.shandian.giga.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
public class StoredFileHelper implements Serializable {
private static final long serialVersionUID = 0L;
public static final String DEFAULT_MIME = "application/octet-stream";
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient Context context;
protected String source;
private String sourceTree;
protected String tag;
private String srcName;
private String srcType;
public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) this.sourceTree = parent.toString();
this.tag = tag;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
this.docTree = tree;
this.context = context;
DocumentFile res;
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) throw new IOException("Cannot create the file");
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
StoredFileHelper(File location, String filename, String mime) throws IOException {
this.ioFile = new File(location, filename);
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete())
throw new IOException("The filename is already in use by non-file entity and cannot overwrite it");
} else {
if (!this.ioFile.createNewFile())
throw new IOException("Cannot create the file");
}
this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) throw new RuntimeException("SAF not available");
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
this.docTree = DocumentFile.fromTreeUri(context, parent);
this.sourceTree = parent.toString();
}
this.srcName = getName();
this.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid())
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) instance.srcName = storage.srcName;
if (instance.srcType == null) instance.srcType = storage.srcType;
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();
if (docFile == null)
return new FileStream(ioFile);
else
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
invalid();
return docFile == null;
}
public boolean isInvalid() {
return source == null;
}
public Uri getUri() {
invalid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
}
public Uri getParentUri() {
invalid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
invalid();
try (SharpStream fs = getStream()) {
fs.setLength(0);
}
}
public boolean delete() {
if (source == null) return true;
if (docFile == null) return ioFile.delete();
boolean res = docFile.delete();
try {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (Exception ex) {
// nothing to do
}
return res;
}
public long length() {
invalid();
return docFile == null ? ioFile.length() : docFile.length();
}
public boolean canWrite() {
if (source == null) return false;
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public String getName() {
if (source == null)
return srcName;
else if (docFile == null)
return ioFile.getName();
String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null || docFile == null)
return srcType;
String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
return tag;
}
public boolean existsAsFile() {
if (source == null) return false;
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
return exists && isFile;
}
public boolean create() {
invalid();
boolean result;
if (docFile == null) {
try {
result = ioFile.createNewFile();
} catch (IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) return false;
try {
docFile = createSAF(context, srcType, srcName);
if (docFile.getName() == null) return false;
result = true;
} catch (IOException e) {
return false;
}
}
if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
return result;
}
public void invalidate() {
if (source == null) return;
srcName = getName();
srcType = getType();
source = null;
docTree = null;
docFile = null;
ioFile = null;
context = null;
}
public boolean equals(StoredFileHelper storage) {
if (this == storage) return true;
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
return false;
if (this.isInvalid() || storage.isInvalid()) {
if (this.srcName == null || storage.srcName == null || this.srcType == null || storage.srcType == null) return false;
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) return false;
if (this.isDirect())
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
return DocumentsContract.getDocumentId(
this.docFile.getUri()
).equalsIgnoreCase(DocumentsContract.getDocumentId(
storage.docFile.getUri()
));
}
@NonNull
@Override
public String toString() {
if (source == null)
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
else
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
}
private void invalid() {
if (source == null)
throw new IllegalStateException("In invalid state");
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (Exception e) {
if (docFile.getName() == null) throw new IOException(e);
}
}
@NonNull
private DocumentFile createSAF(@Nullable Context context, String mime, String filename)
throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete())
throw new IOException("Directory with the same name found but cannot delete");
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) throw new IOException("Cannot create the file");
}
return res;
}
private String getLowerCase(String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(String str1, String str2) {
if (str1 == null && str2 == null) return false;
if ((str1 == null) != (str2 == null)) return true;
return !str1.equals(str2);
}
}

View File

@ -19,8 +19,8 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission; import us.shandian.giga.get.Mission;
import us.shandian.giga.get.sqlite.FinishedMissionStore; import us.shandian.giga.get.sqlite.FinishedMissionStore;
import us.shandian.giga.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -106,7 +106,8 @@ public class DownloadManager {
} }
/** /**
* Loads finished missions from the data source * Loads finished missions from the data source and forgets finished missions whose file does
* not exist anymore.
*/ */
private ArrayList<FinishedMission> loadFinishedMissions() { private ArrayList<FinishedMission> loadFinishedMissions() {
ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions(); ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions();
@ -331,14 +332,29 @@ public class DownloadManager {
} }
/** /**
* Get a finished mission by its path * Get the index into {@link #mMissionsFinished} of a finished mission by its path, return
* {@code -1} if there is no such mission. This function also checks if the matched mission's
* file exists, and, if it does not, the related mission is forgotten about (like in {@link
* #loadFinishedMissions()}) and {@code -1} is returned.
* *
* @param storage where the file possible is stored * @param storage where the file would be stored
* @return the mission index or -1 if no such mission exists * @return the mission index or -1 if no such mission exists
*/ */
private int getFinishedMissionIndex(StoredFileHelper storage) { private int getFinishedMissionIndex(StoredFileHelper storage) {
for (int i = 0; i < mMissionsFinished.size(); i++) { for (int i = 0; i < mMissionsFinished.size(); i++) {
if (mMissionsFinished.get(i).storage.equals(storage)) { if (mMissionsFinished.get(i).storage.equals(storage)) {
// If the file does not exist the mission is not valid anymore. Also checking if
// length == 0 since the file picker may create an empty file before yielding it,
// but that does not mean the file really belonged to a previous mission.
if (!storage.existsAsFile() || storage.length() == 0) {
if (DEBUG) {
Log.d(TAG, "matched downloaded file removed: " + storage.getName());
}
mFinishedMissionStore.deleteMission(mMissionsFinished.get(i));
mMissionsFinished.remove(i);
return -1; // finished mission whose associated file was removed
}
return i; return i;
} }
} }

View File

@ -47,8 +47,8 @@ import java.util.ArrayList;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState; import us.shandian.giga.service.DownloadManager.NetworkState;

View File

@ -61,7 +61,7 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission; import us.shandian.giga.get.Mission;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.Deleter; import us.shandian.giga.ui.common.Deleter;

View File

@ -29,14 +29,13 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
@ -242,27 +241,21 @@ public class MissionsFragment extends Fragment {
private void recoverMission(@NonNull DownloadMission mission) { private void recoverMission(@NonNull DownloadMission mission) {
unsafeMissionTarget = mission; unsafeMissionTarget = mission;
final Uri initialPath;
if (NewPipeSettings.useStorageAccessFramework(mContext)) { if (NewPipeSettings.useStorageAccessFramework(mContext)) {
StoredFileHelper.requestSafWithFileCreation( initialPath = 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
);
} }
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
startActivityForResult(StoredFileHelper.getNewPicker(mContext, mission.storage.getName(),
mission.storage.getType(), initialPath), REQUEST_DOWNLOAD_SAVE_AS);
} }
@Override @Override

View File

@ -29,7 +29,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Locale; import java.util.Locale;
import us.shandian.giga.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
public class Utility { public class Utility {

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 -->
@ -640,10 +641,12 @@
<string name="start_downloads">Start downloads</string> <string name="start_downloads">Start downloads</string>
<string name="pause_downloads">Pause downloads</string> <string name="pause_downloads">Pause downloads</string>
<string name="downloads_storage_ask_title">Ask where to download</string> <string name="downloads_storage_ask_title">Ask where to download</string>
<string name="downloads_storage_ask_summary">You will be asked where to save each download</string> <string name="downloads_storage_ask_summary">You will be asked where to save each download.\nEnable the system folder picker (SAF) if you want to download to an external SD card</string>
<string name="downloads_storage_ask_summary_kitkat">You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card</string> <string name="downloads_storage_ask_summary_no_saf_notice">You will be asked where to save each download</string>
<string name="downloads_storage_use_saf_title">Use SAF</string> <string name="downloads_storage_use_saf_title">Use system folder picker (SAF)</string>
<string name="downloads_storage_use_saf_summary">The \'Storage Access Framework\' allows downloads to an external SD card.\nSome devices are incompatible</string> <string name="downloads_storage_use_saf_summary">The \'Storage Access Framework\' allows downloads to an external SD card</string>
<string name="downloads_storage_use_saf_summary_api_19">The \'Storage Access Framework\' is not supported on Android KitKat and below</string>
<string name="downloads_storage_use_saf_summary_api_29">Starting from Android 10 only \'Storage Access Framework\' is supported</string>
<string name="choose_instance_prompt">Choose an instance</string> <string name="choose_instance_prompt">Choose an instance</string>
<string name="app_language_title">App language</string> <string name="app_language_title">App language</string>
<string name="systems_language">System default</string> <string name="systems_language">System default</string>

View File

@ -3,16 +3,14 @@
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">
<SwitchPreferenceCompat
<CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="@string/downloads_storage_ask" android:key="@string/downloads_storage_ask"
android:summary="@string/downloads_storage_ask_summary_kitkat" android:summary="@string/downloads_storage_ask_summary"
android:title="@string/downloads_storage_ask_title" android:title="@string/downloads_storage_ask_title"
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"

View File

@ -17,6 +17,8 @@ import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.withSettings import org.mockito.Mockito.withSettings
import org.mockito.junit.MockitoJUnitRunner import org.mockito.junit.MockitoJUnitRunner
import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
import java.io.File import java.io.File
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.nio.file.Files import java.nio.file.Files
@ -30,10 +32,12 @@ class ContentSettingsManagerTest {
} }
private lateinit var fileLocator: NewPipeFileLocator private lateinit var fileLocator: NewPipeFileLocator
private lateinit var storedFileHelper: StoredFileHelper
@Before @Before
fun setupFileLocator() { fun setupFileLocator() {
fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly()) fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly())
storedFileHelper = Mockito.mock(StoredFileHelper::class.java, withSettings().stubOnly())
} }
@Test @Test
@ -44,11 +48,13 @@ class ContentSettingsManagerTest {
`when`(fileLocator.settings).thenReturn(newpipeSettings) `when`(fileLocator.settings).thenReturn(newpipeSettings)
val expectedPreferences = mapOf("such pref" to "much wow") val expectedPreferences = mapOf("such pref" to "much wow")
val sharedPreferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) val sharedPreferences =
Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
`when`(sharedPreferences.all).thenReturn(expectedPreferences) `when`(sharedPreferences.all).thenReturn(expectedPreferences)
val output = File.createTempFile("newpipe_", "") val output = File.createTempFile("newpipe_", "")
ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, output.absolutePath) `when`(storedFileHelper.stream).thenReturn(FileStream(output))
ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper)
val zipFile = ZipFile(output) val zipFile = ZipFile(output)
val entries = zipFile.entries().toList() val entries = zipFile.entries().toList()
@ -117,7 +123,8 @@ class ContentSettingsManagerTest {
`when`(fileLocator.dbWal).thenReturn(dbWal) `when`(fileLocator.dbWal).thenReturn(dbWal)
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
val success = ContentSettingsManager(fileLocator).extractDb(zip.path) `when`(storedFileHelper.stream).thenReturn(FileStream(zip))
val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper)
assertTrue(success) assertTrue(success)
assertFalse(dbJournal.exists()) assertFalse(dbJournal.exists())
@ -135,7 +142,8 @@ class ContentSettingsManagerTest {
`when`(fileLocator.db).thenReturn(db) `when`(fileLocator.db).thenReturn(db)
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
val success = ContentSettingsManager(fileLocator).extractDb(emptyZip.path) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper)
assertFalse(success) assertFalse(success)
assertTrue(dbJournal.exists()) assertTrue(dbJournal.exists())
@ -150,7 +158,8 @@ class ContentSettingsManagerTest {
`when`(fileLocator.settings).thenReturn(settings) `when`(fileLocator.settings).thenReturn(settings)
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
val contains = ContentSettingsManager(fileLocator).extractSettings(zip.path) `when`(storedFileHelper.stream).thenReturn(FileStream(zip))
val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper)
assertTrue(contains) assertTrue(contains)
} }
@ -161,7 +170,8 @@ class ContentSettingsManagerTest {
`when`(fileLocator.settings).thenReturn(settings) `when`(fileLocator.settings).thenReturn(settings)
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
val contains = ContentSettingsManager(fileLocator).extractSettings(emptyZip.path) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper)
assertFalse(contains) assertFalse(contains)
} }

View File

@ -1,56 +0,0 @@
package org.schabi.newpipe.util;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class FilePathHelperTest {
private Path dir;
@Before
public void setUp() throws IOException {
dir = Files.createTempDirectory("dir1");
}
@Test
public void testIsValidDirectoryPathWithEmptyString() {
assertFalse(FilePathUtils.isValidDirectoryPath(""));
}
@Test
public void testIsValidDirectoryPathWithNullString() {
assertFalse(FilePathUtils.isValidDirectoryPath(null));
}
@Test
public void testIsValidDirectoryPathWithValidPath() {
assertTrue(FilePathUtils.isValidDirectoryPath(dir.toAbsolutePath().toString()));
}
@Test
public void testIsValidDirectoryPathWithDeepValidDirectory() throws IOException {
final File subDir = Files.createDirectory(dir.resolve("subdir")).toFile();
assertTrue(FilePathUtils.isValidDirectoryPath(subDir.getAbsolutePath()));
}
@Test
public void testIsValidDirectoryPathWithNotExistDirectory() {
assertFalse(FilePathUtils.isValidDirectoryPath(dir.resolve("not-exists-subdir").
toFile().getAbsolutePath()));
}
@Test
public void testIsValidDirectoryPathWithFile() throws IOException {
final File tempFile = Files.createFile(dir.resolve("simple_file")).toFile();
assertFalse(FilePathUtils.isValidDirectoryPath(tempFile.getAbsolutePath()));
}
}