commit
c96bdfcb32
|
@ -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">
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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) {
|
||||||
if (data.getData() == null) {
|
return;
|
||||||
showFailedDialog(R.string.general_error);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (data.getData() == null) {
|
||||||
|
showFailedDialog(R.string.general_error);
|
||||||
|
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
|
||||||
|
// - Download folder uses SAF while SAF is disabled
|
||||||
|
// - Download folder doesn't use SAF while SAF is enabled
|
||||||
|
// - Download folder uses SAF but the user manually revoked access to it
|
||||||
|
Toast.makeText(context, getString(R.string.no_dir_yet),
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
if (!askForSavePath) {
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||||
Toast.makeText(context, getString(R.string.no_available_dir),
|
startActivityForResult(StoredDirectoryHelper.getPicker(context),
|
||||||
Toast.LENGTH_LONG).show();
|
REQUEST_DOWNLOAD_PICK_AUDIO_FOLDER);
|
||||||
}
|
|
||||||
|
|
||||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
|
||||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
|
|
||||||
filename, mime);
|
|
||||||
} else {
|
} else {
|
||||||
File initialSavePath;
|
startActivityForResult(StoredDirectoryHelper.getPicker(context),
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
REQUEST_DOWNLOAD_PICK_VIDEO_FOLDER);
|
||||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
|
||||||
} else {
|
|
||||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialSavePath = new File(initialSavePath, filename);
|
|
||||||
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
|
|
||||||
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -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!!)
|
activity.startService(
|
||||||
val parentFile = exportFile.parentFile!!
|
Intent(activity, SubscriptionsExportService::class.java)
|
||||||
if (!parentFile.canWrite() || !parentFile.canRead()) {
|
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, data.data)
|
||||||
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
|
)
|
||||||
} else {
|
|
||||||
activity.startService(
|
|
||||||
Intent(activity, SubscriptionsExportService::class.java)
|
|
||||||
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} 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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,51 +170,47 @@ 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 {
|
private void saveLastImportExportDataUri(final boolean immediately) {
|
||||||
final File parentFile = file.getParentFile();
|
if (lastImportExportDataUri != null) {
|
||||||
if (parentFile != null) {
|
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||||
directoryPath = parentFile.getAbsolutePath();
|
.putString(importExportDataPathKey, lastImportExportDataUri.toString());
|
||||||
|
if (immediately) {
|
||||||
|
// noinspection ApplySharedPref
|
||||||
|
editor.commit(); // app about to be restarted, commit immediately
|
||||||
} else {
|
} else {
|
||||||
directoryPath = "";
|
editor.apply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final SharedPreferences.Editor editor = defaultPreferences
|
|
||||||
.edit()
|
|
||||||
.putString(importExportDataPathKey, directoryPath);
|
|
||||||
if (immediately) {
|
|
||||||
editor.commit();
|
|
||||||
} else {
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,32 +68,36 @@ 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) {
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
if (!useStorageAccessFramework(context)) {
|
||||||
final String key = context.getString(keyID);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String downloadPath = prefs.getString(key, null);
|
final String key = context.getString(keyID);
|
||||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) {
|
final String downloadPath = prefs.getString(key, null);
|
||||||
return;
|
if (!isNullOrEmpty(downloadPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SharedPreferences.Editor spEditor = prefs.edit();
|
||||||
|
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||||
|
spEditor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
final SharedPreferences.Editor spEditor = prefs.edit();
|
|
||||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
|
||||||
spEditor.apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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,51 +49,49 @@ 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 StoredFileHelper createFile(final String filename, final String mime) {
|
||||||
public StoredDirectoryHelper(@NonNull URI location, String tag) {
|
|
||||||
ioTree = new File(location);
|
|
||||||
this.tag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StoredFileHelper createFile(String filename, 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +101,7 @@ public class StoredDirectoryHelper {
|
||||||
} 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);
|
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
|
private StoredFileHelper createFile(final String filename, final String mime,
|
||||||
StoredFileHelper storage;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()),
|
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
||||||
REQUEST_DOWNLOAD_SAVE_AS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startActivityForResult(StoredFileHelper.getNewPicker(mContext, mission.storage.getName(),
|
||||||
|
mission.storage.getType(), initialPath), REQUEST_DOWNLOAD_SAVE_AS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue