2
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-08-22 10:09:39 +00:00

WIP: Add SettingsMigration to change YouTube trending kiosk tab

This commit is contained in:
TobiGr 2025-07-22 00:01:40 +02:00 committed by Stypox
parent 86efde5996
commit ed93603815
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
5 changed files with 192 additions and 48 deletions

View File

@ -78,8 +78,8 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.SettingMigrations;
import org.schabi.newpipe.settings.UpdateSettingsFragment;
import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
@ -195,7 +195,7 @@ public class MainActivity extends AppCompatActivity {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
SettingMigrations.showUserInfoIfPresent(this);
MigrationManager.showUserInfoIfPresent(this);
}
@Override

View File

@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
@ -46,7 +47,7 @@ public final class NewPipeSettings {
public static void initSettings(final Context context) {
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.runMigrationsIfNeeded(context);
MigrationManager.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);

View File

@ -0,0 +1,103 @@
package org.schabi.newpipe.settings.migration;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import java.util.ArrayList;
import java.util.List;
/**
* MigrationManager is responsible for running migrations and showing the user information about
* the migrations that were applied.
*/
public final class MigrationManager {
private static final String TAG = MigrationManager.class.getSimpleName();
/**
* List of UI actions that are performed after the UI is initialized (e.g. showing alert
* dialogs) to inform the user about changes that were applied by migrations.
*/
private static final List<Consumer<Context>> MIGRATION_INFO = new ArrayList<>();
private MigrationManager() {
// MigrationManager is a utility class that is completely static
}
/**
* Run all migrations that are needed for the current version of NewPipe.
* This method should be called at the start of the application, before any other operations
* that depend on the settings.
*
* @param context Context that can be used to run migrations
*/
public static void runMigrationsIfNeeded(@NonNull final Context context) {
SettingMigrations.runMigrationsIfNeeded(context);
}
/**
* Perform UI actions informing about migrations that took place if they are present.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void showUserInfoIfPresent(@NonNull final Context context) {
if (MIGRATION_INFO.isEmpty()) {
return;
}
try {
MIGRATION_INFO.get(0).accept(context);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(context, "Showing migration info to the user", e);
// Remove the migration that caused the error and continue with the next one
MIGRATION_INFO.remove(0);
showUserInfoIfPresent(context);
}
}
/**
* Add a migration info action that will be executed after the UI is initialized.
* This can be used to show dialogs/snackbars/toasts to inform the user about changes that
* were applied by migrations.
*
* @param info the action to be executed
*/
public static void addMigrationInfo(final Consumer<Context> info) {
MIGRATION_INFO.add(info);
}
/**
* This method should be called when the user dismisses the migration info
* to check if there are any more migration info actions to be shown.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void onMigrationInfoDismissed(@NonNull final Context context) {
MIGRATION_INFO.remove(0);
showUserInfoIfPresent(context);
}
/**
* Creates a dialog to inform the user about the migration.
* @param uiContext Context that can be used to show dialogs/snackbars/toasts
* @param title the title of the dialog
* @param message the message of the dialog
* @return the dialog that can be shown to the user with a custom dismiss listener
*/
static AlertDialog createMigrationInfoDialog(@NonNull final Context uiContext,
@NonNull final String title,
@NonNull final String message) {
return new AlertDialog.Builder(uiContext)
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dialog ->
MigrationManager.onMigrationInfoDismissed(uiContext))
.setCancelable(false) // prevents the dialog from being dismissed accidentally
.create();
}
}

View File

@ -1,11 +1,14 @@
package org.schabi.newpipe.settings;
package org.schabi.newpipe.settings.migration;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import androidx.preference.PreferenceManager;
@ -25,27 +28,28 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
/**
* In order to add a migration, follow these steps, given P is the previous version:<br>
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
* the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
* This class contains the code to migrate the settings from one version to another.
* Migrations are run automatically when the app is started and the settings version changed.
* <br>
* In order to add a migration, follow these steps, given {@code P} is the previous version:
* <ul>
* <li>in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put
* in the {@code migrate()} method the code that need to be run
* when migrating from {@code P} to {@code P+1}</li>
* <li>add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}</li>
* <li>increment {@link SettingMigrations#VERSION}'s value by 1
* (so it becomes {@code P+1})</li>
* </ul>
* Migrations can register UI actions using {@link MigrationManager#addMigrationInfo(Consumer)}
* that will be performed after the UI is initialized to inform the user about changes
* that were applied by migrations.
*/
public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
private static SharedPreferences sp;
/**
* List of UI actions that are performed after the UI is initialized (e.g. showing alert
* dialogs) to inform the user about changes that were applied by migrations.
*/
private static final List<Consumer<Context>> MIGRATION_INFO = new ArrayList<>();
private static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(@NonNull final Context context) {
@ -169,16 +173,63 @@ public final class SettingMigrations {
&& kioskTab.getKioskServiceId() == SoundCloud.getServiceId()
&& kioskTab.getKioskId().equals("Top 50")))
.collect(Collectors.toUnmodifiableList());
if (tabs.size() != cleanedTabs.size()) {
if (tabs.size() != cleanedTabs.size() || DEBUG) { // TODO: remove debug condition
tabsManager.saveTabs(cleanedTabs);
// create an AlertDialog to inform the user about the change
MIGRATION_INFO.add((Context uiContext) -> new AlertDialog.Builder(uiContext)
.setTitle(R.string.migration_info_6_7_title)
.setMessage(R.string.migration_info_6_7_message)
.setPositiveButton(R.string.ok, null)
.setCancelable(false)
.create()
.show());
MigrationManager.addMigrationInfo(uiContext ->
MigrationManager.createMigrationInfoDialog(
uiContext,
uiContext.getString(R.string.migration_info_6_7_title),
uiContext.getString(R.string.migration_info_6_7_message))
.show());
}
}
};
private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
@Override
protected void migrate(@NonNull final Context context) {
// YouTube remove the combined Trending kiosk, see
// https://github.com/TeamNewPipe/NewPipe/discussions/12445 for more information.
// If the user has a dedicated YouTube/Trending kiosk tab,
// it is removed and replaced with the new live kiosk tab.
// The default trending kiosk tab is not touched
// because it uses the default kiosk provided by the extractor
// and is thus updated automatically.
final TabsManager tabsManager = TabsManager.getManager(context);
final List<Tab> tabs = tabsManager.getTabs();
final boolean hadYtTrendingTab = tabs.stream()
.anyMatch(tab -> !(tab instanceof Tab.KioskTab kioskTab
&& kioskTab.getKioskServiceId() == YouTube.getServiceId()
&& kioskTab.getKioskId().equals("Trending")));
if (hadYtTrendingTab) {
final List<Tab> cleanedTabs = new ArrayList<>();
for (final Tab tab : tabs) {
if (tab instanceof Tab.KioskTab kioskTab
&& kioskTab.getKioskServiceId() == YouTube.getServiceId()
&& kioskTab.getKioskId().equals("Trending")) {
// replace the YouTube Trending tab with the new live kiosk tab
// TODO: use the correct kiosk ID for the live kiosk tab
cleanedTabs.add(new Tab.KioskTab(YouTube.getServiceId(), "Live"));
} else {
cleanedTabs.add(tab);
}
}
tabsManager.saveTabs(cleanedTabs);
}
final boolean hasDefaultTrendingTab = tabs.stream()
.anyMatch(tab -> tab instanceof Tab.DefaultKioskTab);
// TODO: remove debugging code
if (hadYtTrendingTab || hasDefaultTrendingTab || newVersion == VERSION) {
// User is informed about the change
MigrationManager.addMigrationInfo(uiContext ->
MigrationManager.createMigrationInfoDialog(
uiContext,
uiContext.getString(R.string.migration_info_7_8_title),
uiContext.getString(R.string.migration_info_7_8_message))
.show());
}
}
};
@ -196,26 +247,28 @@ public final class SettingMigrations {
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7
MIGRATION_6_7,
MIGRATION_7_8,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
private static final int VERSION = 7;
private static final int VERSION = 8;
public static void runMigrationsIfNeeded(@NonNull final Context context) {
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);
//final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
final int lastPrefVersion = 6; // TODO: remove this line after testing
// no migration to run, already up to date
if (App.getApp().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {
} else if (lastPrefVersion == VERSION && !DEBUG) { // TODO: remove DEBUG check
return;
}
@ -249,21 +302,6 @@ public final class SettingMigrations {
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
}
/**
* Perform UI actions informing about migrations that took place if they are present.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void showUserInfoIfPresent(@NonNull final Context context) {
for (final Consumer<Context> consumer : MIGRATION_INFO) {
try {
consumer.accept(context);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(context, "Showing migration info to the user", e);
}
}
MIGRATION_INFO.clear();
}
private SettingMigrations() { }
abstract static class Migration {
@ -282,7 +320,7 @@ public final class SettingMigrations {
* the current settings version.
*/
private boolean shouldMigrate(final int currentVersion) {
return oldVersion >= currentVersion;
return oldVersion >= currentVersion || newVersion == VERSION;
}
protected abstract void migrate(@NonNull Context context);

View File

@ -866,4 +866,6 @@
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
<string name="migration_info_6_7_title">SoundCloud Top 50 page removed</string>
<string name="migration_info_6_7_message">SoundCloud has discontinued the original Top 50 charts. The corresponding tab has been removed from your main page.</string>
<string name="migration_info_7_8_title">YouTube combined trending removed</string>
<string name="migration_info_7_8_message">YouTube has discontinued the combined trending page as of 21st July 2025. NewPipe replaced the default trending page with the trending livestreams.\n\nYou can also select different ones in the content settings.</string>
</resources>