package org.schabi.newpipe.settings; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class DownloadSettingsFragment extends BasePreferenceFragment { public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; private String downloadPathVideoPreference; private String downloadPathAudioPreference; private String storageUseSafPreference; private Preference prefPathVideo; private Preference prefPathAudio; private Preference prefStorageAsk; private Context ctx; private final ActivityResultLauncher requestDownloadVideoPathLauncher = registerForActivityResult( new StartActivityForResult(), this::requestDownloadVideoPathResult); private final ActivityResultLauncher requestDownloadAudioPathLauncher = registerForActivityResult( new StartActivityForResult(), this::requestDownloadAudioPathResult); @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.download_settings); downloadPathVideoPreference = getString(R.string.download_path_video_key); downloadPathAudioPreference = getString(R.string.download_path_audio_key); storageUseSafPreference = getString(R.string.storage_use_saf); final String downloadStorageAsk = getString(R.string.downloads_storage_ask); prefPathVideo = findPreference(downloadPathVideoPreference); prefPathAudio = findPreference(downloadPathAudioPreference); prefStorageAsk = findPreference(downloadStorageAsk); final SwitchPreferenceCompat prefUseSaf = findPreference(storageUseSafPreference); prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { prefUseSaf.setEnabled(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29); } else { prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_19); } prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice); } updatePreferencesSummary(); updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false)); if (hasInvalidPath(downloadPathVideoPreference) || hasInvalidPath(downloadPathAudioPreference)) { updatePreferencesSummary(); } prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> { updatePathPickers(!(boolean) value); return true; }); } @Override public void onAttach(@NonNull final Context context) { super.onAttach(context); ctx = context; } @Override public void onDetach() { super.onDetach(); ctx = null; prefStorageAsk.setOnPreferenceChangeListener(null); } private void updatePreferencesSummary() { showPathInSummary(downloadPathVideoPreference, R.string.download_path_summary, prefPathVideo); showPathInSummary(downloadPathAudioPreference, R.string.download_path_audio_summary, prefPathAudio); } private void showPathInSummary(final String prefKey, @StringRes final int defaultString, final Preference target) { String rawUri = defaultPreferences.getString(prefKey, null); if (rawUri == null || rawUri.isEmpty()) { target.setSummary(getString(defaultString)); return; } if (rawUri.charAt(0) == File.separatorChar) { target.setSummary(rawUri); return; } if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) { target.setSummary(new File(URI.create(rawUri)).getPath()); return; } try { rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); } catch (final UnsupportedEncodingException e) { // nothing to do } target.setSummary(rawUri); } private boolean isFileUri(final String path) { return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); } private boolean hasInvalidPath(final String prefKey) { final String value = defaultPreferences.getString(prefKey, null); return value == null || value.isEmpty(); } private void updatePathPickers(final boolean enabled) { prefPathVideo.setEnabled(enabled); prefPathAudio.setEnabled(enabled); } // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible private void forgetSAFTree(final Context context, final String oldPath) { if (IGNORE_RELEASE_ON_OLD_PATH) { return; } if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) { return; } try { final Uri uri = Uri.parse(oldPath); context.getContentResolver() .releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); context.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); Log.i(TAG, "Revoke old path permissions success on " + oldPath); } catch (final Exception err) { Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); } } private void showMessageDialog(@StringRes final int title, @StringRes final int message) { final AlertDialog.Builder msg = new AlertDialog.Builder(ctx); msg.setTitle(title); msg.setMessage(message); msg.setPositiveButton(getString(R.string.ok), null); msg.show(); } @Override public boolean onPreferenceTreeClick(final Preference preference) { if (DEBUG) { Log.d(TAG, "onPreferenceTreeClick() called with: " + "preference = [" + preference + "]"); } final String key = preference.getKey(); if (key.equals(storageUseSafPreference)) { 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)) { launchDirectoryPicker(requestDownloadVideoPathLauncher); } else if (key.equals(downloadPathAudioPreference)) { launchDirectoryPicker(requestDownloadAudioPathLauncher); } else { return super.onPreferenceTreeClick(preference); } return true; } private void launchDirectoryPicker(final ActivityResultLauncher launcher) { launcher.launch(StoredDirectoryHelper.getPicker(ctx)); } private void requestDownloadVideoPathResult(final ActivityResult result) { requestDownloadPathResult(result, downloadPathVideoPreference); } private void requestDownloadAudioPathResult(final ActivityResult result) { requestDownloadPathResult(result, downloadPathAudioPreference); } private void requestDownloadPathResult(final ActivityResult result, final String key) { assureCorrectAppLanguage(getContext()); if (result.getResultCode() != Activity.RESULT_OK) { return; } Uri uri = null; if (result.getData() != null) { uri = result.getData().getData(); } if (uri == null) { showMessageDialog(R.string.general_error, R.string.invalid_directory); return; } // revoke permissions on the old save path (required for SAF only) final Context context = requireContext(); forgetSAFTree(context, defaultPreferences.getString(key, "")); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !FilePickerActivityHelper.isOwnFileUri(context, uri)) { // steps to acquire the selected path: // 1. acquire permissions on the new save path // 2. save the new path, if step(2) was successful try { context.grantUriPermission(context.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, null); Log.i(TAG, "Acquiring tree success from " + uri.toString()); if (!mainStorage.canWrite()) { throw new IOException("No write permissions on " + uri.toString()); } } catch (final IOException err) { Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); showMessageDialog(R.string.general_error, R.string.no_available_dir); return; } } else { final File target = Utils.getFileForUri(uri); if (!target.canWrite()) { showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); return; } uri = Uri.fromFile(target); } defaultPreferences.edit().putString(key, uri.toString()).apply(); updatePreferencesSummary(); } }