diff --git a/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java b/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java deleted file mode 100644 index d5b0e783d..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java +++ /dev/null @@ -1,103 +0,0 @@ -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> 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 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(); - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.kt b/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.kt new file mode 100644 index 000000000..0974957f8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.kt @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.migration + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import org.schabi.newpipe.R +import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar + +/** + * MigrationManager is responsible for running migrations and showing the user information about + * the migrations that were applied. + */ +object MigrationManager { + private val TAG: String = MigrationManager::class.java.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 val MIGRATION_INFO: MutableList<(Context) -> Unit> = ArrayList() + + /** + * 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 + */ + @JvmStatic + fun runMigrationsIfNeeded(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 + */ + @JvmStatic + fun showUserInfoIfPresent(context: Context) { + if (MIGRATION_INFO.isEmpty()) { + return + } + + try { + MIGRATION_INFO[0](context) + } catch (e: Exception) { + showUiErrorSnackbar(context, "Showing migration info to the user", e) + // Remove the migration that caused the error and continue with the next one + MIGRATION_INFO.removeAt(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 + */ + @JvmStatic + fun addMigrationInfo(info: (Context) -> Unit) { + 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 + */ + @JvmStatic + fun onMigrationInfoDismissed(context: Context) { + MIGRATION_INFO.removeAt(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 + */ + @JvmStatic + fun createMigrationInfoDialog(uiContext: Context, title: String, message: String): AlertDialog { + return AlertDialog.Builder(uiContext) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .setOnDismissListener { onMigrationInfoDismissed(uiContext) } + .setCancelable(false) // prevents the dialog from being dismissed accidentally + .create() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java deleted file mode 100644 index 67944075d..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java +++ /dev/null @@ -1,316 +0,0 @@ -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.core.util.Consumer; -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; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.settings.tabs.Tab; -import org.schabi.newpipe.settings.tabs.TabsManager; -import org.schabi.newpipe.util.DeviceUtils; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * 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. - *
- * In order to add a migration, follow these steps, given {@code P} is the previous version: - *
    - *
  • 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}
  • - *
  • add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}
  • - *
  • increment {@link SettingMigrations#VERSION}'s value by 1 - * (so it becomes {@code P+1})
  • - *
- * 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; - - private static final Migration MIGRATION_0_1 = new Migration(0, 1) { - @Override - 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 - // aware of the changed dialog. - final SharedPreferences.Editor editor = sp.edit(); - editor.putString(context.getString(R.string.preferred_open_action_key), - context.getString(R.string.always_ask_open_action_key)); - editor.apply(); - } - }; - - private static final Migration MIGRATION_1_2 = new Migration(1, 2) { - @Override - 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 - // background. - // Therefore, set default value to background, if it has not been changed yet. - final String minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key); - if (sp.getString(minimizeOnExitKey, "") - .equals(context.getString(R.string.minimize_on_exit_none_key))) { - final SharedPreferences.Editor editor = sp.edit(); - editor.putString(minimizeOnExitKey, - context.getString(R.string.minimize_on_exit_background_key)); - editor.apply(); - } - } - }; - - private static final Migration MIGRATION_2_3 = new Migration(2, 3) { - @Override - 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. 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), - !DeviceUtils.isFireTv() - ).apply(); - } - }; - - private static final Migration MIGRATION_3_4 = new Migration(3, 4) { - @Override - 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 - - final String showSearchSuggestionsKey = - context.getString(R.string.show_search_suggestions_key); - - boolean addAllSearchSuggestionTypes; - try { - addAllSearchSuggestionTypes = sp.getBoolean(showSearchSuggestionsKey, true); - } catch (final ClassCastException e) { - // just in case it was not a boolean for some reason, let's consider it a "true" - addAllSearchSuggestionTypes = true; - } - - final Set showSearchSuggestionsValueList = new HashSet<>(); - if (addAllSearchSuggestionTypes) { - // if the preference was true, all suggestions will be shown, otherwise none - Collections.addAll(showSearchSuggestionsValueList, context.getResources() - .getStringArray(R.array.show_search_suggestions_value_list)); - } - - sp.edit().putStringSet( - showSearchSuggestionsKey, showSearchSuggestionsValueList).apply(); - } - }; - - 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(); - } - }; - - private 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(); - } - }; - - private static final Migration MIGRATION_6_7 = new Migration(6, 7) { - @Override - protected void migrate(@NonNull final Context context) { - // The SoundCloud Top 50 Kiosk was removed in the extractor, - // so we remove the corresponding tab if it exists. - final TabsManager tabsManager = TabsManager.getManager(context); - final List tabs = tabsManager.getTabs(); - final List cleanedTabs = tabs.stream() - .filter(tab -> !(tab instanceof Tab.KioskTab kioskTab - && kioskTab.getKioskServiceId() == SoundCloud.getServiceId() - && kioskTab.getKioskId().equals("Top 50"))) - .collect(Collectors.toUnmodifiableList()); - if (tabs.size() != cleanedTabs.size()) { - tabsManager.saveTabs(cleanedTabs); - // create an AlertDialog to inform the user about the change - 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 tabs = tabsManager.getTabs(); - final List cleanedTabs = tabs.stream() - .filter(tab -> !(tab instanceof Tab.KioskTab kioskTab - && kioskTab.getKioskServiceId() == YouTube.getServiceId() - && kioskTab.getKioskId().equals("Trending"))) - .collect(Collectors.toUnmodifiableList()); - if (tabs.size() != cleanedTabs.size()) { - tabsManager.saveTabs(cleanedTabs); - } - - final boolean hasDefaultTrendingTab = tabs.stream() - .anyMatch(tab -> tab instanceof Tab.DefaultKioskTab); - - if (tabs.size() != cleanedTabs.size() || hasDefaultTrendingTab) { - // 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()); - } - } - }; - - /** - * List of all implemented migrations. - *

- * Append new migrations to the end of the list to keep it sorted ascending. - * If not sorted correctly, migrations which depend on each other, may fail. - */ - private static final Migration[] SETTING_MIGRATIONS = { - MIGRATION_0_1, - MIGRATION_1_2, - MIGRATION_2_3, - MIGRATION_3_4, - MIGRATION_4_5, - MIGRATION_5_6, - MIGRATION_6_7, - MIGRATION_7_8, - }; - - /** - * Version number for preferences. Must be incremented every time a migration is necessary. - */ - private static final int VERSION = 8; - - - 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 (App.getApp().isFirstRun()) { - sp.edit().putInt(lastPrefVersionKey, VERSION).apply(); - return; - } else if (lastPrefVersion == VERSION) { - return; - } - - // run migrations - int currentVersion = lastPrefVersion; - for (final Migration currentMigration : SETTING_MIGRATIONS) { - try { - if (currentMigration.shouldMigrate(currentVersion)) { - if (DEBUG) { - Log.d(TAG, "Migrating preferences from version " - + currentVersion + " to " + currentMigration.newVersion); - } - currentMigration.migrate(context); - currentVersion = currentMigration.newVersion; - } - } catch (final Exception e) { - // save the version with the last successful migration and report the error - sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); - ErrorUtil.openActivity(context, new ErrorInfo( - e, - UserAction.PREFERENCES_MIGRATION, - "Migrating preferences from version " + lastPrefVersion + " to " - + VERSION + ". " - + "Error at " + currentVersion + " => " + ++currentVersion - )); - return; - } - } - - // store the current preferences version - sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); - } - - private SettingMigrations() { } - - abstract static class Migration { - public final int oldVersion; - public final int newVersion; - - protected Migration(final int oldVersion, final int newVersion) { - this.oldVersion = oldVersion; - this.newVersion = newVersion; - } - - /** - * @param currentVersion current settings version - * @return Returns whether this migration should be run. - * A migration is necessary if the old version of this migration is lower than or equal to - * the current settings version. - */ - private boolean shouldMigrate(final int currentVersion) { - return oldVersion >= currentVersion; - } - - protected abstract void migrate(@NonNull Context context); - - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.kt b/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.kt new file mode 100644 index 000000000..aca05484c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.kt @@ -0,0 +1,310 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.migration + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import org.schabi.newpipe.App +import org.schabi.newpipe.MainActivity +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.ServiceList +import org.schabi.newpipe.settings.migration.MigrationManager.addMigrationInfo +import org.schabi.newpipe.settings.tabs.Tab.DefaultKioskTab +import org.schabi.newpipe.settings.tabs.Tab.KioskTab +import org.schabi.newpipe.settings.tabs.TabsManager +import org.schabi.newpipe.util.DeviceUtils + +/** + * 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. + *

+ * In order to add a migration, follow these steps, given `P` is the previous version: + * + * * in the class body add a new `MIGRATION_P_P+1 = new Migration(P, P+1) { ... }` and put + * in the `migrate()` method the code that need to be run + * when migrating from `P` to `P+1` + * * add `MIGRATION_P_P+1` at the end of [SettingMigrations.SETTING_MIGRATIONS] + * * increment [SettingMigrations.VERSION]'s value by 1 + * (so it becomes `P+1`) + * + * Migrations can register UI actions using [MigrationManager.addMigrationInfo] + * that will be performed after the UI is initialized to inform the user about changes + * that were applied by migrations. + */ +object SettingMigrations { + private val TAG = SettingMigrations::class.java.toString() + private lateinit var sp: SharedPreferences + + private val MIGRATION_0_1: Migration = Migration(0, 1) { 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 + // aware of the changed dialog. + sp.edit { + putString( + context.getString(R.string.preferred_open_action_key), + context.getString(R.string.always_ask_open_action_key) + ) + } + } + + private val MIGRATION_1_2: Migration = Migration(1, 2) { 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 + // background. + // Therefore, set default value to background, if it has not been changed yet. + val minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key) + if (sp.getString(minimizeOnExitKey, "") + == context.getString(R.string.minimize_on_exit_none_key) + ) { + sp.edit { + putString( + minimizeOnExitKey, + context.getString(R.string.minimize_on_exit_background_key) + ) + } + } + } + + private val MIGRATION_2_3: Migration = Migration(2, 3) { 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. 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), + !DeviceUtils.isFireTv() + ) + } + } + + private val MIGRATION_3_4: Migration = Migration(3, 4) { 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 + + val showSearchSuggestionsKey = + context.getString(R.string.show_search_suggestions_key) + var addAllSearchSuggestionTypes: Boolean = try { + sp.getBoolean(showSearchSuggestionsKey, true) + } catch (e: ClassCastException) { + // just in case it was not a boolean for some reason, let's consider it a "true" + true + } + + var showSearchSuggestionsValueList = if (addAllSearchSuggestionTypes) { + // if the preference was true, all suggestions will be shown, otherwise none + hashSetOf( + *context.resources + .getStringArray(R.array.show_search_suggestions_value_list) + ) + } else { + HashSet() + } + + sp.edit { + putStringSet(showSearchSuggestionsKey, showSearchSuggestionsValueList) + } + } + + private val MIGRATION_4_5: Migration = Migration(4, 5) { context -> + val brightness = sp.getBoolean("brightness_gesture_control", true) + val volume = sp.getBoolean("volume_gesture_control", true) + + sp.edit { + putString( + context.getString(R.string.right_gesture_control_key), + context.getString( + if (volume) { + R.string.volume_control_key + } else { + R.string.none_control_key + } + ) + ) + putString( + context.getString(R.string.left_gesture_control_key), + context.getString( + if (brightness) { + R.string.brightness_control_key + } else { + R.string.none_control_key + } + ) + ) + } + } + + private val MIGRATION_5_6: Migration = Migration(5, 6) { context -> + val loadImages = sp.getBoolean("download_thumbnail_key", true) + + sp.edit { + putString( + context.getString(R.string.image_quality_key), + context.getString( + if (loadImages) { + R.string.image_quality_default + } else { + R.string.image_quality_none_key + } + ) + ) + } + } + + private val MIGRATION_6_7: Migration = Migration(6, 7) { context -> + // The SoundCloud Top 50 Kiosk was removed in the extractor, + // so we remove the corresponding tab if it exists. + val tabsManager = TabsManager.getManager(context) + val tabs = tabsManager.getTabs() + val cleanedTabs = tabs.filterNot { + it is KioskTab && + it.kioskServiceId == ServiceList.SoundCloud.serviceId && + it.kioskId == "Top 50" + } + if (tabs.size != cleanedTabs.size) { + tabsManager.saveTabs(cleanedTabs) + // create an AlertDialog to inform the user about the change + 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 val MIGRATION_7_8: Migration = Migration(7, 8) { 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. + val tabsManager = TabsManager.getManager(context) + val tabs = tabsManager.getTabs() + val cleanedTabs = tabs.filterNot { + it is KioskTab && + it.kioskServiceId == ServiceList.YouTube.serviceId && + it.kioskId == "Trending" + } + if (tabs.size != cleanedTabs.size) { + tabsManager.saveTabs(cleanedTabs) + } + + val hasDefaultTrendingTab = tabs.any { it is DefaultKioskTab } + + if (tabs.size != cleanedTabs.size || hasDefaultTrendingTab) { + // User is informed about the change + addMigrationInfo { uiContext -> + MigrationManager.createMigrationInfoDialog( + uiContext, + uiContext.getString(R.string.migration_info_7_8_title), + uiContext.getString(R.string.migration_info_7_8_message) + ).show() + } + } + } + + /** + * List of all implemented migrations. + * + * + * **Append new migrations to the end of the list** to keep it sorted ascending. + * If not sorted correctly, migrations which depend on each other, may fail. + */ + private val SETTING_MIGRATIONS = arrayOf( + MIGRATION_0_1, + MIGRATION_1_2, + MIGRATION_2_3, + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + MIGRATION_7_8, + ) + + /** + * Version number for preferences. Must be incremented every time a migration is necessary. + */ + private const val VERSION = 8 + + fun runMigrationsIfNeeded(context: Context) { + // setup migrations and check if there is something to do + sp = PreferenceManager.getDefaultSharedPreferences(context) + val lastPrefVersionKey = context.getString(R.string.last_used_preferences_version) + val lastPrefVersion = sp.getInt(lastPrefVersionKey, 0) + + // no migration to run, already up to date + if (App.getApp().isFirstRun) { + sp.edit { putInt(lastPrefVersionKey, VERSION) } + return + } else if (lastPrefVersion == VERSION) { + return + } + + // run migrations + var currentVersion = lastPrefVersion + for (currentMigration in SETTING_MIGRATIONS) { + try { + if (currentMigration.shouldMigrate(currentVersion)) { + if (MainActivity.DEBUG) { + Log.d( + TAG, + "Migrating preferences from version $currentVersion" + + " to $currentMigration.newVersion" + ) + } + currentMigration.migrate(context) + currentVersion = currentMigration.newVersion + } + } catch (e: Exception) { + // save the version with the last successful migration and report the error + sp.edit { putInt(lastPrefVersionKey, currentVersion) } + ErrorUtil.openActivity( + context, + ErrorInfo( + e, + UserAction.PREFERENCES_MIGRATION, + "Migrating preferences from version $lastPrefVersion to " + + "$VERSION. Error at $currentVersion => ${++currentVersion}" + ) + ) + return + } + } + + // store the current preferences version + sp.edit { putInt(lastPrefVersionKey, currentVersion) } + } + + internal class Migration( + val oldVersion: Int, + val newVersion: Int, + val migrate: (Context) -> Unit + ) { + /** + * @param currentVersion current settings version + * @return Returns whether this migration should be run. + * A migration is necessary if the old version of this migration is lower than or equal to + * the current settings version. + */ + internal fun shouldMigrate(currentVersion: Int): Boolean { + return oldVersion >= currentVersion + } + } +}