diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 681cc4812..ee5578235 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -215,10 +215,6 @@ android:name=".data.updater.AppUpdateService" android:exported="false" /> - - diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt index 5bf89688f..c2cddabaf 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt @@ -41,9 +41,9 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupConst -import eu.kanade.tachiyomi.data.backup.BackupCreatorJob +import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator -import eu.kanade.tachiyomi.data.backup.BackupRestoreService +import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil @@ -93,7 +93,7 @@ object SettingsBackupScreen : SearchableSettings { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION, ) - BackupCreatorJob.startNow(context, it, flag) + BackupCreateJob.startNow(context, it, flag) } flag = 0 } @@ -119,7 +119,7 @@ object SettingsBackupScreen : SearchableSettings { subtitle = stringResource(R.string.pref_create_backup_summ), onClick = { scope.launch { - if (!BackupCreatorJob.isManualJobRunning(context)) { + if (!BackupCreateJob.isManualJobRunning(context)) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) } @@ -275,7 +275,7 @@ object SettingsBackupScreen : SearchableSettings { confirmButton = { TextButton( onClick = { - BackupRestoreService.start(context, err.uri) + BackupRestoreJob.start(context, err.uri) onDismissRequest() }, ) { @@ -305,7 +305,7 @@ object SettingsBackupScreen : SearchableSettings { } if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) { - BackupRestoreService.start(context, it) + BackupRestoreJob.start(context, it) return@rememberLauncherForActivityResult } @@ -317,7 +317,7 @@ object SettingsBackupScreen : SearchableSettings { title = stringResource(R.string.pref_restore_backup), subtitle = stringResource(R.string.pref_restore_backup_summ), onClick = { - if (!BackupRestoreService.isRunning(context)) { + if (!BackupRestoreJob.isRunning(context)) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) } @@ -368,7 +368,7 @@ object SettingsBackupScreen : SearchableSettings { 168 to stringResource(R.string.update_weekly), ), onValueChanged = { - BackupCreatorJob.setupTask(context, it) + BackupCreateJob.setupTask(context, it) true }, ), diff --git a/app/src/main/java/eu/kanade/presentation/util/Resources.kt b/app/src/main/java/eu/kanade/presentation/util/Resources.kt index 44d9968b9..5b7bc9948 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Resources.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Resources.kt @@ -11,11 +11,11 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap /** - * Create a BitmapPainter from an drawable resource. - * - * > Only use this if [androidx.compose.ui.res.painterResource] doesn't work. + * Create a BitmapPainter from a drawable resource. + * Use this only if [androidx.compose.ui.res.painterResource] doesn't work. * * @param id the resource identifier + * * @return the bitmap associated with the resource */ @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 86a41e0dc..90304c282 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -8,7 +8,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.data.backup.BackupCreatorJob +import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.track.TrackManager @@ -59,7 +59,7 @@ object Migrations { // Always set up background tasks to ensure they're running LibraryUpdateJob.setupTask(context) - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) // Fresh install if (oldVersion == 0) { @@ -101,7 +101,7 @@ object Migrations { if (oldVersion < 43) { // Restore jobs after migrating from Evernote's job scheduler to WorkManager. LibraryUpdateJob.setupTask(context) - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) } if (oldVersion < 44) { // Reset sorting preference if using removed sort by source @@ -252,7 +252,7 @@ object Migrations { } } if (oldVersion < 76) { - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) } if (oldVersion < 77) { val oldReaderTap = prefs.getBoolean("reader_tap", false) @@ -287,7 +287,7 @@ object Migrations { } if (backupPreferences.backupInterval().get() == 0) { backupPreferences.backupInterval().set(12) - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) } } if (oldVersion < 85) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index dfbff66b6..d3495c578 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -23,7 +23,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit -class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : +class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { private val notifier = BackupNotifier(context) @@ -41,7 +41,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } } - context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build()) return try { val location = BackupManager(context).createBackup(uri, flags, isAutoBackup) if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) @@ -70,7 +69,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet val interval = prefInterval ?: backupPreferences.backupInterval().get() val workManager = WorkManager.getInstance(context) if (interval > 0) { - val request = PeriodicWorkRequestBuilder( + val request = PeriodicWorkRequestBuilder( interval.toLong(), TimeUnit.HOURS, 10, @@ -92,7 +91,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet LOCATION_URI_KEY to uri.toString(), BACKUP_FLAGS_KEY to flags, ) - val request = OneTimeWorkRequestBuilder() + val request = OneTimeWorkRequestBuilder() .addTag(TAG_MANUAL) .setInputData(inputData) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index 613f66bf2..c38fb2f2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -67,9 +67,7 @@ class BackupNotifier(private val context: Context) { setContentTitle(context.getString(R.string.backup_created)) setContentText(unifile.filePath ?: unifile.name) - // Clear old actions if they exist clearActions() - addAction( R.drawable.ic_share_24dp, context.getString(R.string.action_share), @@ -91,12 +89,10 @@ class BackupNotifier(private val context: Context) { setProgress(maxAmount, progress, false) setOnlyAlertOnce(true) - // Clear old actions if they exist clearActions() - addAction( R.drawable.ic_close_24dp, - context.getString(R.string.action_stop), + context.getString(R.string.action_cancel), NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), ) } @@ -132,9 +128,7 @@ class BackupNotifier(private val context: Context) { setContentTitle(context.getString(R.string.restore_completed)) setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount)) - // Clear old actions if they exist clearActions() - if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) { val destFile = File(path, file) val uri = destFile.getUriCompat(context) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt new file mode 100644 index 000000000..4ec24679c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt @@ -0,0 +1,87 @@ +package eu.kanade.tachiyomi.data.backup + +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.notificationManager +import kotlinx.coroutines.CancellationException +import logcat.LogPriority +import tachiyomi.core.util.system.logcat + +class BackupRestoreJob(private val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + private val notifier = BackupNotifier(context) + + override suspend fun doWork(): Result { + val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() + ?: return Result.failure() + + try { + setForeground(getForegroundInfo()) + } catch (e: IllegalStateException) { + logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } + } + + return try { + val restorer = BackupRestorer(context, notifier) + restorer.restoreBackup(uri) + Result.success() + } catch (e: Exception) { + if (e is CancellationException) { + notifier.showRestoreError(context.getString(R.string.restoring_backup_canceled)) + Result.success() + } else { + logcat(LogPriority.ERROR, e) + notifier.showRestoreError(e.message) + Result.failure() + } + } finally { + context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS) + } + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return ForegroundInfo( + Notifications.ID_RESTORE_PROGRESS, + notifier.showRestoreProgress().build(), + ) + } + + companion object { + fun isRunning(context: Context): Boolean { + val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG).get() + return list.find { it.state == WorkInfo.State.RUNNING } != null + } + + fun start(context: Context, uri: Uri) { + val inputData = workDataOf( + LOCATION_URI_KEY to uri.toString(), + ) + val request = OneTimeWorkRequestBuilder() + .addTag(TAG) + .setInputData(inputData) + .build() + WorkManager.getInstance(context) + .enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) + } + + fun stop(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(TAG) + } + } +} + +private const val TAG = "BackupRestore" + +private const val LOCATION_URI_KEY = "location_uri" // String diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt deleted file mode 100644 index 28b796586..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ /dev/null @@ -1,141 +0,0 @@ -package eu.kanade.tachiyomi.data.backup - -import android.app.Service -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.IBinder -import android.os.PowerManager -import androidx.core.content.ContextCompat -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.util.system.acquireWakeLock -import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat -import eu.kanade.tachiyomi.util.system.isServiceRunning -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import logcat.LogPriority -import tachiyomi.core.util.system.logcat - -/** - * Restores backup. - */ -class BackupRestoreService : Service() { - - companion object { - - /** - * Returns the status of the service. - * - * @param context the application context. - * @return true if the service is running, false otherwise. - */ - fun isRunning(context: Context): Boolean = - context.isServiceRunning(BackupRestoreService::class.java) - - /** - * Starts a service to restore a backup from Json - * - * @param context context of application - * @param uri path of Uri - */ - fun start(context: Context, uri: Uri) { - if (isRunning(context)) return - - val intent = Intent(context, BackupRestoreService::class.java).apply { - putExtra(BackupConst.EXTRA_URI, uri) - } - ContextCompat.startForegroundService(context, intent) - } - - /** - * Stops the service. - * - * @param context the application context. - */ - fun stop(context: Context) { - context.stopService(Intent(context, BackupRestoreService::class.java)) - - BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled)) - } - } - - /** - * Wake lock that will be held until the service is destroyed. - */ - private lateinit var wakeLock: PowerManager.WakeLock - - private lateinit var scope: CoroutineScope - private var restorer: BackupRestorer? = null - private lateinit var notifier: BackupNotifier - - override fun onCreate() { - scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - notifier = BackupNotifier(this) - wakeLock = acquireWakeLock(javaClass.name) - - startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build()) - } - - override fun stopService(name: Intent?): Boolean { - destroyJob() - return super.stopService(name) - } - - override fun onDestroy() { - destroyJob() - } - - private fun destroyJob() { - restorer?.job?.cancel() - scope.cancel() - if (wakeLock.isHeld) { - wakeLock.release() - } - } - - /** - * This method needs to be implemented, but it's not used/needed. - */ - override fun onBind(intent: Intent): IBinder? = null - - /** - * Method called when the service receives an intent. - * - * @param intent the start intent from. - * @param flags the flags of the command. - * @param startId the start id of this command. - * @return the start value of the command. - */ - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val uri = intent?.getParcelableExtraCompat(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY - - // Cancel any previous job if needed. - restorer?.job?.cancel() - - restorer = BackupRestorer(this, notifier) - - val handler = CoroutineExceptionHandler { _, exception -> - logcat(LogPriority.ERROR, exception) - restorer?.writeErrorLog() - - notifier.showRestoreError(exception.message) - stopSelf(startId) - } - val job = scope.launch(handler) { - if (restorer?.restoreBackup(uri) == false) { - notifier.showRestoreError(getString(R.string.restoring_backup_canceled)) - } - } - job.invokeOnCompletion { - stopSelf(startId) - } - restorer?.job = job - - return START_NOT_STICKY - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index deea3bf60..4a0df005c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -14,7 +14,8 @@ import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import exh.EXHMigrations import exh.source.MERGED_SOURCE_ID -import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.CustomMangaInfo import tachiyomi.domain.manga.model.Manga @@ -29,8 +30,6 @@ class BackupRestorer( private val notifier: BackupNotifier, ) { - var job: Job? = null - private var backupManager = BackupManager(context) private var restoreAmount = 0 @@ -61,7 +60,7 @@ class BackupRestorer( return true } - fun writeErrorLog(): File { + private fun writeErrorLog(): File { try { if (errors.isNotEmpty()) { val file = context.createFileInCacheDir("tachiyomi_restore.txt") @@ -101,18 +100,19 @@ class BackupRestorer( val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources sourceMapping = backupMaps.associate { it.sourceId to it.name } - // Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids - backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach { - if (job?.isActive != true) { - return false + return coroutineScope { + // Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids + backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach { + if (!isActive) { + return@coroutineScope false + } + + restoreManga(it, backup.backupCategories) } - restoreManga(it, backup.backupCategories) + // TODO: optionally trigger online library + tracker update + true } - - // TODO: optionally trigger online library + tracker update - - return true } private suspend fun restoreCategories(backupCategories: List) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 5ad86ad50..2428da302 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -6,10 +6,9 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build -import androidx.core.content.ContextCompat import androidx.core.net.toUri import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupRestoreService +import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.updater.AppUpdateService @@ -82,10 +81,7 @@ class NotificationReceiver : BroadcastReceiver() { "application/x-protobuf+gzip", intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1), ) - ACTION_CANCEL_RESTORE -> cancelRestore( - context, - intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1), - ) + ACTION_CANCEL_RESTORE -> cancelRestore(context) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) // Cancel downloading app update @@ -206,11 +202,9 @@ class NotificationReceiver : BroadcastReceiver() { * Method called when user wants to stop a backup restore job. * * @param context context of application - * @param notificationId id of notification */ - private fun cancelRestore(context: Context, notificationId: Int) { - BackupRestoreService.stop(context) - ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) } + private fun cancelRestore(context: Context) { + BackupRestoreJob.stop(context) } /** diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index ab8186102..25a901a4e 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -12,7 +12,7 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.data.backup.BackupCreatorJob +import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.track.TrackManager @@ -111,7 +111,7 @@ object EXHMigrations { lastVersionCode.set(BuildConfig.VERSION_CODE) LibraryUpdateJob.setupTask(context) - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) EHentaiUpdateWorker.scheduleBackground(context) // Fresh install @@ -374,7 +374,7 @@ object EXHMigrations { } } if (oldVersion under 30) { - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) } if (oldVersion under 31) { runBlocking { @@ -460,7 +460,7 @@ object EXHMigrations { } if (backupPreferences.backupInterval().get() == 0) { backupPreferences.backupInterval().set(12) - BackupCreatorJob.setupTask(context) + BackupCreateJob.setupTask(context) } } if (oldVersion under 41) { diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 5b16cf228..f4a4d0935 100755 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -86,7 +86,6 @@ Delete category Edit cover View chapters - Stop Pause Previous chapter Next chapter diff --git a/i18n/src/main/res/values/strings_sy.xml b/i18n/src/main/res/values/strings_sy.xml index 2acbc3654..e7bde3226 100644 --- a/i18n/src/main/res/values/strings_sy.xml +++ b/i18n/src/main/res/values/strings_sy.xml @@ -426,6 +426,7 @@ No chapters found, this entry cannot be used for migration No Alternatives Found Stop migrating? + Stop Migrate %1$d%2$s entry? Migrate %1$d%2$s entries?