diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c24c911931b..9f24b28c4c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,7 +21,6 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:logo="@mipmap/ic_launcher" - android:requestLegacyExternalStorage="true" android:theme="@style/OpeningTheme" tools:ignore="AllowBackup"> () { } private fun onImportPreviousSelected() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE) + startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_CODE) } private fun onExportSelected() { val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val exportName = "newpipe_subscriptions_$date.json" - val exportFile = File(Environment.getExternalStorageDirectory(), exportName) - startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE) + startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName), REQUEST_EXPORT_CODE) } private fun openReorderDialog() { @@ -204,18 +202,16 @@ class SubscriptionFragment : BaseStateFragment() { super.onActivityResult(requestCode, resultCode, data) if (data != null && data.data != null && resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_EXPORT_CODE) { - val exportFile = Utils.getFileForUri(data.data!!) - if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) { - Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() - } else { - activity.startService(Intent(activity, SubscriptionsExportService::class.java) - .putExtra(KEY_FILE_PATH, exportFile.absolutePath)) + var uri = data.data!! + if (FilePickerActivityHelper.isOwnFileUri(activity, uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)) } + activity.startService(Intent(activity, SubscriptionsExportService::class.java) + .putExtra(SubscriptionsExportService.KEY_FILE_PATH, uri)) } else if (requestCode == REQUEST_IMPORT_CODE) { - val path = Utils.getFileForUri(data.data!!).absolutePath ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java) .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) - .putExtra(KEY_VALUE, path)) + .putExtra(KEY_VALUE, data.data)) } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index d812a2a576d..7cb0c3750c1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -18,8 +18,6 @@ import androidx.appcompat.app.ActionBar; import androidx.core.text.util.LinkifyCompat; -import com.nononsenseapps.filepicker.Utils; - import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -29,13 +27,13 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ServiceHelper; import java.util.Collections; import java.util.List; import icepick.State; +import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL; import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; @@ -172,8 +170,7 @@ public void onImportUrl(final String value) { } public void onImportFile() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), - REQUEST_IMPORT_FILE_CODE); + startActivityForResult(StoredFileHelper.getPicker(activity), REQUEST_IMPORT_FILE_CODE); } @Override @@ -185,10 +182,10 @@ public void onActivityResult(final int requestCode, final int resultCode, final if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) { - final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) - .putExtra(KEY_MODE, INPUT_STREAM_MODE).putExtra(KEY_VALUE, path) + .putExtra(KEY_MODE, INPUT_STREAM_MODE) + .putExtra(KEY_VALUE, data.getData()) .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 12b64d89dd2..3019e99c44a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -20,7 +20,7 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; -import android.text.TextUtils; +import android.net.Uri; import android.util.Log; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -30,16 +30,17 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; +import org.schabi.newpipe.streams.io.SharpOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; +import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; @@ -54,8 +55,8 @@ public class SubscriptionsExportService extends BaseImportExportService { + ".services.SubscriptionsExportService.EXPORT_COMPLETE"; private Subscription subscription; - private File outFile; - private FileOutputStream outputStream; + private StoredFileHelper outFile; + private OutputStream outputStream; @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { @@ -63,18 +64,18 @@ public int onStartCommand(final Intent intent, final int flags, final int startI return START_NOT_STICKY; } - final String path = intent.getStringExtra(KEY_FILE_PATH); - if (TextUtils.isEmpty(path)) { + final Uri path = intent.getParcelableExtra(KEY_FILE_PATH); + if (path == null) { stopAndReportError(new IllegalStateException( - "Exporting to a file, but the path is empty or null"), + "Exporting to a file, but the path is null"), "Exporting subscriptions"); return START_NOT_STICKY; } try { - outFile = new File(path); - outputStream = new FileOutputStream(outFile); - } catch (FileNotFoundException e) { + outFile = new StoredFileHelper(this, path, "application/json"); + outputStream = new SharpOutputStream(outFile.getStream()); + } catch (IOException e) { handleError(e); return START_NOT_STICKY; } @@ -121,8 +122,8 @@ private void startExport() { .subscribe(getSubscriber()); } - private Subscriber getSubscriber() { - return new Subscriber() { + private Subscriber getSubscriber() { + return new Subscriber() { @Override public void onSubscribe(final Subscription s) { subscription = s; @@ -130,7 +131,7 @@ public void onSubscribe(final Subscription s) { } @Override - public void onNext(final File file) { + public void onNext(final StoredFileHelper file) { if (DEBUG) { Log.d(TAG, "startExport() success: file = " + file); } @@ -152,7 +153,7 @@ public void onComplete() { }; } - private Function, File> exportToFile() { + private Function, StoredFileHelper> exportToFile() { return subscriptionItems -> { ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener); return outFile; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 06ba5510619..f3b640ac347 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -20,6 +20,7 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -34,13 +35,11 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; +import org.schabi.newpipe.streams.io.SharpInputStream; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -52,8 +51,10 @@ import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; +import org.schabi.newpipe.streams.io.StoredFileHelper; import static org.schabi.newpipe.MainActivity.DEBUG; +import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME; public class SubscriptionsImportService extends BaseImportExportService { public static final int CHANNEL_URL_MODE = 0; @@ -100,17 +101,18 @@ public int onStartCommand(final Intent intent, final int flags, final int startI if (currentMode == CHANNEL_URL_MODE) { channelUrl = intent.getStringExtra(KEY_VALUE); } else { - final String filePath = intent.getStringExtra(KEY_VALUE); - if (TextUtils.isEmpty(filePath)) { + final Uri uri = intent.getParcelableExtra(KEY_VALUE); + if (uri == null) { stopAndReportError(new IllegalStateException( - "Importing from input stream, but file path is empty or null"), + "Importing from input stream, but file path is null"), "Importing subscriptions"); return START_NOT_STICKY; } try { - inputStream = new FileInputStream(new File(filePath)); - } catch (FileNotFoundException e) { + inputStream = new SharpInputStream( + new StoredFileHelper(this, uri, DEFAULT_MIME).getStream()); + } catch (IOException e) { handleError(e); return START_NOT_STICKY; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index b0bb30aa70c..8dff1fafc1e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -26,9 +27,12 @@ import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.streams.io.SharpInputStream; +import org.schabi.newpipe.streams.io.SharpOutputStream; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ZipHelper; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; @@ -41,9 +45,12 @@ import java.util.Date; import java.util.Locale; import java.util.Map; -import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; + import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class ContentSettingsFragment extends BasePreferenceFragment { @@ -104,8 +111,7 @@ public boolean onPreferenceTreeClick(final Preference preference) { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - - String homeDir = getActivity().getApplicationInfo().dataDir; + final String homeDir = getActivity().getApplicationInfo().dataDir; databasesDir = new File(homeDir + "/databases"); newpipeDb = new File(homeDir + "/databases/newpipe.db"); newpipeDbJournal = new File(homeDir + "/databases/newpipe.db-journal"); @@ -117,25 +123,16 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro addPreferencesFromResource(R.xml.content_settings); - Preference importDataPreference = findPreference(getString(R.string.import_data)); + final Preference importDataPreference = findPreference(getString(R.string.import_data)); importDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_FILE); - startActivityForResult(i, REQUEST_IMPORT_PATH); + startActivityForResult(StoredFileHelper.getPicker(getContext()), REQUEST_IMPORT_PATH); return true; }); - Preference exportDataPreference = findPreference(getString(R.string.export_data)); - exportDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_DIR); - startActivityForResult(i, REQUEST_EXPORT_PATH); + final Preference exportDataPreference = findPreference(getString(R.string.export_data)); + exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { + startActivityForResult(StoredDirectoryHelper.getPicker(getContext()), + REQUEST_EXPORT_PATH); return true; }); } @@ -175,30 +172,43 @@ public void onActivityResult(final int requestCode, final int resultCode, if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data.getData() != null) { - String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); - if (requestCode == REQUEST_EXPORT_PATH) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(R.string.override_current_data) - .setPositiveButton(getString(R.string.finish), - (DialogInterface d, int id) -> importDatabase(path)) - .setNegativeButton(android.R.string.cancel, - (DialogInterface d, int id) -> d.cancel()); - builder.create().show(); + try { + Uri uri = data.getData(); + if (FilePickerActivityHelper.isOwnFileUri(getContext(), uri)) { + uri = Uri.fromFile(Utils.getFileForUri(uri)); + } + if (requestCode == REQUEST_EXPORT_PATH) { + final StoredDirectoryHelper directory + = new StoredDirectoryHelper(getContext(), uri, null); + final SimpleDateFormat sdf + = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + exportDatabase(directory.createFile("NewPipeData-" + + sdf.format(new Date()) + ".zip", "application/zip")); + } else { + final StoredFileHelper file = new StoredFileHelper(getContext(), uri, + StoredFileHelper.DEFAULT_MIME); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.override_current_data) + .setPositiveButton(R.string.finish, + (DialogInterface d, int id) -> importDatabase(file)) + .setNegativeButton(R.string.cancel, + (DialogInterface d, int id) -> d.cancel()); + builder.create().show(); + } + } catch (IOException e) { + e.printStackTrace(); } } } - private void exportDatabase(final String path) { + private void exportDatabase(final StoredFileHelper file) { try { //checkpoint before export NewPipeDatabase.checkpoint(); - ZipOutputStream outZip = new ZipOutputStream( - new BufferedOutputStream( - new FileOutputStream(path))); + file.create(); + final ZipOutputStream outZip = new ZipOutputStream(new BufferedOutputStream( + new SharpOutputStream(file.getStream()))); ZipHelper.addFileToZip(outZip, newpipeDb.getPath(), "newpipe.db"); saveSharedPreferencesToFile(newpipeSettings); @@ -236,20 +246,17 @@ private void saveSharedPreferencesToFile(final File dst) { } } - private void importDatabase(final String filePath) { + private void importDatabase(final StoredFileHelper file) { // check if file is supported - ZipFile zipFile = null; - try { - zipFile = new ZipFile(filePath); + try (ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream( + new SharpInputStream(file.getStream())))) { + if (zipInputStream.getNextEntry() == null) { + throw new IOException("Empty zip"); + } } catch (IOException ioe) { Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) .show(); return; - } finally { - try { - zipFile.close(); - } catch (Exception ignored) { - } } try { @@ -257,7 +264,7 @@ private void importDatabase(final String filePath) { throw new Exception("Could not create databases dir"); } - final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, + final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(file, newpipeDb.getPath(), "newpipe.db"); if (isDbFileExtracted) { @@ -270,7 +277,7 @@ private void importDatabase(final String filePath) { } //If settings file exist, ask if it should be imported. - if (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(), + if (ZipHelper.extractFileFromZip(file, newpipeSettings.getPath(), "newpipe.settings")) { AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setTitle(R.string.import_settings); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index aaa572eab9b..f436ff7f860 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -9,11 +9,11 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; -import android.widget.Toast; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.nononsenseapps.filepicker.Utils; @@ -27,7 +27,7 @@ import java.net.URLDecoder; 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; @@ -58,6 +58,14 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { prefPathAudio = findPreference(downloadPathAudioPreference); prefStorageAsk = findPreference(downloadStorageAsk); + final SwitchPreference prefUseSaf = findPreference(storageUseSafPreference); + prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); + prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + prefUseSaf.setEnabled(false); + } + updatePreferencesSummary(); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); @@ -179,12 +187,18 @@ public boolean onPreferenceTreeClick(final Preference preference) { + "preference = [" + preference + "]"); } - String key = preference.getKey(); - int request; + final String key = preference.getKey(); + final int request; if (key.equals(storageUseSafPreference)) { - Toast.makeText(getContext(), R.string.download_choose_new_path, - Toast.LENGTH_LONG).show(); + if (!NewPipeSettings.useStorageAccessFramework(ctx)) { + NewPipeSettings.saveDefaultVideoDownloadDirectory(ctx); + NewPipeSettings.saveDefaultAudioDownloadDirectory(ctx); + } else { + defaultPreferences.edit().putString(downloadPathVideoPreference, null) + .putString(downloadPathAudioPreference, null).apply(); + } + updatePreferencesSummary(); return true; } else if (key.equals(downloadPathVideoPreference)) { request = REQUEST_DOWNLOAD_VIDEO_PATH; @@ -194,22 +208,7 @@ public boolean onPreferenceTreeClick(final Preference preference) { return super.onPreferenceTreeClick(preference); } - Intent i; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && NewPipeSettings.useStorageAccessFramework(ctx)) { - i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | StoredDirectoryHelper.PERMISSION_FLAGS); - } else { - i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_DIR); - } - - startActivityForResult(i, request); + startActivityForResult(StoredDirectoryHelper.getPicker(ctx), request); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 47a16f6f3fe..53178fee938 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.Environment; import androidx.annotation.NonNull; @@ -11,6 +12,8 @@ import java.io.File; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + /* * Created by k3b on 07.01.2016. * @@ -46,30 +49,34 @@ public static void initSettings(final Context context) { PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true); PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); - getVideoDownloadFolder(context); - getAudioDownloadFolder(context); + saveDefaultVideoDownloadDirectory(context); + saveDefaultAudioDownloadDirectory(context); } - private static void getVideoDownloadFolder(final Context context) { - getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); + static void saveDefaultVideoDownloadDirectory(final Context context) { + saveDefaultDirectory(context, R.string.download_path_video_key, + Environment.DIRECTORY_MOVIES); } - private static void getAudioDownloadFolder(final Context context) { - getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); + static void saveDefaultAudioDownloadDirectory(final Context context) { + saveDefaultDirectory(context, R.string.download_path_audio_key, + Environment.DIRECTORY_MUSIC); } - private static void getDir(final Context context, final int keyID, - final String defaultDirectoryName) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(keyID); - String downloadPath = prefs.getString(key, null); - if ((downloadPath != null) && (!downloadPath.isEmpty())) { - return; + private static void saveDefaultDirectory(final Context context, final int keyID, + final String defaultDirectoryName) { + if (!useStorageAccessFramework(context)) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String key = context.getString(keyID); + final String downloadPath = prefs.getString(key, null); + if (!isNullOrEmpty(downloadPath)) { + return; + } + + final SharedPreferences.Editor spEditor = prefs.edit(); + spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); + spEditor.apply(); } - - SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); - spEditor.apply(); } @NonNull @@ -82,10 +89,15 @@ private static String getNewPipeChildFolderPathForDir(final File dir) { } public static boolean useStorageAccessFramework(final Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return true; + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return false; + } + final String key = context.getString(R.string.storage_use_saf); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(key, false); + return prefs.getBoolean(key, true); } - } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 18cbece6fb4..98927dfcf2a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.settings; -import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -41,11 +40,6 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { - - public static void initSettings(final Context context) { - NewPipeSettings.initSettings(context); - } - @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java new file mode 100644 index 00000000000..4b871d1dc13 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpInputStream.java @@ -0,0 +1,48 @@ +package org.schabi.newpipe.streams.io; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +public class SharpInputStream extends InputStream { + private final SharpStream stream; + + public SharpInputStream(final SharpStream stream) throws IOException { + if (!stream.canRead()) { + throw new IOException("SharpStream is not readable"); + } + this.stream = stream; + } + + @Override + public int read() throws IOException { + return stream.read(); + } + + @Override + public int read(@NonNull final byte[] b) throws IOException { + return stream.read(b); + } + + @Override + public int read(@NonNull final byte[] b, final int off, final int len) throws IOException { + return stream.read(b, off, len); + } + + @Override + public long skip(final long n) throws IOException { + return stream.skip(n); + } + + @Override + public int available() { + final long res = stream.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; + } + + @Override + public void close() { + stream.close(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java new file mode 100644 index 00000000000..23a2393a8dd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpOutputStream.java @@ -0,0 +1,42 @@ +package org.schabi.newpipe.streams.io; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.OutputStream; + +public class SharpOutputStream extends OutputStream { + private final SharpStream stream; + + public SharpOutputStream(final SharpStream stream) throws IOException { + if (!stream.canWrite()) { + throw new IOException("SharpStream is not writable"); + } + this.stream = stream; + } + + @Override + public void write(final int b) throws IOException { + stream.write((byte) b); + } + + @Override + public void write(@NonNull final byte[] b) throws IOException { + stream.write(b); + } + + @Override + public void write(@NonNull final byte[] b, final int off, final int len) throws IOException { + stream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + stream.flush(); + } + + @Override + public void close() { + stream.close(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 46ec68d9e53..9b4f79047f4 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.streams.io; import java.io.Closeable; +import java.io.Flushable; import java.io.IOException; /** * Based on C#'s Stream class. */ -public abstract class SharpStream implements Closeable { +public abstract class SharpStream implements Closeable, Flushable { public abstract int read() throws IOException; public abstract int read(byte[] buffer) throws IOException; diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java similarity index 53% rename from app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java rename to app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index 8f6070ff4d3..8253f5f2b37 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -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.Context; import android.content.Intent; @@ -13,6 +12,9 @@ import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; +import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.util.FilePickerActivityHelper; + import java.io.File; import java.io.IOException; import java.net.URI; @@ -21,10 +23,11 @@ import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; - +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; 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 DocumentFile docTree; @@ -33,7 +36,8 @@ public class StoredDirectoryHelper { private 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; if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { @@ -49,47 +53,45 @@ public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String 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"); + } this.docTree = DocumentFile.fromTreeUri(context, path); - if (this.docTree == null) + if (this.docTree == null) { throw new IOException("Failed to create the tree from Uri"); + } } - @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredDirectoryHelper(@NonNull URI location, String tag) { - ioTree = new File(location); - this.tag = tag; - } - - public StoredFileHelper createFile(String filename, String mime) { + public StoredFileHelper createFile(final String filename, final String mime) { return createFile(filename, mime, false); } - public StoredFileHelper createUniqueFile(String name, String mime) { - ArrayList matches = new ArrayList<>(); - String[] filename = splitFilename(name); - String lcFilename = filename[0].toLowerCase(); + public StoredFileHelper createUniqueFile(final String name, final String mime) { + final ArrayList matches = new ArrayList<>(); + final String[] filename = splitFilename(name); + final String lcFilename = filename[0].toLowerCase(); 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()); + } } else { // warning: SAF file listing is very slow - Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( - docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()) - ); + final Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( + docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())); - String[] projection = new String[]{COLUMN_DISPLAY_NAME}; - String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; - ContentResolver cr = context.getContentResolver(); + final String[] projection = new String[]{COLUMN_DISPLAY_NAME}; + final String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; + 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) { - while (cursor.moveToNext()) + while (cursor.moveToNext()) { addIfStartWith(matches, lcFilename, cursor.getString(0)); + } } } } @@ -99,7 +101,7 @@ public StoredFileHelper createUniqueFile(String name, String mime) { } else { // check if the filename is in use String lcName = name.toLowerCase(); - for (String testName : matches) { + for (final String testName : matches) { if (testName.equals(lcName)) { lcName = null; break; @@ -107,27 +109,33 @@ public StoredFileHelper createUniqueFile(String name, String mime) { } // 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); 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(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) { - StoredFileHelper storage; + private StoredFileHelper createFile(final String filename, final String mime, + final boolean safe) { + final StoredFileHelper storage; try { - if (docTree == null) + if (docTree == null) { storage = new StoredFileHelper(ioTree, filename, mime); - else + } else { storage = new StoredFileHelper(context, docTree, filename, mime, safe); + } } catch (IOException e) { return null; } @@ -146,7 +154,7 @@ public boolean exists() { } /** - * 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 */ @@ -169,7 +177,9 @@ public boolean mkdirs() { return ioTree.exists() || ioTree.mkdirs(); } - if (docTree.exists()) return true; + if (docTree.exists()) { + return true; + } try { DocumentFile parent; @@ -177,14 +187,18 @@ public boolean mkdirs() { while (true) { parent = docTree.getParentFile(); - if (parent == null || child == null) break; - if (parent.exists()) return true; + if (parent == null || child == null) { + break; + } + if (parent.exists()) { + return true; + } parent.createDirectory(child); - child = parent.getName();// for the next iteration + child = parent.getName(); // for the next iteration } - } catch (Exception e) { + } catch (Exception ignored) { // no more parent directories or unsupported by the storage provider } @@ -195,13 +209,13 @@ public String getTag() { return tag; } - public Uri findFile(String filename) { + public Uri findFile(final String filename) { if (docTree == null) { - File res = new File(ioTree, filename); + final File res = new File(ioTree, filename); 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(); } @@ -215,76 +229,100 @@ public String toString() { return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); } - //////////////////// // Utils /////////////////// - private static void addIfStartWith(ArrayList list, @NonNull String base, String str) { - if (str == null || str.isEmpty()) return; - str = str.toLowerCase(); - if (str.startsWith(base)) list.add(str); + private static void addIfStartWith(final ArrayList list, @NonNull final String base, + final String str) { + if (isNullOrEmpty(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('.'); - if (dotIndex < 0 || (dotIndex == filename.length() - 1)) + if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { return new String[]{filename, ""}; + } 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); } /** - * 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 tree Directory where search * @param filename Target filename * @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) { - 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 documentId = 1; // LOWER() SQL function is not supported - String selection = COLUMN_DISPLAY_NAME + " = ?"; - //String selection = COLUMN_DISPLAY_NAME + " LIKE ?%"; + final String selection = COLUMN_DISPLAY_NAME + " = ?"; + //final String selection = COLUMN_DISPLAY_NAME + " LIKE ?%"; - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - tree.getUri(), DocumentsContract.getDocumentId(tree.getUri()) - ); - String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; - ContentResolver contentResolver = context.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(tree.getUri(), + DocumentsContract.getDocumentId(tree.getUri())); + final String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; + final ContentResolver contentResolver = context.getContentResolver(); - filename = filename.toLowerCase(); + final String lowerFilename = filename.toLowerCase(); - try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) { - if (cursor == null) return null; + try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, + new String[]{lowerFilename}, null)) { + if (cursor == null) { + return null; + } while (cursor.moveToNext()) { - if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename)) + if (cursor.isNull(name) + || !cursor.getString(name).toLowerCase().startsWith(lowerFilename)) { continue; + } - return DocumentFile.fromSingleUri( - context, DocumentsContract.buildDocumentUriUsingTree( - tree.getUri(), cursor.getString(documentId) - ) - ); + return DocumentFile.fromSingleUri(context, + DocumentsContract.buildDocumentUriUsingTree(tree.getUri(), + cursor.getString(documentId))); } } return null; } + public static Intent getPicker(final Context ctx) { + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + return new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_DIR); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java new file mode 100644 index 00000000000..3989e5c54ba --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -0,0 +1,506 @@ +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(); + } + + + 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 (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 + final boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); + // ¿docFile.isVirtual() means is no-physical? + final boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile(); + + return exists && isFile; + } + + public boolean create() { + assertValid(); + final 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 == null || 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(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()) { + 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 (Exception e) { + if (docFile.getName() == null) { + throw new IOException(e); + } + } + } + + 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(final Context ctx) { + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + return new Intent(Intent.ACTION_OPEN_DOCUMENT) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .setType("*/*") + .addCategory(Intent.CATEGORY_OPENABLE) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + return new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_FILE); + } + } + + public static Intent getNewPicker(@NonNull final Context ctx, @Nullable final String startPath, + @Nullable final String filename) { + final Intent i; + if (NewPipeSettings.useStorageAccessFramework(ctx)) { + i = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .setType("*/*") + .addCategory(Intent.CATEGORY_OPENABLE) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); + + if (startPath != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + i.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(startPath)); + } + if (filename != null) { + i.putExtra(Intent.EXTRA_TITLE, filename); + } + } else { + i = new Intent(ctx, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_NEW_FILE); + + if (startPath != null || filename != null) { + File fullStartPath; + if (startPath == null) { + fullStartPath = Environment.getExternalStorageDirectory(); + } else { + fullStartPath = new File(startPath); + } + if (filename != null) { + fullStartPath = new File(fullStartPath, filename); + } + i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, + fullStartPath.getAbsolutePath()); + } + } + return i; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 6ede163a3b8..20d8ce30c3d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.util; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Environment; @@ -28,25 +27,6 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { private CustomFilePickerFragment currentFragment; - public static Intent chooseSingleFile(@NonNull final Context context) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); - } - - public static Intent chooseFileToSave(@NonNull final Context context, - @Nullable final String startPath) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) - .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, - FilePickerActivityHelper.MODE_NEW_FILE); - } - public static boolean isOwnFileUri(@NonNull final Context context, @NonNull final Uri uri) { if (uri.getAuthority() == null) { return false; diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index 31f5fd22250..7626914a3e7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.util; +import org.schabi.newpipe.streams.io.SharpInputStream; + import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -8,6 +10,8 @@ import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import org.schabi.newpipe.streams.io.StoredFileHelper; + /** * Created by Christian Schabesberger on 28.01.18. * Copyright 2018 Christian Schabesberger @@ -57,23 +61,21 @@ public static void addFileToZip(final ZipOutputStream outZip, final String file, } /** - * This will extract data from Zipfiles. + * This will extract data from ZipInputStream. * Caution this will override the original file. * - * @param filePath The path of the zip + * @param zipFile The zip file * @param file The path of the file on the disk where the data should be extracted to. * @param name The path of the file inside the zip. * @return will return true if the file was found within the zip file * @throws Exception */ - public static boolean extractFileFromZip(final String filePath, final String file, + public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file, final String name) throws Exception { + final ZipInputStream inZip = new ZipInputStream(new BufferedInputStream( + new SharpInputStream(zipFile.getStream()))); - ZipInputStream inZip = new ZipInputStream( - new BufferedInputStream( - new FileInputStream(filePath))); - - byte[] data = new byte[BUFFER_SIZE]; + final byte[] data = new byte[BUFFER_SIZE]; boolean found = false; @@ -82,14 +84,14 @@ public static boolean extractFileFromZip(final String filePath, final String fil if (ze.getName().equals(name)) { found = true; // delete old file first - File oldFile = new File(file); + final File oldFile = new File(file); if (oldFile.exists()) { if (!oldFile.delete()) { throw new Exception("Could not delete " + file); } } - FileOutputStream outFile = new FileOutputStream(file); + final FileOutputStream outFile = new FileOutputStream(file); int count = 0; while ((count = inZip.read(data)) != -1) { outFile.write(data, 0, count); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 3f651d2ee1b..b7472b16dfe 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -25,7 +25,7 @@ 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.service.DownloadManagerService; import us.shandian.giga.util.Utility; diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index 8e814a2aff5..71c3a7963b2 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -5,7 +5,7 @@ import java.io.Serializable; import java.util.Calendar; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; public abstract class Mission implements Serializable { private static final long serialVersionUID = 1L;// last bump: 27 march 2019 diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index bf9460b3d19..a1853d58e73 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -16,7 +16,7 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; 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 diff --git a/app/src/main/java/us/shandian/giga/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java deleted file mode 100644 index 0d6320b53e4..00000000000 --- a/app/src/main/java/us/shandian/giga/io/SharpInputStream.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package us.shandian.giga.io; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Wrapper for the classic {@link java.io.InputStream} - * - * @author kapodamy - */ -public class SharpInputStream extends InputStream { - - private final SharpStream base; - - public SharpInputStream(SharpStream base) throws IOException { - if (!base.canRead()) { - throw new IOException("The provided stream is not readable"); - } - this.base = base; - } - - @Override - public int read() throws IOException { - return base.read(); - } - - @Override - public int read(@NonNull byte[] bytes) throws IOException { - return base.read(bytes); - } - - @Override - public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { - return base.read(bytes, i, i1); - } - - @Override - public long skip(long l) throws IOException { - return base.skip(l); - } - - @Override - public int available() { - long res = base.available(); - return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; - } - - @Override - public void close() { - base.close(); - } -} diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java deleted file mode 100644 index ad3ceec3dfc..00000000000 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ /dev/null @@ -1,383 +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 == null || 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()) { - 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); - } - } - - 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); - } -} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 994c6ee63ea..ac3313826f5 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -18,8 +18,8 @@ import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.sqlite.FinishedMissionStore; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 3da0e75b85d..d9c06cf476f 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -45,8 +45,8 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.MissionRecoveryInfo; -import us.shandian.giga.io.StoredDirectoryHelper; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredDirectoryHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager.NetworkState; diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index c2d3a9b9e83..3eb77c15ebc 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -55,7 +55,7 @@ import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; 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.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 09f4d0c7942..d31ccc6c387 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -36,7 +36,7 @@ import java.io.IOException; import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; @@ -242,27 +242,21 @@ private void setAdapterButtons() { private void recoverMission(@NonNull DownloadMission mission) { unsafeMissionTarget = mission; + final String startPath; if (NewPipeSettings.useStorageAccessFramework(mContext)) { - StoredFileHelper.requestSafWithFileCreation( - MissionsFragment.this, - REQUEST_DOWNLOAD_SAVE_AS, - mission.storage.getName(), - mission.storage.getType() - ); - + startPath = null; } else { - File initialSavePath; - if (DownloadManager.TAG_VIDEO.equals(mission.storage.getType())) - initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); - else + final File initialSavePath; + if (DownloadManager.TAG_AUDIO.equals(mission.storage.getType())) { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); - - initialSavePath = new File(initialSavePath, mission.storage.getName()); - startActivityForResult( - FilePickerActivityHelper.chooseFileToSave(mContext, initialSavePath.getAbsolutePath()), - REQUEST_DOWNLOAD_SAVE_AS - ); + } else { + initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); + } + startPath = initialSavePath.getAbsolutePath(); } + + startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath, + mission.storage.getName()), REQUEST_DOWNLOAD_SAVE_AS); } @Override diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 551e80a3e65..54c6f8e265f 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -29,7 +29,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Locale; -import us.shandian.giga.io.StoredFileHelper; +import org.schabi.newpipe.streams.io.StoredFileHelper; public class Utility { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3625e67f4a1..167ae68a72f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -328,6 +328,7 @@ Please wait… Copied to clipboard Please define a download folder later in settings + No download folder set yet, choose the default download folder now This permission is needed to\nopen in popup mode 1 item deleted. diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 0df021842c7..08038537d55 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -4,7 +4,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_category_downloads_title"> -