Add migration system.

This commit is contained in:
NerdNumber9
2017-01-06 18:01:40 -05:00
parent cb73e55db1
commit 2b988c51ad
17 changed files with 498 additions and 152 deletions
@@ -9,6 +9,7 @@ import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.track.TrackService
import exh.ui.migration.MigrationStatus
import java.io.File
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
@@ -161,6 +162,10 @@ class PreferencesHelper(val context: Context) {
fun migrateLibraryAsked() = rxPrefs.getBoolean("ex_migrate_library", false)
fun migrationStatus() = rxPrefs.getInteger("migration_status", MigrationStatus.NOT_INITIALIZED)
fun finishMainActivity() = rxPrefs.getBoolean("finish_main_activity", false)
//EH Cookies
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
@@ -120,7 +120,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
.searchSubject
.debounce(100L, TimeUnit.MILLISECONDS)
.subscribe { text -> //Debounce search (EH)
adapter.asyncSearchText = text.trim().toLowerCase()
adapter.asyncSearchText = text?.trim()?.toLowerCase()
adapter.updateDataSet()
}
@@ -25,7 +25,6 @@ class ChangelogDialogFragment : DialogFragment() {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
ChangelogDialogFragment().show(fm, "changelog")
// TODO better upgrades management
if (oldVersion == 0) return
if (oldVersion < 14) {
@@ -39,6 +38,7 @@ class ChangelogDialogFragment : DialogFragment() {
// Delete internal chapter cache dir.
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
}
//TODO Review any other changes below
}
}
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.main
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
@@ -7,7 +8,6 @@ import android.support.v4.app.TaskStackBuilder
import android.support.v4.view.GravityCompat
import android.view.MenuItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.backup.BackupFragment
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
@@ -18,18 +18,21 @@ import eu.kanade.tachiyomi.ui.library.LibraryFragment
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import exh.ui.MetadataFetchDialog
import exh.ui.batchadd.BatchAddFragment
import exh.ui.migration.LibraryMigrationManager
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.toolbar.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import rx.Subscription
import uy.kohesive.injekt.injectLazy
class MainActivity : BaseActivity() {
val preferences: PreferencesHelper by injectLazy()
var finishSubscription: Subscription? = null
var dismissQueue = mutableListOf<DialogInterface>()
private val startScreenId by lazy {
when (preferences.startScreen()) {
1 -> R.id.nav_drawer_library
@@ -88,15 +91,25 @@ class MainActivity : BaseActivity() {
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
// Migrate library if needed
Injekt.get<DatabaseHelper>().getLibraryMangas().asRxSingle().subscribe {
if(it.size > 0)
runOnUiThread {
MetadataFetchDialog().tryAskMigration(this)
}
LibraryMigrationManager(this, dismissQueue).askMigrationIfNecessary()
//Last part of migration requires finishing this activity
finishSubscription?.unsubscribe()
preferences.finishMainActivity().set(false)
finishSubscription = preferences.finishMainActivity().asObservable().subscribe {
if(it)
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
finishSubscription?.unsubscribe()
preferences.finishMainActivity().set(false)
dismissQueue.forEach { it.dismiss() }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> drawer.openDrawer(GravityCompat.START)
@@ -155,5 +168,6 @@ class MainActivity : BaseActivity() {
companion object {
private const val REQUEST_OPEN_SETTINGS = 200
const val FINALIZE_MIGRATION = "finalize_migration"
}
}
@@ -6,7 +6,7 @@ import android.support.v7.preference.XpPreferenceFragment
import android.view.View
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.plusAssign
import exh.ui.MetadataFetchDialog
import exh.ui.migration.MetadataFetchDialog
import exh.ui.login.LoginActivity
import net.xpece.android.support.preference.Preference
import net.xpece.android.support.preference.SwitchPreference
@@ -0,0 +1,263 @@
package exh.ui.migration
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.text.Html
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.toast
import uy.kohesive.injekt.injectLazy
/**
* Guide to migrate thel ibrary between two TachiyomiEH apps
*/
class LibraryMigrationManager(val context: MainActivity,
val dismissQueue: MutableList<DialogInterface>? = null) {
val preferenceHelper: PreferencesHelper by injectLazy()
val databaseHelper: DatabaseHelper by injectLazy()
private fun mainTachiyomiEHActivity()
= context.packageManager.getLaunchIntentForPackage(TACHIYOMI_EH_PACKAGE)
fun askMigrationIfNecessary() {
//Check already migrated
val ms = preferenceHelper.migrationStatus().getOrDefault()
if(ms == MigrationStatus.COMPLETED) return
val ma = mainTachiyomiEHActivity()
//Old version not installed, migration not required
if(ma == null) {
preferenceHelper.migrationStatus().set(MigrationStatus.COMPLETED)
return
}
context.requestPermissionsOnMarshmallow()
if(ms == MigrationStatus.NOT_INITIALIZED) {
//We need migration
jumpToMigrationStep(MigrationStatus.NOTIFY_USER)
} else {
//Migration process already started, jump to step
jumpToMigrationStep(ms)
}
}
fun notifyUserMigration() {
redDialog()
.title("Migration necessary")
.content("Due to an unplanned technical error, this update could not be applied on top of the old app and was instead installed as a separate app!\n\n" +
"To keep your library/favorited galleries after this update, you must migrate it over from the old app.\n\n" +
"This migration process is not automatic, tap 'CONTINUE' to be guided through it.")
.positiveText("Continue")
.negativeText("Cancel")
.onPositive { materialDialog, dialogAction -> jumpToMigrationStep(MigrationStatus.OPEN_BACKUP_MENU) }
.onNegative { materialDialog, dialogAction -> warnUserMigration() }
.show()
}
fun warnUserMigration() {
redDialog()
.title("Are you sure?")
.content("You are cancelling the migration process! If you do not migrate your library, you will lose all of your favorited galleries!\n\n" +
"Press 'MIGRATE' to restart the migration process, press 'OK' if you still wish to cancel the migration process.")
.positiveText("Ok")
.negativeText("Migrate")
.onPositive { materialDialog, dialogAction -> completeMigration() }
.onNegative { materialDialog, dialogAction -> notifyUserMigration() }
.show()
}
fun openBackupMenuMigrationStep() {
val view = MigrationViewBuilder()
.text("1. Use the 'LAUNCH OLD APP' button below to launch the old app.")
.text("2. Tap on the 'three-lines' button at the top-left of the screen as shown below:")
.image(R.drawable.eh_migration_hamburgers)
.text("3. Highlight the 'Backup' item by tapping on it as shown below:")
.image(R.drawable.eh_migration_backup)
.text("4. Return to this app but <b>do not close</b> the old app.")
.text("5. When you have completed the above steps, tap 'CONTINUE'.")
.toView(context)
migrationStepDialog(1, null, MigrationStatus.PERFORM_BACKUP)
.customView(view, true)
.neutralText("Launch Old App")
.onNeutral { materialDialog, dialogAction ->
//Auto dismiss messes this up so we have to reopen the dialog manually
val ma = mainTachiyomiEHActivity()
if(ma != null) {
context.startActivity(ma)
} else {
context.toast("Failed to launch old app! Try launching it manually.")
}
openBackupMenuMigrationStep()
}
.show()
}
fun performBackupMigrationStep() {
val view = MigrationViewBuilder()
.text("6. Return to the old app.")
.text("7. Tap on the 'BACKUP' button in the old app (shown below):")
.image(R.drawable.eh_migration_backup_button)
.text("8. In the menu that appears, tap on 'Complete migration' (shown below):")
.image(R.drawable.eh_migration_share_icon)
.toView(context)
migrationStepDialog(2, MigrationStatus.OPEN_BACKUP_MENU, null)
.customView(view, true)
.show()
}
fun finalizeMigration() {
migrationDialog()
.title("Migration complete")
.content(fromHtmlCompat("Your library has been migrated over to the new app!<br><br>" +
"You may now uninstall the old app by pressing the 'UNINSTALL OLD APP' button below!<br><br>" +
"<b>If you were previously using ExHentai, your library may appear blank, just log in again to fix this.</b><br><br>" +
"Then tap 'OK' to exit the migration process!"))
.positiveText("Ok")
.neutralText("Uninstall Old App")
.onPositive { materialDialog, dialogAction ->
completeMigration()
//Check if the metadata needs to be updated
databaseHelper.getLibraryMangas().asRxSingle().subscribe {
if (it.size > 0)
context.runOnUiThread {
MetadataFetchDialog().tryAskMigration(context)
}
}
}
.onNeutral { materialDialog, dialogAction ->
val packageUri = Uri.parse("package:$TACHIYOMI_EH_PACKAGE")
val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
context.startActivity(uninstallIntent)
//Cancel out auto-dismiss
finalizeMigration()
}
.show()
}
fun migrationDialog() = MaterialDialog.Builder(context)
.cancelable(false)
.canceledOnTouchOutside(false)
.showListener { dismissQueue?.add(it) }!!
fun migrationStepDialog(step: Int, previousStep: Int?, nextStep: Int?) = migrationDialog()
.title("Migration part $step of ${MigrationStatus.MAX_MIGRATION_STEPS}")
.apply {
if(previousStep != null) {
negativeText("Back")
onNegative { materialDialog, dialogAction -> jumpToMigrationStep(previousStep) }
}
if(nextStep != null) {
positiveText("Continue")
onPositive { materialDialog, dialogAction -> jumpToMigrationStep(nextStep) }
}
}!!
fun redDialog() = migrationDialog()
.backgroundColor(Color.parseColor("#F44336"))
.titleColor(Color.WHITE)
.contentColor(Color.WHITE)
.positiveColor(Color.WHITE)
.negativeColor(Color.WHITE)
.neutralColor(Color.WHITE)!!
fun completeMigration() {
preferenceHelper.migrationStatus().set(MigrationStatus.COMPLETED)
//Enable orientation changes again
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
}
fun jumpToMigrationStep(migrationStatus: Int) {
preferenceHelper.migrationStatus().set(migrationStatus)
//Too lazy to actually deal with orientation changes
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
when(migrationStatus) {
MigrationStatus.NOTIFY_USER -> notifyUserMigration()
MigrationStatus.OPEN_BACKUP_MENU -> openBackupMenuMigrationStep()
MigrationStatus.PERFORM_BACKUP -> performBackupMigrationStep()
MigrationStatus.FINALIZE_MIGRATION -> finalizeMigration()
}
}
companion object {
const val TACHIYOMI_EH_PACKAGE = "eu.kanade.tachiyomi.eh"
fun fromHtmlCompat(string: String)
= if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(string, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(string)
}
class MigrationViewBuilder {
val elements = mutableListOf<MigrationElement>()
fun text(text: String) = apply { elements += TextElement(text) }
fun image(drawable: Int) = apply { elements += ImageElement(drawable) }
fun toView(context: Activity): View {
val root = LinearLayout(context)
val rootParams = root.layoutParams ?: ViewGroup.LayoutParams(0, 0)
fun ViewGroup.LayoutParams.setup() = apply {
height = ViewGroup.LayoutParams.WRAP_CONTENT
width = ViewGroup.LayoutParams.MATCH_PARENT
}
fun dpToPx(dp: Float) = (dp * context.resources.displayMetrics.density + 0.5f).toInt()
rootParams.setup()
root.layoutParams = rootParams
root.gravity = Gravity.CENTER
root.orientation = LinearLayout.VERTICAL
for(element in elements) {
val view: View
if(element is TextElement) {
view = TextView(context)
view.text = fromHtmlCompat(element.value)
} else if(element is ImageElement) {
view = ImageView(context)
view.setImageResource(element.drawable)
view.adjustViewBounds = true
} else {
throw IllegalArgumentException("Unknown migration view!")
}
val viewParams = view.layoutParams ?: ViewGroup.LayoutParams(0, 0)
viewParams.setup()
view.layoutParams = viewParams
val eightDpAsPx = dpToPx(8f)
view.setPadding(0, eightDpAsPx, 0, eightDpAsPx)
root.addView(view)
}
return root
}
}
open class MigrationElement
class TextElement(val value: String): MigrationElement()
class ImageElement(val drawable: Int): MigrationElement()
}
@@ -1,4 +1,4 @@
package exh.ui
package exh.ui.migration
import android.app.Activity
import android.content.pm.ActivityInfo
@@ -30,7 +30,7 @@ class MetadataFetchDialog {
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
val progressDialog = MaterialDialog.Builder(context)
.title("Migrating library")
.title("Fetching library metadata")
.content("Preparing library")
.progress(false, 0, true)
.cancelable(false)
@@ -87,27 +87,40 @@ class MetadataFetchDialog {
}
fun askMigration(activity: Activity) {
MaterialDialog.Builder(activity)
.title("Migrate library")
.content("You need to migrate your library before tag searching in the library will function.\n\n" +
"This migration may take a long time depending on your library size and will also use up a significant amount of internet bandwidth.\n\n" +
"This process can be done later if required.")
.positiveText("Migrate")
.negativeText("Later")
.onPositive { materialDialog, dialogAction -> show(activity) }
.onNegative { materialDialog, dialogAction -> adviseMigrationLater(activity) }
.cancelable(false)
.canceledOnTouchOutside(false)
.dismissListener {
preferenceHelper.migrateLibraryAsked().set(true)
}.show()
var extra = ""
db.getLibraryMangas().asRxSingle().subscribe {
//Not logged in but have ExHentai galleries
if(!preferenceHelper.enableExhentai().getOrDefault()) {
it.find { it.source == 2 }?.let {
extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
}
}
activity.runOnUiThread {
MaterialDialog.Builder(activity)
.title("Fetch library metadata")
.content(LibraryMigrationManager.fromHtmlCompat("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
"This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth.<br><br>" +
extra +
"This process can be done later if required."))
.positiveText("Migrate")
.negativeText("Later")
.onPositive { materialDialog, dialogAction -> show(activity) }
.onNegative { materialDialog, dialogAction -> adviseMigrationLater(activity) }
.cancelable(false)
.canceledOnTouchOutside(false)
.dismissListener {
preferenceHelper.migrateLibraryAsked().set(true)
}.show()
}
}
}
fun adviseMigrationLater(activity: Activity) {
MaterialDialog.Builder(activity)
.title("Migration canceled")
.content("Library migration has been canceled.\n\n" +
"You can run this operation later by going to: Settings > EHentai > Migrate Library")
.title("Metadata fetch canceled")
.content("Library metadata fetch has been canceled.\n\n" +
"You can run this operation later by going to: Settings > E-Hentai > Migrate library metadata")
.positiveText("Ok")
.cancelable(true)
.canceledOnTouchOutside(true)
@@ -0,0 +1,92 @@
package exh.ui.migration
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.ShareCompat
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import kotlinx.android.synthetic.main.toolbar.*
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
/**
* Read backups directly from another Tachiyomi app
*/
class MigrationCompletionActivity : BaseActivity() {
private val backupManager by lazy { BackupManager(Injekt.get()) }
private val preferenceManager: PreferencesHelper by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) {
setAppTheme()
super.onCreate(savedInstanceState)
setContentView(R.layout.eh_activity_finish_migration)
setup()
setupToolbar(toolbar, backNavigation = false)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
fun setup() {
try {
val sc = ShareCompat.IntentReader.from(this)
Timber.i("CP: " + sc.callingPackage)
if(sc.isShareIntent) {
//Try to restore backup
thread {
//Finish old MainActivity
preferenceManager.finishMainActivity().set(true)
try {
backupManager.restoreFromStream(contentResolver.openInputStream(sc.stream))
} catch(t: Throwable) {
Timber.e(t, "Failed to restore manga/galleries!")
migrationError("Failed to restore manga/galleries!")
return@thread
}
//Go back to MainActivity
//Set final steps
preferenceManager.migrationStatus().set(MigrationStatus.FINALIZE_MIGRATION)
//Wait for MainActivity to finish
Thread.sleep(1000)
//Start new MainActivity
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(MainActivity.Companion.FINALIZE_MIGRATION, true)
finish()
startActivity(intent)
}
}
} catch(t: Throwable) {
Timber.e(t, "Failed to migrate manga!")
migrationError("An unknown error occurred during migration!")
}
}
fun migrationError(message: String) {
runOnUiThread {
MaterialDialog.Builder(this)
.title("Migration error")
.content(message)
.positiveText("Ok")
.cancelable(false)
.canceledOnTouchOutside(false)
.dismissListener { finish() }
.show()
}
}
override fun onBackPressed() {
//Do not allow finishing this activity
}
}
@@ -0,0 +1,16 @@
package exh.ui.migration
class MigrationStatus {
companion object {
val NOT_INITIALIZED = -1
val COMPLETED = 0
//Migration process
val NOTIFY_USER = 1
val OPEN_BACKUP_MENU = 2
val PERFORM_BACKUP = 3
val FINALIZE_MIGRATION = 4
val MAX_MIGRATION_STEPS = 2
}
}