diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 1c3c86f4d..c055f8f23 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -702,9 +702,9 @@ public class DownloadDialog extends DialogFragment } if (askForSavePath) { - final String startPath; + final Uri initialPath; if (NewPipeSettings.useStorageAccessFramework(context)) { - startPath = null; + initialPath = null; } else { final File initialSavePath; if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { @@ -712,11 +712,11 @@ public class DownloadDialog extends DialogFragment } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); } - startPath = initialSavePath.getAbsolutePath(); + initialPath = Uri.parse(initialSavePath.getAbsolutePath()); } - startActivityForResult(StoredFileHelper.getNewPicker(context, startPath, - filenameTmp, mimeTmp), REQUEST_DOWNLOAD_SAVE_AS); + startActivityForResult(StoredFileHelper.getNewPicker(context, + filenameTmp, mimeTmp, initialPath), REQUEST_DOWNLOAD_SAVE_AS); return; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index d6c08a269..095c8dbc7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -191,7 +191,10 @@ class SubscriptionFragment : BaseStateFragment() { val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) val exportName = "newpipe_subscriptions_$date.json" - startActivityForResult(StoredFileHelper.getNewPicker(activity, null, exportName, "application/json"), REQUEST_EXPORT_CODE) + startActivityForResult( + StoredFileHelper.getNewPicker(activity, exportName, "application/json", null), + REQUEST_EXPORT_CODE + ) } private fun openReorderDialog() { @@ -283,7 +286,8 @@ class SubscriptionFragment : BaseStateFragment() { private fun showLongTapDialog(selectedItem: ChannelInfoItem) { 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) ) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index b64543d27..8b2bd9c9a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -6,12 +6,16 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.util.ThemeHelper; +import java.util.Objects; + public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final boolean DEBUG = MainActivity.DEBUG; @@ -37,4 +41,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { super.onResume(); 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; + } } 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 45325dc6c..4ef5b8b25 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; @@ -32,18 +33,25 @@ import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; 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; public class ContentSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_IMPORT_PATH = 8945; 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 String importExportDataPathKey; private String thumbnailLoadToggleKey; private String youtubeRestrictedModeEnabledKey; + @Nullable private Uri lastImportExportDataUri = null; private Localization initialSelectedLocalization; private ContentCountry initialSelectedContentCountry; private String initialLanguage; @@ -51,29 +59,35 @@ public class ContentSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { final File homeDir = ContextCompat.getDataDir(requireContext()); + Objects.requireNonNull(homeDir); manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); manager.deleteSettingsFile(); + importExportDataPathKey = getString(R.string.import_export_data_path); + thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); + youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); + addPreferencesFromResource(R.xml.content_settings); - final Preference importDataPreference = findPreference(getString(R.string.import_data)); + final Preference importDataPreference = requirePreference(R.string.import_data); importDataPreference.setOnPreferenceClickListener((Preference p) -> { - startActivityForResult(StoredFileHelper.getPicker(getContext()), REQUEST_IMPORT_PATH); + startActivityForResult( + StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()), + REQUEST_IMPORT_PATH); return true; }); - final Preference exportDataPreference = findPreference(getString(R.string.export_data)); + final Preference exportDataPreference = requirePreference(R.string.export_data); exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - startActivityForResult(StoredFileHelper.getNewPicker(getContext(), null, - "NewPipeData-" + sdf.format(new Date()) + ".zip", "application/zip"), + + startActivityForResult( + StoredFileHelper.getNewPicker(requireContext(), + "NewPipeData-" + EXPORT_DATE_FORMAT.format(new Date()) + ".zip", + ZIP_MIME_TYPE, getImportExportDataUri()), REQUEST_EXPORT_PATH); return true; }); - thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); - youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); - initialSelectedLocalization = org.schabi.newpipe.util.Localization .getPreferredLocalization(requireContext()); initialSelectedContentCountry = org.schabi.newpipe.util.Localization @@ -81,7 +95,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { initialLanguage = PreferenceManager .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 -> { defaultPreferences.edit() .putString(getString(R.string.recaptcha_cookies_key), "").apply(); @@ -157,8 +171,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment { if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - final StoredFileHelper file = new StoredFileHelper(getContext(), data.getData(), - "application/zip"); + + 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) { exportDatabase(file); } else { @@ -182,6 +199,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .getDefaultSharedPreferences(requireContext()); manager.exportDatabase(preferences, file); + saveLastImportExportDataUri(false); // save export path only on success Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e); @@ -206,30 +224,55 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .show(); } - //If settings file exist, ask if it should be imported. + // if settings file exist, ask if it should be imported. if (manager.extractSettings(file)) { final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()); alert.setTitle(R.string.import_settings); alert.setNegativeButton(android.R.string.no, (dialog, which) -> { dialog.dismiss(); - // restart app to properly load db - System.exit(0); + finishImport(); }); alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { dialog.dismiss(); manager.loadSharedPreferences(PreferenceManager .getDefaultSharedPreferences(requireContext())); - // restart app to properly load db - System.exit(0); + finishImport(); }); alert.show(); } else { - // restart app to properly load db - System.exit(0); + finishImport(); } } catch (final Exception e) { ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e); } } + + /** + * Save import path and restart system. + */ + private void finishImport() { + // save import path only on success; save immediately because app is about to exit + saveLastImportExportDataUri(true); + // restart app to properly load db + System.exit(0); + } + + private Uri getImportExportDataUri() { + final String path = defaultPreferences.getString(importExportDataPathKey, null); + return isBlank(path) ? null : Uri.parse(path); + } + + private void saveLastImportExportDataUri(final boolean immediately) { + if (lastImportExportDataUri != null) { + final SharedPreferences.Editor editor = defaultPreferences.edit() + .putString(importExportDataPathKey, lastImportExportDataUri.toString()); + if (immediately) { + // noinspection ApplySharedPref + editor.commit(); // app about to be restarted, commit immediately + } else { + editor.apply(); + } + } + } } 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 index 0fcad2958..c86164ed2 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -448,7 +448,7 @@ public class StoredFileHelper implements Serializable { return !str1.equals(str2); } - public static Intent getPicker(final Context ctx) { + 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) @@ -466,10 +466,14 @@ public class StoredFileHelper implements Serializable { } } + 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 startPath, @Nullable final String filename, - @NonNull final String mimeType) { + @NonNull final String mimeType, + @Nullable final Uri initialPath) { final Intent i; if (NewPipeSettings.useStorageAccessFramework(ctx)) { i = new Intent(Intent.ACTION_CREATE_DOCUMENT) @@ -478,10 +482,6 @@ public class StoredFileHelper implements Serializable { .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); } @@ -492,21 +492,63 @@ public class StoredFileHelper implements Serializable { .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; + 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()); + } } } 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 c56587c55..793d147b5 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 @@ -29,14 +29,13 @@ import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.FilePickerActivityHelper; -import org.schabi.newpipe.util.ThemeHelper; import java.io.File; import java.io.IOException; import us.shandian.giga.get.DownloadMission; -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,9 +241,9 @@ public class MissionsFragment extends Fragment { private void recoverMission(@NonNull DownloadMission mission) { unsafeMissionTarget = mission; - final String startPath; + final Uri initialPath; if (NewPipeSettings.useStorageAccessFramework(mContext)) { - startPath = null; + initialPath = null; } else { final File initialSavePath; if (DownloadManager.TAG_AUDIO.equals(mission.storage.getType())) { @@ -252,11 +251,11 @@ public class MissionsFragment extends Fragment { } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); } - startPath = initialSavePath.getAbsolutePath(); + initialPath = Uri.parse(initialSavePath.getAbsolutePath()); } - startActivityForResult(StoredFileHelper.getNewPicker(mContext, startPath, - mission.storage.getName(), mission.storage.getType()), REQUEST_DOWNLOAD_SAVE_AS); + startActivityForResult(StoredFileHelper.getNewPicker(mContext, mission.storage.getName(), + mission.storage.getType(), initialPath), REQUEST_DOWNLOAD_SAVE_AS); } @Override diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fd6cc7251..c23e81fbe 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -265,6 +265,7 @@ feed_use_dedicated_fetch_method + import_export_data_path import_data export_data