Convert settings migration logic to kotlin

This commit is contained in:
Yevhen Babiichuk (DustDFG) 2026-01-09 02:24:24 +02:00
parent 2704c20fea
commit 6dc4ac305a
4 changed files with 408 additions and 419 deletions

View File

@ -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<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

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2020-2026 NewPipe contributors <https://newpipe.net>
* 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()
}
}

View File

@ -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.
* <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;
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<String> 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<Tab> tabs = tabsManager.getTabs();
final List<Tab> 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<Tab> tabs = tabsManager.getTabs();
final List<Tab> 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.
* <p>
* <b>Append new migrations to the end of the list</b> 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);
}
}

View File

@ -0,0 +1,310 @@
/*
* SPDX-FileCopyrightText: 2020-2026 NewPipe contributors <https://newpipe.net>
* 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.
* <br></br>
* 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<String>()
}
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>(
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
}
}
}