Merge branch 'dev' into pr8221

This commit is contained in:
Stypox 2024-03-29 16:09:13 +01:00
commit e1ce3fef1b
No known key found for this signature in database
GPG key ID: 4BDF1B40A49FDD23
1741 changed files with 56947 additions and 21110 deletions

View file

@ -44,7 +44,13 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
return false;
});
} else {
removePreference(nightThemeKey);
// disable the night theme selection
final Preference preference = findPreference(nightThemeKey);
if (preference != null) {
preference.setEnabled(false);
preference.setSummary(getString(R.string.night_theme_available,
getString(R.string.auto_device_theme_title)));
}
}
}
@ -61,20 +67,13 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
return super.onPreferenceTreeClick(preference);
}
private void removePreference(final String preferenceKey) {
final Preference preference = findPreference(preferenceKey);
if (preference != null) {
getPreferenceScreen().removePreference(preference);
}
}
private void applyThemeChange(final String beginningThemeKey,
final String themeKey,
final Object newValue) {
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
defaultPreferences.edit().putString(themeKey, newValue.toString()).apply();
ThemeHelper.setDayNightMode(getContext(), newValue.toString());
ThemeHelper.setDayNightMode(requireContext(), newValue.toString());
if (!newValue.equals(beginningThemeKey) && getActivity() != null) {
// if it's not the current theme

View file

@ -0,0 +1,271 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ZipHelper;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
private static final String ZIP_MIME_TYPE = "application/zip";
private final SimpleDateFormat exportDateFormat =
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ContentSettingsManager manager;
private String importExportDataPathKey;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestImportPathResult);
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestExportPathResult);
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable 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);
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestImportPathLauncher,
StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestExportPathLauncher,
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference resetSettings = findPreference(getString(R.string.reset_settings));
// Resets all settings by deleting shared preference and restarting the app
// A dialogue will pop up to confirm if user intends to reset all settings
assert resetSettings != null;
resetSettings.setOnPreferenceClickListener(preference -> {
// Show Alert Dialogue
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage(R.string.reset_all_settings);
builder.setCancelable(true);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
// Deletes all shared preferences xml files.
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(requireContext());
sharedPreferences.edit().clear().apply();
// Restarts the app
if (getActivity() == null) {
return;
}
NavigationHelper.restartApp(getActivity());
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
});
final AlertDialog alertDialog = builder.create();
alertDialog.show();
return true;
});
}
private void requestExportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
final StoredFileHelper file = new StoredFileHelper(
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
exportDatabase(file, lastExportDataUri);
}
}
private void requestImportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
final StoredFileHelper file = new StoredFileHelper(
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
new androidx.appcompat.app.AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
.show();
}
}
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
try {
//checkpoint before export
NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, file);
saveLastImportExportDataUri(exportDataUri); // save export path only on success
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
.show();
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
}
}
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
// check if file is supported
if (!ZipHelper.isValidZipFile(file)) {
Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
return;
}
try {
if (!manager.ensureDbDirectoryExists()) {
throw new IOException("Could not create databases dir");
}
if (!manager.extractDb(file)) {
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
Toast.LENGTH_LONG)
.show();
}
// if settings file exist, ask if it should be imported.
if (manager.extractSettings(file)) {
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.import_settings)
.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
})
.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
manager.loadSharedPreferences(prefs);
cleanImport(context, prefs);
finishImport(importDataUri);
})
.show();
} else {
finishImport(importDataUri);
}
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
}
}
/**
* Remove settings that are not supposed to be imported on different devices
* and reset them to default values.
* @param context the context used for the import
* @param prefs the preferences used while running the import
*/
private void cleanImport(@NonNull final Context context,
@NonNull final SharedPreferences prefs) {
// Check if media tunnelling needs to be disabled automatically,
// if it was disabled automatically in the imported preferences.
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String automaticTunnelingKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
// R.string.disable_media_tunneling_key should always be true
// if R.string.disabled_media_tunneling_automatically_key equals 1,
// but we double check here just to be sure and to avoid regressions
// caused by possible later modification of the media tunneling functionality.
// R.string.disabled_media_tunneling_automatically_key == 0:
// automatic value overridden by user in settings
// R.string.disabled_media_tunneling_automatically_key == -1: not set
final boolean wasMediaTunnelingDisabledAutomatically =
prefs.getInt(automaticTunnelingKey, -1) == 1
&& prefs.getBoolean(tunnelingKey, false);
if (wasMediaTunnelingDisabledAutomatically) {
prefs.edit()
.putInt(automaticTunnelingKey, -1)
.putBoolean(tunnelingKey, false)
.apply();
NewPipeSettings.setMediaTunneling(context);
}
}
/**
* Save import path and restart system.
*
* @param importDataUri The import path to save
*/
private void finishImport(final Uri importDataUri) {
// save import path only on success
saveLastImportExportDataUri(importDataUri);
// restart app to properly load db
NavigationHelper.restartApp(requireActivity());
}
private Uri getImportExportDataUri() {
final String path = defaultPreferences.getString(importExportDataPathKey, null);
return isBlank(path) ? null : Uri.parse(path);
}
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
}

View file

@ -1,112 +1,47 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ZipHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final String ZIP_MIME_TYPE = "application/zip";
private final SimpleDateFormat exportDateFormat
= new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ContentSettingsManager manager;
private String importExportDataPathKey;
private String youtubeRestrictedModeEnabledKey;
private Localization initialSelectedLocalization;
private ContentCountry initialSelectedContentCountry;
private String initialLanguage;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
@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);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestImportPathLauncher,
StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestExportPathLauncher,
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
initialSelectedLocalization = org.schabi.newpipe.util.Localization
.getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext());
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
imageQualityPreference.setOnPreferenceChangeListener(
(preference, newValue) -> {
PicassoHelper.setShouldLoadImages((Boolean) newValue);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
@ -153,118 +88,4 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
}
}
private void requestExportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
final StoredFileHelper file
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
exportDatabase(file, lastExportDataUri);
}
}
private void requestImportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
final StoredFileHelper file
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
.create()
.show();
}
}
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
try {
//checkpoint before export
NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, file);
saveLastImportExportDataUri(exportDataUri); // save export path only on success
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
}
}
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
// check if file is supported
if (!ZipHelper.isValidZipFile(file)) {
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
return;
}
try {
if (!manager.ensureDbDirectoryExists()) {
throw new IOException("Could not create databases dir");
}
if (!manager.extractDb(file)) {
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show();
}
// 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(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
});
alert.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));
finishImport(importDataUri);
});
alert.show();
} else {
finishImport(importDataUri);
}
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
}
}
/**
* Save import path and restart system.
*
* @param importDataUri The import path to save
*/
private void finishImport(final Uri importDataUri) {
// save import path only on success
saveLastImportExportDataUri(importDataUri);
// restart app to properly load db
NavigationHelper.restartApp(requireActivity());
}
private Uri getImportExportDataUri() {
final String path = defaultPreferences.getString(importExportDataPathKey, null);
return isBlank(path) ? null : Uri.parse(path);
}
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
}

View file

@ -2,12 +2,10 @@ package org.schabi.newpipe.settings
import android.content.SharedPreferences
import android.util.Log
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@ -25,17 +23,19 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
file.create()
ZipOutputStream(BufferedOutputStream(SharpOutputStream(file.stream)))
ZipOutputStream(SharpOutputStream(file.stream).buffered())
.use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
try {
ObjectOutputStream(FileOutputStream(fileLocator.settings)).use { output ->
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
output.writeObject(preferences.all)
output.flush()
}
} catch (e: IOException) {
Log.e(TAG, "Unable to exportDatabase", e)
if (DEBUG) {
Log.e(TAG, "Unable to exportDatabase", e)
}
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
@ -70,11 +70,14 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
ObjectInputStream(FileInputStream(fileLocator.settings)).use { input ->
ObjectInputStream(fileLocator.settings.inputStream()).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
@ -105,9 +108,13 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
preferenceEditor.commit()
}
} catch (e: IOException) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
} catch (e: ClassNotFoundException) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
}
}
}

View file

@ -9,8 +9,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Optional;
@ -21,20 +21,20 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResourceRegistry();
final Preference allowHeapDumpingPreference
= findPreference(getString(R.string.allow_heap_dumping_key));
final Preference showMemoryLeaksPreference
= findPreference(getString(R.string.show_memory_leaks_key));
final Preference showImageIndicatorsPreference
= findPreference(getString(R.string.show_image_indicators_key));
final Preference checkNewStreamsPreference
= findPreference(getString(R.string.check_new_streams_key));
final Preference crashTheAppPreference
= findPreference(getString(R.string.crash_the_app_key));
final Preference showErrorSnackbarPreference
= findPreference(getString(R.string.show_error_snackbar_key));
final Preference createErrorNotificationPreference
= findPreference(getString(R.string.create_error_notification_key));
final Preference allowHeapDumpingPreference =
findPreference(getString(R.string.allow_heap_dumping_key));
final Preference showMemoryLeaksPreference =
findPreference(getString(R.string.show_memory_leaks_key));
final Preference showImageIndicatorsPreference =
findPreference(getString(R.string.show_image_indicators_key));
final Preference checkNewStreamsPreference =
findPreference(getString(R.string.check_new_streams_key));
final Preference crashTheAppPreference =
findPreference(getString(R.string.crash_the_app_key));
final Preference showErrorSnackbarPreference =
findPreference(getString(R.string.show_error_snackbar_key));
final Preference createErrorNotificationPreference =
findPreference(getString(R.string.create_error_notification_key));
assert allowHeapDumpingPreference != null;
assert showMemoryLeaksPreference != null;

View file

@ -1,5 +1,8 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.decodeUrlUtf8;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
@ -29,10 +32,6 @@ 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;
@ -66,16 +65,10 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
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);
}
prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29);
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice);
}
@ -131,7 +124,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
try {
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
rawUri = decodeUrlUtf8(rawUri);
} catch (final UnsupportedEncodingException e) {
// nothing to do
}
@ -177,11 +170,11 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
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();
new AlertDialog.Builder(ctx)
.setTitle(title)
.setMessage(message)
.setPositiveButton(getString(R.string.ok), null)
.show();
}
@Override
@ -253,8 +246,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
forgetSAFTree(context, defaultPreferences.getString(key, ""));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& !FilePickerActivityHelper.isOwnFileUri(context, uri)) {
if (!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
@ -262,8 +254,8 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
context.grantUriPermission(context.getPackageName(), uri,
StoredDirectoryHelper.PERMISSION_FLAGS);
final StoredDirectoryHelper mainStorage
= new StoredDirectoryHelper(context, uri, null);
final StoredDirectoryHelper mainStorage =
new StoredDirectoryHelper(context, uri, null);
Log.i(TAG, "Acquiring tree success from " + uri.toString());
if (!mainStorage.canWrite()) {

View file

@ -0,0 +1,45 @@
package org.schabi.newpipe.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.SwitchPreferenceCompat;
import org.schabi.newpipe.R;
public class ExoPlayerSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
addPreferencesFromResourceRegistry();
final String disabledMediaTunnelingAutomaticallyKey =
getString(R.string.disabled_media_tunneling_automatically_key);
final SwitchPreferenceCompat disableMediaTunnelingPref =
(SwitchPreferenceCompat) requirePreference(R.string.disable_media_tunneling_key);
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(requireContext());
final boolean mediaTunnelingAutomaticallyDisabled =
prefs.getInt(disabledMediaTunnelingAutomaticallyKey, -1) == 1;
final String summaryText = getString(R.string.disable_media_tunneling_summary);
disableMediaTunnelingPref.setSummary(mediaTunnelingAutomaticallyDisabled
? summaryText + " " + getString(R.string.disable_media_tunneling_automatic_info)
: summaryText);
disableMediaTunnelingPref.setOnPreferenceChangeListener((Preference p, Object enabled) -> {
if (Boolean.FALSE.equals(enabled)) {
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putInt(disabledMediaTunnelingAutomaticallyKey, 0)
.apply();
// the info text might have been shown before
p.setSummary(R.string.disable_media_tunneling_summary);
}
return true;
});
}
}

View file

@ -132,7 +132,6 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
disposables.add(getWholeStreamHistoryDisposable(context, recordManager));
disposables.add(getRemoveOrphanedRecordsDisposable(context, recordManager));
}))
.create()
.show();
}
@ -144,7 +143,6 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) ->
disposables.add(getDeletePlaybackStatesDisposable(context, recordManager))))
.create()
.show();
}
@ -156,7 +154,6 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) ->
disposables.add(getDeleteSearchHistoryDisposable(context, recordManager))))
.create()
.show();
}
}

View file

@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
// Check if the app is updatable
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
getPreferenceScreen().removePreference(
findPreference(getString(R.string.update_pref_screen_key)));

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
@ -9,14 +11,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
import java.util.Set;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/*
* Created by k3b on 07.01.2016.
*
@ -44,24 +45,8 @@ public final class NewPipeSettings {
private NewPipeSettings() { }
public static void initSettings(final Context context) {
// check if there are entries in the prefs to determine whether this is the first app run
Boolean isFirstRun = null;
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
.getAll().keySet();
for (final String key: prefsKeys) {
// ACRA stores some info in the prefs during app initialization
// which happens before this method is called. Therefore ignore ACRA-related keys.
if (!key.toLowerCase().startsWith("acra")) {
isFirstRun = false;
break;
}
}
if (isFirstRun == null) {
isFirstRun = true;
}
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.initMigrations(context, isFirstRun);
SettingMigrations.runMigrationsIfNeeded(context);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
@ -73,9 +58,12 @@ public final class NewPipeSettings {
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true);
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);
disableMediaTunnelingIfNecessary(context);
}
static void saveDefaultVideoDownloadDirectory(final Context context) {
@ -116,7 +104,7 @@ public final class NewPipeSettings {
public static boolean useStorageAccessFramework(final Context context) {
// There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with a
// remote (see #6455).
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || DeviceUtils.isFireTv()) {
if (DeviceUtils.isFireTv()) {
return false;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return true;
@ -152,4 +140,48 @@ public final class NewPipeSettings {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_remote_search_suggestions_key);
}
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String disabledTunnelingAutomaticallyKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
final String blacklistVersionKey =
context.getString(R.string.media_tunneling_device_blacklist_version);
final int lastMediaTunnelingUpdate = prefs.getInt(blacklistVersionKey, 0);
final boolean wasDeviceBlacklistUpdated =
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION != lastMediaTunnelingUpdate;
final boolean wasMediaTunnelingEnabledByUser =
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (App.getApp().isFirstRun()
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}
}
/**
* Check if device does not support media tunneling
* and disable that exoplayer feature if necessary.
* @see DeviceUtils#shouldSupportMediaTunneling()
* @param context
*/
public static void setMediaTunneling(@NonNull final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (!DeviceUtils.shouldSupportMediaTunneling()) {
prefs.edit()
.putBoolean(context.getString(R.string.disable_media_tunneling_key), true)
.putInt(context.getString(
R.string.disabled_media_tunneling_automatically_key), 1)
.putInt(context.getString(R.string.media_tunneling_device_blacklist_version),
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION)
.apply();
} else {
prefs.edit()
.putInt(context.getString(R.string.media_tunneling_device_blacklist_version),
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION).apply();
}
}
}

View file

@ -1,19 +1,9 @@
package org.schabi.newpipe.settings
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import org.schabi.newpipe.R
class NotificationSettingsFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResourceRegistry()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
colorizePref?.let {
preferenceScreen.removePreference(it)
}
}
}
}

View file

@ -26,6 +26,10 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.notifications_settings)
// main check is done in onResume, but also do it here to prevent flickering
preferenceScreen.isEnabled =
NotificationHelper.areNotificationsEnabledOnDevice(requireContext())
}
override fun onStart() {
@ -64,7 +68,7 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
// If they are disabled, show a snackbar informing the user about that
// while allowing them to open the device's app settings.
val enabled = NotificationHelper.areNotificationsEnabledOnDevice(requireContext())
preferenceScreen.isEnabled = enabled
preferenceScreen.isEnabled = enabled // it is disabled by default, see the xml
if (!enabled) {
if (notificationWarningSnackbar == null) {
notificationWarningSnackbar = Snackbar.make(
@ -85,9 +89,6 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
show()
}
}
} else {
notificationWarningSnackbar?.dismiss()
notificationWarningSnackbar = null
}
// (Re-)Create loader
@ -102,6 +103,9 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
loader?.dispose()
loader = null
notificationWarningSnackbar?.dismiss()
notificationWarningSnackbar = null
super.onPause()
}

View file

@ -12,28 +12,27 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.databinding.FragmentInstanceListBinding;
import org.schabi.newpipe.databinding.ItemInstanceBinding;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.PeertubeHelper;
@ -41,7 +40,6 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
@ -50,12 +48,11 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class PeertubeInstanceListFragment extends Fragment {
private final List<PeertubeInstance> instanceList = new ArrayList<>();
private PeertubeInstance selectedInstance;
private String savedInstanceListKey;
private InstanceListAdapter instanceListAdapter;
private ProgressBar progressBar;
private FragmentInstanceListBinding binding;
private SharedPreferences sharedPreferences;
private CompositeDisposable disposables = new CompositeDisposable();
@ -71,7 +68,6 @@ public class PeertubeInstanceListFragment extends Fragment {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
savedInstanceListKey = getString(R.string.peertube_instance_list_key);
selectedInstance = PeertubeHelper.getCurrentInstance();
updateInstanceList();
setHasOptionsMenu(true);
}
@ -79,7 +75,8 @@ public class PeertubeInstanceListFragment extends Fragment {
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_instance_list, container, false);
binding = FragmentInstanceListBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
@ -87,26 +84,17 @@ public class PeertubeInstanceListFragment extends Fragment {
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
initViews(rootView);
}
private void initViews(@NonNull final View rootView) {
final TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV);
instanceHelpTV.setText(getString(R.string.peertube_instance_url_help,
binding.instanceHelpTV.setText(getString(R.string.peertube_instance_url_help,
getString(R.string.peertube_instance_list_url)));
initButton(rootView);
final RecyclerView listInstances = rootView.findViewById(R.id.instances);
listInstances.setLayoutManager(new LinearLayoutManager(requireContext()));
binding.addInstanceButton.setOnClickListener(v -> showAddItemDialog(requireContext()));
binding.instances.setLayoutManager(new LinearLayoutManager(requireContext()));
final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(listInstances);
itemTouchHelper.attachToRecyclerView(binding.instances);
instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper);
listInstances.setAdapter(instanceListAdapter);
progressBar = rootView.findViewById(R.id.loading_progress_bar);
binding.instances.setAdapter(instanceListAdapter);
instanceListAdapter.submitList(PeertubeHelper.getInstanceList(requireContext()));
}
@Override
@ -131,6 +119,12 @@ public class PeertubeInstanceListFragment extends Fragment {
disposables = null;
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@ -156,11 +150,6 @@ public class PeertubeInstanceListFragment extends Fragment {
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void updateInstanceList() {
instanceList.clear();
instanceList.addAll(PeertubeHelper.getInstanceList(requireContext()));
}
private void selectInstance(final PeertubeInstance instance) {
selectedInstance = PeertubeHelper.selectInstance(instance, requireContext());
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
@ -168,7 +157,7 @@ public class PeertubeInstanceListFragment extends Fragment {
private void saveChanges() {
final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
for (final PeertubeInstance instance : instanceList) {
for (final PeertubeInstance instance : instanceListAdapter.getCurrentList()) {
jsonWriter.object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
@ -179,35 +168,28 @@ public class PeertubeInstanceListFragment extends Fragment {
}
private void restoreDefaults() {
new AlertDialog.Builder(requireContext())
final Context context = requireContext();
new AlertDialog.Builder(context)
.setTitle(R.string.restore_defaults)
.setMessage(R.string.restore_defaults_confirmation)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, which) -> {
sharedPreferences.edit().remove(savedInstanceListKey).apply();
selectInstance(PeertubeInstance.DEFAULT_INSTANCE);
updateInstanceList();
instanceListAdapter.notifyDataSetChanged();
instanceListAdapter.submitList(PeertubeHelper.getInstanceList(context));
})
.show();
}
private void initButton(final View rootView) {
final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton);
fab.setOnClickListener(v ->
showAddItemDialog(requireContext()));
}
private void showAddItemDialog(final Context c) {
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
final var dialogBinding = DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
dialogBinding.dialogEditText.setHint(R.string.peertube_instance_add_help);
new AlertDialog.Builder(c)
.setTitle(R.string.peertube_instance_add_title)
.setIcon(R.drawable.place_holder_peertube)
.setIcon(R.drawable.ic_placeholder_peertube)
.setView(dialogBinding.getRoot())
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog1, which) -> {
@ -222,17 +204,17 @@ public class PeertubeInstanceListFragment extends Fragment {
if (cleanUrl == null) {
return;
}
progressBar.setVisibility(View.VISIBLE);
binding.loadingProgressBar.setVisibility(View.VISIBLE);
final Disposable disposable = Single.fromCallable(() -> {
final PeertubeInstance instance = new PeertubeInstance(cleanUrl);
instance.fetchInstanceMetaData();
return instance;
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe((instance) -> {
progressBar.setVisibility(View.GONE);
binding.loadingProgressBar.setVisibility(View.GONE);
add(instance);
}, e -> {
progressBar.setVisibility(View.GONE);
binding.loadingProgressBar.setVisibility(View.GONE);
Toast.makeText(getActivity(), R.string.peertube_instance_add_fail,
Toast.LENGTH_SHORT).show();
});
@ -255,7 +237,7 @@ public class PeertubeInstanceListFragment extends Fragment {
return null;
}
// only allow if not already exists
for (final PeertubeInstance instance : instanceList) {
for (final PeertubeInstance instance : instanceListAdapter.getCurrentList()) {
if (instance.getUrl().equals(cleanUrl)) {
Toast.makeText(getActivity(), R.string.peertube_instance_add_exists,
Toast.LENGTH_SHORT).show();
@ -266,8 +248,9 @@ public class PeertubeInstanceListFragment extends Fragment {
}
private void add(final PeertubeInstance instance) {
instanceList.add(instance);
instanceListAdapter.notifyDataSetChanged();
final var list = new ArrayList<>(instanceListAdapter.getCurrentList());
list.add(instance);
instanceListAdapter.submitList(list);
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
@ -281,8 +264,7 @@ public class PeertubeInstanceListFragment extends Fragment {
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int minimumAbsVelocity = Math.max(12,
Math.abs(standardSpeed));
final int minimumAbsVelocity = Math.max(12, Math.abs(standardSpeed));
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@ -316,17 +298,19 @@ public class PeertubeInstanceListFragment extends Fragment {
final int swipeDir) {
final int position = viewHolder.getBindingAdapterPosition();
// do not allow swiping the selected instance
if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
if (instanceListAdapter.getCurrentList().get(position).getUrl()
.equals(selectedInstance.getUrl())) {
instanceListAdapter.notifyItemChanged(position);
return;
}
instanceList.remove(position);
instanceListAdapter.notifyItemRemoved(position);
final var list = new ArrayList<>(instanceListAdapter.getCurrentList());
list.remove(position);
if (instanceList.isEmpty()) {
instanceList.add(selectedInstance);
instanceListAdapter.notifyItemInserted(0);
if (list.isEmpty()) {
list.add(selectedInstance);
}
instanceListAdapter.submitList(list);
}
};
}
@ -336,96 +320,94 @@ public class PeertubeInstanceListFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
private class InstanceListAdapter
extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> {
extends ListAdapter<PeertubeInstance, InstanceListAdapter.TabViewHolder> {
private final LayoutInflater inflater;
private final ItemTouchHelper itemTouchHelper;
private RadioButton lastChecked;
InstanceListAdapter(final Context context, final ItemTouchHelper itemTouchHelper) {
super(new PeertubeInstanceCallback());
this.itemTouchHelper = itemTouchHelper;
this.inflater = LayoutInflater.from(context);
}
public void swapItems(final int fromPosition, final int toPosition) {
Collections.swap(instanceList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
final var list = new ArrayList<>(getCurrentList());
Collections.swap(list, fromPosition, toPosition);
submitList(list);
}
@NonNull
@Override
public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
final View view = inflater.inflate(R.layout.item_instance, parent, false);
return new InstanceListAdapter.TabViewHolder(view);
return new InstanceListAdapter.TabViewHolder(ItemInstanceBinding.inflate(inflater,
parent, false));
}
@Override
public void onBindViewHolder(@NonNull final InstanceListAdapter.TabViewHolder holder,
final int position) {
holder.bind(position, holder);
}
@Override
public int getItemCount() {
return instanceList.size();
holder.bind(position);
}
class TabViewHolder extends RecyclerView.ViewHolder {
private final AppCompatImageView instanceIconView;
private final TextView instanceNameView;
private final TextView instanceUrlView;
private final RadioButton instanceRB;
private final ImageView handle;
private final ItemInstanceBinding itemBinding;
TabViewHolder(final View itemView) {
super(itemView);
instanceIconView = itemView.findViewById(R.id.instanceIcon);
instanceNameView = itemView.findViewById(R.id.instanceName);
instanceUrlView = itemView.findViewById(R.id.instanceUrl);
instanceRB = itemView.findViewById(R.id.selectInstanceRB);
handle = itemView.findViewById(R.id.handle);
TabViewHolder(final ItemInstanceBinding binding) {
super(binding.getRoot());
this.itemBinding = binding;
}
@SuppressLint("ClickableViewAccessibility")
void bind(final int position, final TabViewHolder holder) {
handle.setOnTouchListener(getOnTouchListener(holder));
final PeertubeInstance instance = instanceList.get(position);
instanceNameView.setText(instance.getName());
instanceUrlView.setText(instance.getUrl());
instanceRB.setOnCheckedChangeListener(null);
if (selectedInstance.getUrl().equals(instance.getUrl())) {
if (lastChecked != null && lastChecked != instanceRB) {
lastChecked.setChecked(false);
}
instanceRB.setChecked(true);
lastChecked = instanceRB;
}
instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
selectInstance(instance);
if (lastChecked != null && lastChecked != instanceRB) {
lastChecked.setChecked(false);
}
lastChecked = instanceRB;
}
});
instanceIconView.setImageResource(R.drawable.place_holder_peertube);
}
@SuppressLint("ClickableViewAccessibility")
private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) {
return (view, motionEvent) -> {
void bind(final int position) {
itemBinding.handle.setOnTouchListener((view, motionEvent) -> {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (itemTouchHelper != null && getItemCount() > 1) {
itemTouchHelper.startDrag(item);
itemTouchHelper.startDrag(this);
return true;
}
}
return false;
};
});
final PeertubeInstance instance = getItem(position);
itemBinding.instanceName.setText(instance.getName());
itemBinding.instanceUrl.setText(instance.getUrl());
itemBinding.selectInstanceRB.setOnCheckedChangeListener(null);
if (selectedInstance.getUrl().equals(instance.getUrl())) {
if (lastChecked != null && lastChecked != itemBinding.selectInstanceRB) {
lastChecked.setChecked(false);
}
itemBinding.selectInstanceRB.setChecked(true);
lastChecked = itemBinding.selectInstanceRB;
}
itemBinding.selectInstanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
selectInstance(instance);
if (lastChecked != null && lastChecked != itemBinding.selectInstanceRB) {
lastChecked.setChecked(false);
}
lastChecked = itemBinding.selectInstanceRB;
}
});
itemBinding.instanceIcon.setImageResource(R.drawable.ic_placeholder_peertube);
}
}
}
private static class PeertubeInstanceCallback extends DiffUtil.ItemCallback<PeertubeInstance> {
@Override
public boolean areItemsTheSame(@NonNull final PeertubeInstance oldItem,
@NonNull final PeertubeInstance newItem) {
return oldItem.getUrl().equals(newItem.getUrl());
}
@Override
public boolean areContentsTheSame(@NonNull final PeertubeInstance oldItem,
@NonNull final PeertubeInstance newItem) {
return oldItem.getName().equals(newItem.getName())
&& oldItem.getUrl().equals(newItem.getUrl());
}
}
}

View file

@ -1,19 +1,9 @@
package org.schabi.newpipe.settings
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import org.schabi.newpipe.R
class PlayerNotificationSettingsFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResourceRegistry()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
colorizePref?.let {
preferenceScreen.removePreference(it)
}
}
}
}

View file

@ -19,7 +19,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;

View file

@ -25,7 +25,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Vector;

View file

@ -2,11 +2,12 @@ package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
@ -31,9 +32,9 @@ public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
private static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(final Context context) {
public void migrate(@NonNull final Context context) {
// We changed the content of the dialog which opens when sharing a link to NewPipe
// by removing the "open detail page" option.
// Therefore, show the dialog once again to ensure users need to choose again and are
@ -45,9 +46,9 @@ public final class SettingMigrations {
}
};
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
protected void migrate(final Context context) {
protected void migrate(@NonNull final Context context) {
// The new application workflow introduced in #2907 allows minimizing videos
// while playing to do other stuff within the app.
// For an even better workflow, we minimize a stream when switching the app to play in
@ -64,25 +65,25 @@ public final class SettingMigrations {
}
};
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
protected void migrate(final Context context) {
protected void migrate(@NonNull final Context context) {
// Storage Access Framework implementation was improved in #5415, allowing the modern
// and standard way to access folders and files to be used consistently everywhere.
// We reset the setting to its default value, i.e. "use SAF", since now there are no
// more issues with SAF and users should use that one instead of the old
// NoNonsenseFilePicker. SAF does not work on KitKat and below, though, so the setting
// is set to false in that case. Also, there's a bug on FireOS in which SAF open/close
// NoNonsenseFilePicker. Also, there's a bug on FireOS in which SAF open/close
// dialogs cannot be confirmed with a remote (see #6455).
sp.edit().putBoolean(context.getString(R.string.storage_use_saf),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& !DeviceUtils.isFireTv()).apply();
sp.edit().putBoolean(
context.getString(R.string.storage_use_saf),
!DeviceUtils.isFireTv()
).apply();
}
};
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
protected void migrate(final Context context) {
protected void migrate(@NonNull final Context context) {
// Pull request #3546 added support for choosing the type of search suggestions to
// show, replacing the on-off switch used before, so migrate the previous user choice
@ -109,6 +110,39 @@ public final class SettingMigrations {
}
};
private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
@Override
protected void migrate(@NonNull final Context context) {
final boolean brightness = sp.getBoolean("brightness_gesture_control", true);
final boolean volume = sp.getBoolean("volume_gesture_control", true);
final SharedPreferences.Editor editor = sp.edit();
editor.putString(context.getString(R.string.right_gesture_control_key),
context.getString(volume
? R.string.volume_control_key : R.string.none_control_key));
editor.putString(context.getString(R.string.left_gesture_control_key),
context.getString(brightness
? R.string.brightness_control_key : R.string.none_control_key));
editor.apply();
}
};
public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
@Override
protected void migrate(@NonNull final Context context) {
final boolean loadImages = sp.getBoolean("download_thumbnail_key", true);
sp.edit()
.putString(context.getString(R.string.image_quality_key),
context.getString(loadImages
? R.string.image_quality_default
: R.string.image_quality_none_key))
.apply();
}
};
/**
* List of all implemented migrations.
* <p>
@ -120,22 +154,24 @@ public final class SettingMigrations {
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 4;
private static final int VERSION = 6;
public static void initMigrations(final Context context, final boolean isFirstRun) {
public static void runMigrationsIfNeeded(@NonNull final Context context) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (isFirstRun) {
if (App.getApp().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {
@ -193,7 +229,7 @@ public final class SettingMigrations {
return oldVersion >= currentVersion;
}
protected abstract void migrate(Context context);
protected abstract void migrate(@NonNull Context context);
}

View file

@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
*/
private void ensureSearchRepresentsApplicationState() {
// Check if the update settings are available
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
SettingsResourceRegistry.getInstance()
.getEntryByPreferencesResId(R.xml.update_settings)
.setSearchable(false);

View file

@ -40,6 +40,8 @@ public final class SettingsResourceRegistry {
add(PlayerNotificationSettingsFragment.class, R.xml.player_notification_settings);
add(UpdateSettingsFragment.class, R.xml.update_settings);
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings);
}
private SettingRegistryEntry add(

View file

@ -1,40 +1,35 @@
package org.schabi.newpipe.settings;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewVersionWorker;
import org.schabi.newpipe.R;
public class UpdateSettingsFragment extends BasePreferenceFragment {
private final Preference.OnPreferenceChangeListener updatePreferenceChange
= (preference, checkForUpdates) -> {
private final Preference.OnPreferenceChangeListener updatePreferenceChange = (p, nVal) -> {
final boolean checkForUpdates = (boolean) nVal;
defaultPreferences.edit()
.putBoolean(getString(R.string.update_app_key), (boolean) checkForUpdates).apply();
.putBoolean(getString(R.string.update_app_key), checkForUpdates)
.apply();
if ((boolean) checkForUpdates) {
checkNewVersionNow();
if (checkForUpdates) {
NewVersionWorker.enqueueNewVersionCheckingWork(requireContext(), true);
}
return true;
};
private final Preference.OnPreferenceClickListener manualUpdateClick
= preference -> {
private final Preference.OnPreferenceClickListener manualUpdateClick = preference -> {
Toast.makeText(getContext(), R.string.checking_updates_toast, Toast.LENGTH_SHORT).show();
checkNewVersionNow();
NewVersionWorker.enqueueNewVersionCheckingWork(requireContext(), true);
return true;
};
private void checkNewVersionNow() {
// Search for updates immediately when update checks are enabled.
// Reset the expire time. This is necessary to check for an update immediately.
defaultPreferences.edit()
.putLong(getString(R.string.update_expiry_key), 0).apply();
NewVersionWorker.enqueueNewVersionCheckingWork(getContext());
}
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResourceRegistry();
@ -44,4 +39,38 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
findPreference(getString(R.string.manual_update_key))
.setOnPreferenceClickListener(manualUpdateClick);
}
public static void askForConsentToUpdateChecks(final Context context) {
new AlertDialog.Builder(context)
.setTitle(context.getString(R.string.check_for_updates))
.setMessage(context.getString(R.string.auto_update_check_description))
.setPositiveButton(context.getString(R.string.yes), (d, w) -> {
d.dismiss();
setAutoUpdateCheckEnabled(context, true);
})
.setNegativeButton(R.string.no, (d, w) -> {
d.dismiss();
// set explicitly to false, since the default is true on previous versions
setAutoUpdateCheckEnabled(context, false);
})
.show();
}
private static void setAutoUpdateCheckEnabled(final Context context, final boolean enabled) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(context.getString(R.string.update_app_key), enabled)
.putBoolean(context.getString(R.string.update_check_consent_key), true)
.apply();
}
/**
* Whether the user was asked for consent to automatically check for app updates.
* @param context
* @return true if the user was asked for consent, false otherwise
*/
public static boolean wasUserAskedForConsent(final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.update_check_consent_key), false);
}
}

View file

@ -13,6 +13,7 @@ import androidx.preference.ListPreference;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import java.util.LinkedList;
@ -26,15 +27,15 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResourceRegistry();
updateSeekOptions();
listener = (sharedPreferences, s) -> {
updateResolutionOptions();
listener = (sharedPreferences, key) -> {
// on M and above, if user chooses to minimise to popup player on exit
// and the app doesn't have display over other apps permission,
// show a snackbar to let the user give permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& s.equals(getString(R.string.minimize_on_exit_key))) {
final String newSetting = sharedPreferences.getString(s, null);
&& getString(R.string.minimize_on_exit_key).equals(key)) {
final String newSetting = sharedPreferences.getString(key, null);
if (newSetting != null
&& newSetting.equals(getString(R.string.minimize_on_exit_popup_key))
&& !Settings.canDrawOverlays(getContext())) {
@ -46,12 +47,86 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
.show();
}
} else if (s.equals(getString(R.string.use_inexact_seek_key))) {
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
updateSeekOptions();
} else if (getString(R.string.show_higher_resolutions_key).equals(key)) {
updateResolutionOptions();
}
};
}
/**
* Update default resolution, default popup resolution & mobile data resolution options.
* <br />
* Show high resolutions when "Show higher resolution" option is enabled.
* Set default resolution to "best resolution" when "Show higher resolution" option
* is disabled.
*/
private void updateResolutionOptions() {
final Resources resources = getResources();
final boolean showHigherResolutions = getPreferenceManager().getSharedPreferences()
.getBoolean(resources.getString(R.string.show_higher_resolutions_key), false);
// get sorted resolution lists
final List<String> resolutionListDescriptions = ListHelper.getSortedResolutionList(
resources,
R.array.resolution_list_description,
R.array.high_resolution_list_descriptions,
showHigherResolutions);
final List<String> resolutionListValues = ListHelper.getSortedResolutionList(
resources,
R.array.resolution_list_values,
R.array.high_resolution_list_values,
showHigherResolutions);
final List<String> limitDataUsageResolutionValues = ListHelper.getSortedResolutionList(
resources,
R.array.limit_data_usage_values_list,
R.array.high_resolution_limit_data_usage_values_list,
showHigherResolutions);
final List<String> limitDataUsageResolutionDescriptions = ListHelper
.getSortedResolutionList(resources,
R.array.limit_data_usage_description_list,
R.array.high_resolution_list_descriptions,
showHigherResolutions);
// get resolution preferences
final ListPreference defaultResolution = findPreference(
getString(R.string.default_resolution_key));
final ListPreference defaultPopupResolution = findPreference(
getString(R.string.default_popup_resolution_key));
final ListPreference mobileDataResolution = findPreference(
getString(R.string.limit_mobile_data_usage_key));
// update resolution preferences with new resolutions, entries & values for each
defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
defaultResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
defaultPopupResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
defaultPopupResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
mobileDataResolution.setEntries(
limitDataUsageResolutionDescriptions.toArray(new String[0]));
mobileDataResolution.setEntryValues(limitDataUsageResolutionValues.toArray(new String[0]));
// if "Show higher resolution" option is disabled,
// set default resolution to "best resolution"
if (!showHigherResolutions) {
if (ListHelper.isHighResolutionSelected(defaultResolution.getValue(),
R.array.high_resolution_list_values,
resources)) {
defaultResolution.setValueIndex(0);
}
if (ListHelper.isHighResolutionSelected(defaultPopupResolution.getValue(),
R.array.high_resolution_list_values,
resources)) {
defaultPopupResolution.setValueIndex(0);
}
if (ListHelper.isHighResolutionSelected(mobileDataResolution.getValue(),
R.array.high_resolution_limit_data_usage_values_list,
resources)) {
mobileDataResolution.setValueIndex(0);
}
}
}
/**
* Update fast-forward/-rewind seek duration options
* according to language and inexact seek setting.

View file

@ -1,38 +1,28 @@
package org.schabi.newpipe.settings.custom;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.widget.TextViewCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import org.schabi.newpipe.player.notification.NotificationConstants;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class NotificationActionsPreference extends Preference {
@ -42,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
}
@Nullable private NotificationSlot[] notificationSlots = null;
@Nullable private List<Integer> compactSlots = null;
private NotificationSlot[] notificationSlots;
private List<Integer> compactSlots;
////////////////////////////////////////////////////////////////////////////
// Lifecycle
@ -53,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
((TextView) holder.itemView.findViewById(R.id.summary))
.setText(R.string.notification_actions_summary_android13);
}
holder.itemView.setClickable(false);
setupActions(holder.itemView);
}
@ -61,7 +57,9 @@ public class NotificationActionsPreference extends Preference {
public void onDetached() {
super.onDetached();
saveChanges();
getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
// set package to this app's package to prevent the intent from being seen outside
getContext().sendBroadcast(new Intent(ACTION_RECREATE_NOTIFICATION)
.setPackage(App.PACKAGE_NAME));
}
@ -70,13 +68,27 @@ public class NotificationActionsPreference extends Preference {
////////////////////////////////////////////////////////////////////////////
private void setupActions(@NonNull final View view) {
compactSlots =
NotificationConstants.getCompactSlotsFromPreferences(
getContext(), getSharedPreferences(), 5);
notificationSlots = new NotificationSlot[5];
for (int i = 0; i < 5; i++) {
notificationSlots[i] = new NotificationSlot(i, view);
compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
getContext(), getSharedPreferences()));
notificationSlots = IntStream.range(0, 5)
.mapToObj(i -> new NotificationSlot(getContext(), getSharedPreferences(), i, view,
compactSlots.contains(i), this::onToggleCompactSlot))
.toArray(NotificationSlot[]::new);
}
private void onToggleCompactSlot(final int i, final CheckBox checkBox) {
if (checkBox.isChecked()) {
compactSlots.remove((Integer) i);
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(getContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
}
checkBox.toggle();
}
@ -96,148 +108,10 @@ public class NotificationActionsPreference extends Preference {
for (int i = 0; i < 5; i++) {
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i].selectedAction);
notificationSlots[i].getSelectedAction());
}
editor.apply();
}
}
////////////////////////////////////////////////////////////////////////////
// Notification action
////////////////////////////////////////////////////////////////////////////
private static final int[] SLOT_ITEMS = {
R.id.notificationAction0,
R.id.notificationAction1,
R.id.notificationAction2,
R.id.notificationAction3,
R.id.notificationAction4,
};
private static final int[] SLOT_TITLES = {
R.string.notification_action_0_title,
R.string.notification_action_1_title,
R.string.notification_action_2_title,
R.string.notification_action_3_title,
R.string.notification_action_4_title,
};
private class NotificationSlot {
final int i;
@NotificationConstants.Action int selectedAction;
ImageView icon;
TextView summary;
NotificationSlot(final int actionIndex, final View parentView) {
this.i = actionIndex;
final View view = parentView.findViewById(SLOT_ITEMS[i]);
setupSelectedAction(view);
setupTitle(view);
setupCheckbox(view);
}
void setupTitle(final View view) {
((TextView) view.findViewById(R.id.notificationActionTitle))
.setText(SLOT_TITLES[i]);
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
v -> openActionChooserDialog());
}
void setupCheckbox(final View view) {
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
compactSlotCheckBox.setChecked(compactSlots.contains(i));
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
v -> {
if (compactSlotCheckBox.isChecked()) {
compactSlots.remove((Integer) i);
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(getContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
}
compactSlotCheckBox.toggle();
});
}
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
selectedAction = getSharedPreferences().getInt(
getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
updateInfo();
}
void updateInfo() {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(getContext());
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
.setTitle(SLOT_TITLES[i])
.setView(radioGroup)
.setCancelable(true)
.create();
final View.OnClickListener radioButtonsClickListener = v -> {
selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
updateInfo();
alertDialog.dismiss();
};
for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
final RadioButton radioButton
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
// if present set action icon with correct color
if (NotificationConstants.ACTION_ICONS[action] != 0) {
Drawable drawable = AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[action]);
if (drawable != null) {
final int color = ThemeHelper.resolveColorFromAttr(getContext(),
android.R.attr.textColorPrimary);
drawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(drawable, color);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
null, null, drawable, null);
}
}
radioButton.setText(NotificationConstants.getActionName(getContext(), action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
radioGroup.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(getContext())) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
}
}

View file

@ -0,0 +1,172 @@
package org.schabi.newpipe.settings.custom;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.widget.TextViewCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.Objects;
import java.util.function.BiConsumer;
class NotificationSlot {
private static final int[] SLOT_ITEMS = {
R.id.notificationAction0,
R.id.notificationAction1,
R.id.notificationAction2,
R.id.notificationAction3,
R.id.notificationAction4,
};
private static final int[] SLOT_TITLES = {
R.string.notification_action_0_title,
R.string.notification_action_1_title,
R.string.notification_action_2_title,
R.string.notification_action_3_title,
R.string.notification_action_4_title,
};
private final int i;
private @NotificationConstants.Action int selectedAction;
private final Context context;
private final BiConsumer<Integer, CheckBox> onToggleCompactSlot;
private ImageView icon;
private TextView summary;
NotificationSlot(final Context context,
final SharedPreferences prefs,
final int actionIndex,
final View parentView,
final boolean isCompactSlotChecked,
final BiConsumer<Integer, CheckBox> onToggleCompactSlot) {
this.context = context;
this.i = actionIndex;
this.onToggleCompactSlot = onToggleCompactSlot;
selectedAction = Objects.requireNonNull(prefs).getInt(
context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
final View view = parentView.findViewById(SLOT_ITEMS[i]);
// only show the last two notification slots on Android 13+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || i >= 3) {
setupSelectedAction(view);
setupTitle(view);
setupCheckbox(view, isCompactSlotChecked);
} else {
view.setVisibility(View.GONE);
}
}
void setupTitle(final View view) {
((TextView) view.findViewById(R.id.notificationActionTitle))
.setText(SLOT_TITLES[i]);
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
v -> openActionChooserDialog());
}
void setupCheckbox(final View view, final boolean isCompactSlotChecked) {
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// there are no compact slots to customize on Android 13+
compactSlotCheckBox.setVisibility(View.GONE);
view.findViewById(R.id.notificationActionCheckBoxClickableArea)
.setVisibility(View.GONE);
return;
}
compactSlotCheckBox.setChecked(isCompactSlotChecked);
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
v -> onToggleCompactSlot.accept(i, compactSlotCheckBox));
}
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
updateInfo();
}
void updateInfo() {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(context,
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(context, selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(context);
final SingleChoiceDialogViewBinding binding =
SingleChoiceDialogViewBinding.inflate(inflater);
final AlertDialog alertDialog = new AlertDialog.Builder(context)
.setTitle(SLOT_TITLES[i])
.setView(binding.getRoot())
.setCancelable(true)
.create();
final View.OnClickListener radioButtonsClickListener = v -> {
selectedAction = NotificationConstants.ALL_ACTIONS[v.getId()];
updateInfo();
alertDialog.dismiss();
};
for (int id = 0; id < NotificationConstants.ALL_ACTIONS.length; ++id) {
final int action = NotificationConstants.ALL_ACTIONS[id];
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
.getRoot();
// if present set action icon with correct color
final int iconId = NotificationConstants.ACTION_ICONS[action];
if (iconId != 0) {
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
final var color = ColorStateList.valueOf(ThemeHelper
.resolveColorFromAttr(context, android.R.attr.textColorPrimary));
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
}
radioButton.setText(NotificationConstants.getActionName(context, action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
binding.list.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(context)) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
@NotificationConstants.Action
public int getSelectedAction() {
return selectedAction;
}
}

View file

@ -1,15 +1,13 @@
package org.schabi.newpipe.settings.notifications
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.schabi.newpipe.R
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.ItemNotificationConfigBinding
import org.schabi.newpipe.settings.notifications.NotificationModeConfigAdapter.SubscriptionHolder
/**
@ -19,85 +17,46 @@ import org.schabi.newpipe.settings.notifications.NotificationModeConfigAdapter.S
*/
class NotificationModeConfigAdapter(
private val listener: ModeToggleListener
) : RecyclerView.Adapter<SubscriptionHolder>() {
private val differ = AsyncListDiffer(this, DiffCallback())
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): SubscriptionHolder {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.item_notification_config, viewGroup, false)
return SubscriptionHolder(view, listener)
}
override fun onBindViewHolder(subscriptionHolder: SubscriptionHolder, i: Int) {
subscriptionHolder.bind(differ.currentList[i])
}
fun getItem(position: Int): SubscriptionItem = differ.currentList[position]
override fun getItemCount() = differ.currentList.size
override fun getItemId(position: Int): Long {
return differ.currentList[position].id
}
fun getCurrentList(): List<SubscriptionItem> = differ.currentList
fun update(newData: List<SubscriptionEntity>) {
differ.submitList(
newData.map {
SubscriptionItem(
id = it.uid,
title = it.name,
notificationMode = it.notificationMode,
serviceId = it.serviceId,
url = it.url
)
}
) : ListAdapter<SubscriptionItem, SubscriptionHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, i: Int): SubscriptionHolder {
return SubscriptionHolder(
ItemNotificationConfigBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
data class SubscriptionItem(
val id: Long,
val title: String,
@NotificationMode
val notificationMode: Int,
val serviceId: Int,
val url: String
)
override fun onBindViewHolder(holder: SubscriptionHolder, position: Int) {
holder.bind(currentList[position])
}
class SubscriptionHolder(
itemView: View,
private val listener: ModeToggleListener
) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val checkedTextView = itemView as CheckedTextView
fun update(newData: List<SubscriptionEntity>) {
val items = newData.map {
SubscriptionItem(it.uid, it.name, it.notificationMode, it.serviceId, it.url)
}
submitList(items)
}
inner class SubscriptionHolder(
private val itemBinding: ItemNotificationConfigBinding
) : RecyclerView.ViewHolder(itemBinding.root) {
init {
itemView.setOnClickListener(this)
itemView.setOnClickListener {
val mode = if (itemBinding.root.isChecked) {
NotificationMode.DISABLED
} else {
NotificationMode.ENABLED
}
listener.onModeChange(bindingAdapterPosition, mode)
}
}
fun bind(data: SubscriptionItem) {
checkedTextView.text = data.title
checkedTextView.isChecked = data.notificationMode != NotificationMode.DISABLED
}
override fun onClick(v: View) {
val mode = if (checkedTextView.isChecked) {
NotificationMode.DISABLED
} else {
NotificationMode.ENABLED
}
listener.onModeChange(bindingAdapterPosition, mode)
itemBinding.root.text = data.title
itemBinding.root.isChecked = data.notificationMode != NotificationMode.DISABLED
}
}
private class DiffCallback : DiffUtil.ItemCallback<SubscriptionItem>() {
private object DiffCallback : DiffUtil.ItemCallback<SubscriptionItem>() {
override fun areItemsTheSame(oldItem: SubscriptionItem, newItem: SubscriptionItem): Boolean {
return oldItem.id == newItem.id
}
@ -107,18 +66,27 @@ class NotificationModeConfigAdapter(
}
override fun getChangePayload(oldItem: SubscriptionItem, newItem: SubscriptionItem): Any? {
if (oldItem.notificationMode != newItem.notificationMode) {
return newItem.notificationMode
return if (oldItem.notificationMode != newItem.notificationMode) {
newItem.notificationMode
} else {
return super.getChangePayload(oldItem, newItem)
super.getChangePayload(oldItem, newItem)
}
}
}
interface ModeToggleListener {
fun interface ModeToggleListener {
/**
* Triggered when the UI representation of a notification mode is changed.
*/
fun onModeChange(position: Int, @NotificationMode mode: Int)
}
}
data class SubscriptionItem(
val id: Long,
val title: String,
@NotificationMode
val notificationMode: Int,
val serviceId: Int,
val url: String
)

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.settings.notifications
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -8,30 +9,36 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.databinding.FragmentChannelsNotificationsBinding
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.settings.notifications.NotificationModeConfigAdapter.ModeToggleListener
/**
* [NotificationModeConfigFragment] is a settings fragment
* which allows changing the [NotificationMode] of all subscribed channels.
* The [NotificationMode] can either be changed one by one or toggled for all channels.
*/
class NotificationModeConfigFragment : Fragment(), ModeToggleListener {
class NotificationModeConfigFragment : Fragment() {
private var _binding: FragmentChannelsNotificationsBinding? = null
private val binding get() = _binding!!
private lateinit var updaters: CompositeDisposable
private val disposables = CompositeDisposable()
private var loader: Disposable? = null
private var adapter: NotificationModeConfigAdapter? = null
private lateinit var adapter: NotificationModeConfigAdapter
private lateinit var subscriptionManager: SubscriptionManager
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
updaters = CompositeDisposable()
setHasOptionsMenu(true)
}
@ -39,28 +46,34 @@ class NotificationModeConfigFragment : Fragment(), ModeToggleListener {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = inflater.inflate(R.layout.fragment_channels_notifications, container, false)
): View {
_binding = FragmentChannelsNotificationsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView: RecyclerView = view.findViewById(R.id.recycler_view)
adapter = NotificationModeConfigAdapter(this)
recyclerView.adapter = adapter
adapter = NotificationModeConfigAdapter { position, mode ->
// Notification mode has been changed via the UI.
// Now change it in the database.
updateNotificationMode(adapter.currentList[position], mode)
}
binding.recyclerView.adapter = adapter
loader?.dispose()
loader = SubscriptionManager(requireContext())
.subscriptions()
loader = subscriptionManager.subscriptions()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newData -> adapter?.update(newData) }
.subscribe(adapter::update)
}
override fun onDestroyView() {
loader?.dispose()
loader = null
_binding = null
super.onDestroyView()
}
override fun onDestroy() {
updaters.dispose()
disposables.dispose()
super.onDestroy()
}
@ -79,41 +92,20 @@ class NotificationModeConfigFragment : Fragment(), ModeToggleListener {
}
}
override fun onModeChange(position: Int, @NotificationMode mode: Int) {
// Notification mode has been changed via the UI.
// Now change it in the database.
val subscription = adapter?.getItem(position) ?: return
updaters.add(
SubscriptionManager(requireContext())
.updateNotificationMode(
subscription.serviceId,
subscription.url,
mode
)
.subscribeOn(Schedulers.io())
.subscribe()
)
}
private fun toggleAll() {
val subscriptions = adapter?.getCurrentList() ?: return
val mode = subscriptions.firstOrNull()?.notificationMode ?: return
val mode = adapter.currentList.firstOrNull()?.notificationMode ?: return
val newMode = when (mode) {
NotificationMode.DISABLED -> NotificationMode.ENABLED
else -> NotificationMode.DISABLED
}
val subscriptionManager = SubscriptionManager(requireContext())
updaters.add(
CompositeDisposable(
subscriptions.map { item ->
subscriptionManager.updateNotificationMode(
serviceId = item.serviceId,
url = item.url,
mode = newMode
).subscribeOn(Schedulers.io())
.subscribe()
}
)
adapter.currentList.forEach { updateNotificationMode(it, newMode) }
}
private fun updateNotificationMode(item: SubscriptionItem, @NotificationMode mode: Int) {
disposables.add(
subscriptionManager.updateNotificationMode(item.serviceId, item.url, mode)
.subscribeOn(Schedulers.io())
.subscribe()
)
}
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.settings.preferencesearch;
import android.text.TextUtils;
import android.util.Pair;
import org.apache.commons.text.similarity.FuzzyScore;
@ -8,6 +9,7 @@ import java.util.Comparator;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PreferenceFuzzySearchFunction
@ -31,7 +33,7 @@ public class PreferenceFuzzySearchFunction
// Specific search - Used for determining order of search results
// Calculate a score based on specific search fields
.map(item -> new FuzzySearchSpecificDTO(item, keyword))
.sorted(Comparator.comparing(FuzzySearchSpecificDTO::getScore).reversed())
.sorted(Comparator.comparingDouble(FuzzySearchSpecificDTO::getScore).reversed())
.map(FuzzySearchSpecificDTO::getItem)
// Limit the amount of search results
.limit(20);
@ -72,39 +74,22 @@ public class PreferenceFuzzySearchFunction
);
private final PreferenceSearchItem item;
private final float score;
private final double score;
FuzzySearchSpecificDTO(
final PreferenceSearchItem item,
final String keyword) {
FuzzySearchSpecificDTO(final PreferenceSearchItem item, final String keyword) {
this.item = item;
float attributeScoreSum = 0;
int countOfAttributesWithScore = 0;
for (final Map.Entry<Function<PreferenceSearchItem, String>, Float> we
: WEIGHT_MAP.entrySet()) {
final String valueToProcess = we.getKey().apply(item);
if (valueToProcess.isEmpty()) {
continue;
}
attributeScoreSum +=
FUZZY_SCORE.fuzzyScore(valueToProcess, keyword) * we.getValue();
countOfAttributesWithScore++;
}
if (countOfAttributesWithScore != 0) {
this.score = attributeScoreSum / countOfAttributesWithScore;
} else {
this.score = 0;
}
this.score = WEIGHT_MAP.entrySet().stream()
.map(entry -> new Pair<>(entry.getKey().apply(item), entry.getValue()))
.filter(pair -> !pair.first.isEmpty())
.collect(Collectors.averagingDouble(pair ->
FUZZY_SCORE.fuzzyScore(pair.first, keyword) * pair.second));
}
public PreferenceSearchItem getItem() {
return item;
}
public float getScore() {
public double getScore() {
return score;
}
}

View file

@ -9,13 +9,13 @@ import androidx.annotation.Nullable;
import androidx.annotation.XmlRes;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.util.Localization;
import org.xmlpull.v1.XmlPullParser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Parses the corresponding preference-file(s).
@ -54,7 +54,7 @@ public class PreferenceParser {
if (xpp.getEventType() == XmlPullParser.START_TAG) {
final PreferenceSearchItem result = parseSearchResult(
xpp,
joinBreadcrumbs(breadcrumbs),
Localization.concatenateStrings(" > ", breadcrumbs),
resId
);
@ -82,12 +82,6 @@ public class PreferenceParser {
return results;
}
private String joinBreadcrumbs(final List<String> breadcrumbs) {
return breadcrumbs.stream()
.filter(crumb -> !TextUtils.isEmpty(crumb))
.collect(Collectors.joining(" > "));
}
private String getAttribute(
final XmlPullParser xpp,
@NonNull final String attribute

View file

@ -1,54 +1,48 @@
package org.schabi.newpipe.settings.preferencesearch;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.databinding.SettingsPreferencesearchListItemResultBinding;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
class PreferenceSearchAdapter
extends RecyclerView.Adapter<PreferenceSearchAdapter.PreferenceViewHolder> {
private List<PreferenceSearchItem> dataset = new ArrayList<>();
extends ListAdapter<PreferenceSearchItem, PreferenceSearchAdapter.PreferenceViewHolder> {
private Consumer<PreferenceSearchItem> onItemClickListener;
PreferenceSearchAdapter() {
super(new PreferenceCallback());
}
@NonNull
@Override
public PreferenceViewHolder onCreateViewHolder(
@NonNull final ViewGroup parent,
final int viewType
) {
return new PreferenceViewHolder(
SettingsPreferencesearchListItemResultBinding.inflate(
LayoutInflater.from(parent.getContext()),
parent,
false));
public PreferenceViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
return new PreferenceViewHolder(SettingsPreferencesearchListItemResultBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(
@NonNull final PreferenceViewHolder holder,
final int position
) {
final PreferenceSearchItem item = dataset.get(position);
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder, final int position) {
final PreferenceSearchItem item = getItem(position);
holder.binding.title.setText(item.getTitle());
if (TextUtils.isEmpty(item.getSummary())) {
if (item.getSummary().isEmpty()) {
holder.binding.summary.setVisibility(View.GONE);
} else {
holder.binding.summary.setVisibility(View.VISIBLE);
holder.binding.summary.setText(item.getSummary());
}
if (TextUtils.isEmpty(item.getBreadcrumbs())) {
if (item.getBreadcrumbs().isEmpty()) {
holder.binding.breadcrumbs.setVisibility(View.GONE);
} else {
holder.binding.breadcrumbs.setVisibility(View.VISIBLE);
@ -62,16 +56,6 @@ class PreferenceSearchAdapter
});
}
void setContent(final List<PreferenceSearchItem> items) {
dataset = new ArrayList<>(items);
this.notifyDataSetChanged();
}
@Override
public int getItemCount() {
return dataset.size();
}
void setOnItemClickListener(final Consumer<PreferenceSearchItem> onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
@ -84,4 +68,19 @@ class PreferenceSearchAdapter
this.binding = binding;
}
}
private static class PreferenceCallback extends DiffUtil.ItemCallback<PreferenceSearchItem> {
@Override
public boolean areItemsTheSame(@NonNull final PreferenceSearchItem oldItem,
@NonNull final PreferenceSearchItem newItem) {
return oldItem.getKey().equals(newItem.getKey());
}
@Override
public boolean areContentsTheSame(@NonNull final PreferenceSearchItem oldItem,
@NonNull final PreferenceSearchItem newItem) {
return oldItem.getAllRelevantSearchFields().equals(newItem
.getAllRelevantSearchFields());
}
}
}

View file

@ -3,8 +3,6 @@ package org.schabi.newpipe.settings.preferencesearch;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@ -12,9 +10,9 @@ import java.util.stream.Stream;
public class PreferenceSearchConfiguration {
private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction();
private final List<String> parserIgnoreElements = Collections.singletonList(
private final List<String> parserIgnoreElements = List.of(
PreferenceCategory.class.getSimpleName());
private final List<String> parserContainerElements = Arrays.asList(
private final List<String> parserContainerElements = List.of(
PreferenceCategory.class.getSimpleName(),
PreferenceScreen.class.getSimpleName());

View file

@ -1,7 +1,6 @@
package org.schabi.newpipe.settings.preferencesearch;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -13,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
import java.util.ArrayList;
import java.util.List;
/**
@ -54,13 +52,8 @@ public class PreferenceSearchFragment extends Fragment {
return;
}
final List<PreferenceSearchItem> results =
!TextUtils.isEmpty(keyword)
? searcher.searchFor(keyword)
: new ArrayList<>();
adapter.setContent(new ArrayList<>(results));
final List<PreferenceSearchItem> results = searcher.searchFor(keyword);
adapter.submitList(results);
setEmptyViewShown(results.isEmpty());
}

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.settings.preferencesearch;
import androidx.annotation.NonNull;
import androidx.annotation.XmlRes;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@ -92,11 +91,7 @@ public class PreferenceSearchItem {
}
public List<String> getAllRelevantSearchFields() {
return Arrays.asList(
getTitle(),
getSummary(),
getEntries(),
getBreadcrumbs());
return List.of(getTitle(), getSummary(), getEntries(), getBreadcrumbs());
}
@NonNull

View file

@ -6,7 +6,6 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@ -65,8 +64,7 @@ public final class PreferenceSearchResultHighlighter {
recyclerView.findViewHolderForAdapterPosition(position);
if (holder != null) {
final Drawable background = holder.itemView.getBackground();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& background instanceof RippleDrawable) {
if (background instanceof RippleDrawable) {
showRippleAnimation((RippleDrawable) background);
return;
}

View file

@ -3,6 +3,7 @@ package org.schabi.newpipe.settings.preferencesearch;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -21,7 +22,7 @@ public class PreferenceSearcher {
List<PreferenceSearchItem> searchFor(final String keyword) {
if (TextUtils.isEmpty(keyword)) {
return new ArrayList<>();
return Collections.emptyList();
}
return configuration.getSearcher()

View file

@ -1,5 +1,8 @@
package org.schabi.newpipe.settings.tabs;
import static org.schabi.newpipe.settings.tabs.Tab.typeFrom;
import static org.schabi.newpipe.util.ServiceHelper.getNameOfServiceById;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
@ -28,7 +31,6 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.settings.SelectChannelFragment;
import org.schabi.newpipe.settings.SelectKioskFragment;
import org.schabi.newpipe.settings.SelectPlaylistFragment;
@ -39,8 +41,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.settings.tabs.Tab.typeFrom;
public class ChooseTabsFragment extends Fragment {
private TabsManager tabsManager;
@ -374,36 +374,31 @@ public class ChooseTabsFragment extends Fragment {
return;
}
final String tabName;
tabNameView.setText(getTabName(type, tab));
tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
}
private String getTabName(@NonNull final Tab.Type type, @NonNull final Tab tab) {
switch (type) {
case BLANK:
tabName = getString(R.string.blank_page_summary);
break;
return getString(R.string.blank_page_summary);
case DEFAULT_KIOSK:
tabName = getString(R.string.default_kiosk_page_summary);
break;
return getString(R.string.default_kiosk_page_summary);
case KIOSK:
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab)
.getKioskServiceId()) + "/" + tab.getTabName(requireContext());
break;
return getNameOfServiceById(((Tab.KioskTab) tab).getKioskServiceId())
+ "/" + tab.getTabName(requireContext());
case CHANNEL:
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab)
.getChannelServiceId()) + "/" + tab.getTabName(requireContext());
break;
return getNameOfServiceById(((Tab.ChannelTab) tab).getChannelServiceId())
+ "/" + tab.getTabName(requireContext());
case PLAYLIST:
final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId();
final String serviceName = serviceId == -1
? getString(R.string.local)
: NewPipe.getNameOfService(serviceId);
tabName = serviceName + "/" + tab.getTabName(requireContext());
break;
: getNameOfServiceById(serviceId);
return serviceName + "/" + tab.getTabName(requireContext());
default:
tabName = tab.getTabName(requireContext());
break;
return tab.getTabName(requireContext());
}
tabNameView.setText(tabName);
tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
}
@SuppressLint("ClickableViewAccessibility")

View file

@ -248,7 +248,7 @@ public abstract class Tab {
@DrawableRes
@Override
public int getTabIconRes(final Context context) {
return R.drawable.ic_rss_feed;
return R.drawable.ic_subscriptions;
}
@Override

View file

@ -10,8 +10,6 @@ import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
@ -20,11 +18,11 @@ import java.util.List;
public final class TabsJsonHelper {
private static final String JSON_TABS_ARRAY_KEY = "tabs";
private static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(
Arrays.asList(
Tab.Type.DEFAULT_KIOSK.getTab(),
Tab.Type.SUBSCRIPTIONS.getTab(),
Tab.Type.BOOKMARKS.getTab()));
private static final List<Tab> FALLBACK_INITIAL_TABS_LIST = List.of(
Tab.Type.DEFAULT_KIOSK.getTab(),
Tab.Type.FEED.getTab(),
Tab.Type.SUBSCRIPTIONS.getTab(),
Tab.Type.BOOKMARKS.getTab());
private TabsJsonHelper() { }

View file

@ -73,10 +73,8 @@ public final class TabsManager {
private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() {
return (sp, key) -> {
if (key.equals(savedTabsKey)) {
if (savedTabsChangeListener != null) {
savedTabsChangeListener.onTabsChanged();
}
if (savedTabsKey.equals(key) && savedTabsChangeListener != null) {
savedTabsChangeListener.onTabsChanged();
}
};
}