Merge branch 'dev' into pr8221
This commit is contained in:
commit
e1ce3fef1b
1741 changed files with 56947 additions and 21110 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() { }
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue