Convert rotation to FlowPreference, remove some unused subscriptions code

Also remove EH lock code(was broken because of RxController changes)

(cherry picked from commit d46a742a43d23c62aecf203e21a5221a06195131)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
This commit is contained in:
arkon 2020-05-10 11:42:46 -04:00 committed by Jobobby04
parent e4e069ccca
commit ef3f4c2e17
10 changed files with 11 additions and 682 deletions

View File

@ -92,7 +92,7 @@ class PreferencesHelper(val context: Context) {
fun themeDark() = flowPrefs.getString(Keys.themeDark, Values.THEME_DARK_DEFAULT)
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)

View File

@ -10,25 +10,7 @@ import rx.subscriptions.CompositeSubscription
abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseController<VB>(bundle) {
var untilDetachSubscriptions = CompositeSubscription()
private set
var untilDestroySubscriptions = CompositeSubscription()
private set
@CallSuper
override fun onAttach(view: View) {
super.onAttach(view)
if (untilDetachSubscriptions.isUnsubscribed) {
untilDetachSubscriptions = CompositeSubscription()
}
}
@CallSuper
override fun onDetach(view: View) {
super.onDetach(view)
untilDetachSubscriptions.unsubscribe()
}
private var untilDestroySubscriptions = CompositeSubscription()
@CallSuper
override fun onViewCreated(view: View) {
@ -43,49 +25,7 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
untilDestroySubscriptions.unsubscribe()
}
fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
return subscribe().also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit
): Subscription {
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onCompleted: () -> Unit
): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
return subscribe().also { untilDestroySubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit
): Subscription {
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onCompleted: () -> Unit
): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
}
}

View File

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
@ -64,6 +65,7 @@ import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.roundToLong
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.observeOn
@ -226,7 +228,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
super.onDestroy()
viewer?.destroy()
viewer = null
config?.destroy()
config = null
progressDialog?.dismiss()
progressDialog = null
@ -852,22 +853,17 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
*/
private inner class ReaderConfig {
/**
* List of subscriptions to keep while the reader is alive.
*/
private val subscriptions = CompositeSubscription()
/**
* Initializes the reader subscriptions.
*/
init {
val sharedRotation = preferences.rotation().asObservable().share()
val initialRotation = sharedRotation.take(1)
val rotationUpdates = sharedRotation.skip(1)
.delay(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
subscriptions += Observable.merge(initialRotation, rotationUpdates)
.subscribe { setOrientation(it) }
preferences.rotation().asImmediateFlow { setOrientation(it) }
.drop(1)
.onEach {
delay(250)
setOrientation(it)
}
.launchIn(scope)
preferences.readerTheme().asFlow()
.drop(1) // We only care about updates
@ -905,13 +901,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
.launchIn(scope)
}
/**
* Called when the reader is being destroyed. It cleans up all the subscriptions.
*/
fun destroy() {
subscriptions.unsubscribe()
}
/**
* Forces the user preferred [orientation] on the activity.
*/

View File

@ -1,158 +0,0 @@
package exh.ui.lock
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.Gravity
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.preference.SwitchPreferenceCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.github.ajalt.reprint.core.AuthenticationResult
import com.github.ajalt.reprint.core.Reprint
import com.github.ajalt.reprint.rxjava.RxReprint
import com.mattprecious.swirl.SwirlView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.preference.onChange
import exh.util.dpToPx
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy
class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SwitchPreferenceCompat(context, attrs) {
val prefs: PreferencesHelper by injectLazy()
val fingerprintSupported
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Reprint.isHardwarePresent() &&
Reprint.hasFingerprintRegistered()
val useFingerprint
get() = fingerprintSupported &&
prefs.eh_lockUseFingerprint().getOrDefault()
@SuppressLint("NewApi")
override fun onAttached() {
super.onAttached()
if (fingerprintSupported) {
updateSummary()
onChange {
if (it as Boolean) {
tryChange()
} else {
prefs.eh_lockUseFingerprint().set(false)
}
!it
}
} else {
title = "Fingerprint unsupported"
shouldDisableView = true
summary = if (!Reprint.hasFingerprintRegistered()) {
"No fingerprints enrolled!"
} else {
"Fingerprint unlock is unsupported on this device!"
}
onChange { false }
}
}
private fun updateSummary() {
isChecked = useFingerprint
title = if (isChecked) {
"Fingerprint enabled"
} else {
"Fingerprint disabled"
}
}
@TargetApi(Build.VERSION_CODES.M)
fun tryChange() {
val statusTextView = TextView(context).apply {
text = "Please touch the fingerprint sensor"
val size = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size
)
).apply {
width = size
height = size
setPadding(0, 0, dpToPx(context, 8), 0)
}
}
val iconView = SwirlView(context).apply {
val size = dpToPx(context, 30)
layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size
)
).apply {
width = size
height = size
}
setState(SwirlView.State.OFF, false)
}
val linearLayout = LinearLayoutCompat(context).apply {
orientation = LinearLayoutCompat.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT
layoutParams = (
layoutParams ?: LinearLayoutCompat.LayoutParams(
size, size
)
).apply {
width = size
height = size
val pSize = dpToPx(context, 24)
setPadding(pSize, 0, pSize, 0)
}
addView(statusTextView)
addView(iconView)
}
val dialog = MaterialDialog(context)
.title(text = "Fingerprint verification")
.customView(view = linearLayout)
.negativeButton(R.string.action_cancel)
.cancelable(true)
.cancelOnTouchOutside(true)
dialog.show()
iconView.setState(SwirlView.State.ON)
val subscription = RxReprint.authenticate()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
when (result.status) {
AuthenticationResult.Status.SUCCESS -> {
iconView.setState(SwirlView.State.ON)
prefs.eh_lockUseFingerprint().set(true)
dialog.dismiss()
updateSummary()
}
AuthenticationResult.Status.NONFATAL_FAILURE -> {
iconView.setState(SwirlView.State.ERROR)
statusTextView.text = result.errorMessage
}
AuthenticationResult.Status.FATAL_FAILURE, null -> {
MaterialDialog(context)
.title(text = "Fingerprint verification failed!")
.message(text = result.errorMessage)
.positiveButton(android.R.string.ok)
.cancelable(true)
.cancelOnTouchOutside(false)
.show()
dialog.dismiss()
}
}
}
dialog.setOnDismissListener {
subscription.unsubscribe()
}
}
}

View File

@ -1,51 +0,0 @@
package exh.ui.lock
import android.view.WindowManager
import androidx.fragment.app.FragmentActivity
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
object LockActivityDelegate {
private val preferences by injectLazy<PreferencesHelper>()
var willLock: Boolean = true
private val uiScope = CoroutineScope(Dispatchers.Main)
fun doLock(router: Router, animate: Boolean = false) {
router.pushController(
RouterTransaction.with(LockController())
.popChangeHandler(LockChangeHandler(animate))
)
}
fun onCreate(activity: FragmentActivity) {
preferences.secureScreen().asFlow()
.onEach {
if (it) {
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
} else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
.launchIn(uiScope)
}
fun onResume(activity: FragmentActivity, router: Router) {
if (lockEnabled() && !isAppLocked(router) && willLock && !preferences.eh_lockManually().getOrDefault()) {
doLock(router)
willLock = false
}
}
private fun isAppLocked(router: Router): Boolean {
return router.backstack.lastOrNull()?.controller() is LockController
}
}

View File

@ -1,39 +0,0 @@
package exh.ui.lock
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import java.util.ArrayList
class LockChangeHandler : AnimatorChangeHandler {
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(duration, removesFromViewOnPush)
override fun getAnimator(container: ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator {
val animator = AnimatorSet()
val viewAnimators = ArrayList<Animator>()
if (!isPush && from != null) {
viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_X, 3f))
viewAnimators.add(ObjectAnimator.ofFloat(from, View.SCALE_Y, 3f))
viewAnimators.add(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
}
animator.playTogether(viewAnimators)
return animator
}
override fun resetFromView(from: View) {}
override fun copy(): ControllerChangeHandler =
LockChangeHandler(animationDuration, removesFromViewOnPush())
}

View File

@ -1,142 +0,0 @@
package exh.ui.lock
import android.annotation.SuppressLint
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import com.andrognito.pinlockview.PinLockListener
import com.github.ajalt.reprint.core.AuthenticationResult
import com.github.ajalt.reprint.rxjava.RxReprint
import com.mattprecious.swirl.SwirlView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.databinding.ActivityLockBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import exh.util.dpToPx
import kotlinx.android.synthetic.main.activity_lock.view.swirl_container
import uy.kohesive.injekt.injectLazy
class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
val prefs: PreferencesHelper by injectLazy()
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ActivityLockBinding.inflate(inflater)
return binding.root
}
override fun createPresenter() = LockPresenter()
override fun getTitle() = "Application locked"
override fun onViewCreated(view: View) {
super.onViewCreated(view)
if (!lockEnabled(prefs)) {
closeLock()
return
}
with(view) {
// Setup pin lock
binding.pinLockView.attachIndicatorDots(binding.indicatorDots)
binding.pinLockView.pinLength = prefs.eh_lockLength().getOrDefault()
binding.pinLockView.setPinLockListener(object : PinLockListener {
override fun onEmpty() {}
override fun onComplete(pin: String) {
if (sha512(pin, prefs.eh_lockSalt().get()!!) == prefs.eh_lockHash().get()) {
// Yay!
closeLock()
} else {
MaterialDialog(context)
.title(text = "PIN code incorrect")
.message(text = "The PIN code you entered is incorrect. Please try again.")
.cancelable(true)
.cancelOnTouchOutside(true)
.positiveButton(android.R.string.ok)
.show()
binding.pinLockView.resetPinLockView()
}
}
override fun onPinChange(pinLength: Int, intermediatePin: String?) {}
})
}
}
@SuppressLint("NewApi")
override fun onAttach(view: View) {
super.onAttach(view)
with(view) {
// Fingerprint
if (presenter.useFingerprint) {
binding.swirlContainer.visibility = View.VISIBLE
binding.swirlContainer.removeAllViews()
val icon = SwirlView(context).apply {
val size = dpToPx(context, 60)
layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size
)
).apply {
width = size
height = size
val pSize = dpToPx(context, 8)
setPadding(pSize, pSize, pSize, pSize)
}
val lockColor = resolvColor(android.R.attr.windowBackground)
setBackgroundColor(lockColor)
val bgColor = resolvColor(android.R.attr.colorBackground)
// Disable elevation if lock color is same as background color
if (lockColor == bgColor) {
this@with.swirl_container.cardElevation = 0f
}
setState(SwirlView.State.OFF, true)
}
binding.swirlContainer.addView(icon)
icon.setState(SwirlView.State.ON)
RxReprint.authenticate()
.subscribeUntilDetach {
when (it.status) {
AuthenticationResult.Status.SUCCESS -> closeLock()
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
AuthenticationResult.Status.FATAL_FAILURE, null -> {
MaterialDialog(context)
.title(text = "Fingerprint error!")
.message(text = it.errorMessage)
.cancelable(false)
.cancelOnTouchOutside(false)
.positiveButton(android.R.string.ok)
.show()
icon.setState(SwirlView.State.OFF)
}
}
}
} else {
binding.swirlContainer.visibility = View.GONE
}
}
}
private fun resolvColor(color: Int): Int {
val typedVal = TypedValue()
activity!!.theme!!.resolveAttribute(color, typedVal, true)
return typedVal.data
}
override fun onDetach(view: View) {
super.onDetach(view)
}
fun closeLock() {
router.popCurrentController()
}
override fun handleBack() = true
}

View File

@ -1,91 +0,0 @@
package exh.ui.lock
import android.content.Context
import android.text.InputType
import android.util.AttributeSet
import androidx.preference.SwitchPreferenceCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.preference.onChange
import java.math.BigInteger
import java.security.SecureRandom
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SwitchPreferenceCompat(context, attrs) {
private val secureRandom by lazy { SecureRandom() }
val prefs: PreferencesHelper by injectLazy()
override fun onAttached() {
super.onAttached()
updateSummary()
onChange {
tryChange()
false
}
}
private fun updateSummary() {
isChecked = lockEnabled(prefs)
if (isChecked) {
title = "Lock enabled"
summary = "Tap to disable or change pin code"
} else {
title = "Lock disabled"
summary = "Tap to enable"
}
}
fun tryChange() {
if (!notifyLockSecurity(context)) {
MaterialDialog(context)
.title(text = "Lock application")
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
// .inputRangeRes(0, 10, R.color.material_red_500)
// .inputType(InputType.TYPE_CLASS_NUMBER)
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
val progressDialog = MaterialDialog(context)
.title(text = "Saving password")
.cancelable(false)
progressDialog.show()
Observable.fromCallable {
savePassword(c.toString())
}.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
progressDialog.dismiss()
updateSummary()
}
}
.negativeButton(R.string.action_cancel)
.cancelable(true)
.cancelOnTouchOutside(true)
.show()
}
}
private fun savePassword(password: String) {
val salt: String?
val hash: String?
val length: Int
if (password.isEmpty()) {
salt = null
hash = null
length = -1
} else {
salt = BigInteger(130, secureRandom).toString(32)
hash = sha512(password, salt)
length = password.length
}
prefs.eh_lockSalt().set(salt)
prefs.eh_lockHash().set(hash)
prefs.eh_lockLength().set(length)
}
}

View File

@ -1,18 +0,0 @@
package exh.ui.lock
import android.os.Build
import com.github.ajalt.reprint.core.Reprint
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import uy.kohesive.injekt.injectLazy
class LockPresenter : BasePresenter<LockController>() {
val prefs: PreferencesHelper by injectLazy()
val useFingerprint
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Reprint.isHardwarePresent() &&
Reprint.hasFingerprintRegistered() &&
prefs.eh_lockUseFingerprint().getOrDefault()
}

View File

@ -1,101 +0,0 @@
package exh.ui.lock
import android.app.AppOpsManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.provider.Settings
import com.afollestad.materialdialogs.MaterialDialog
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import java.security.MessageDigest
import kotlin.experimental.and
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Password hashing utils
*/
/**
* Yes, I know SHA512 is fast, but bcrypt on mobile devices is too slow apparently
*/
fun sha512(passwordToHash: String, salt: String): String {
val md = MessageDigest.getInstance("SHA-512")
md.update(salt.toByteArray(charset("UTF-8")))
val bytes = md.digest(passwordToHash.toByteArray(charset("UTF-8")))
val sb = StringBuilder()
for (i in bytes.indices) {
sb.append(Integer.toString((bytes[i] and 0xff.toByte()) + 0x100, 16).substring(1))
}
return sb.toString()
}
/**
* Check if lock is enabled
*/
fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) =
prefs.eh_lockHash().get() != null &&
prefs.eh_lockSalt().get() != null &&
prefs.eh_lockLength().getOrDefault() != -1
/**
* Check if the lock will function properly
*
* @return true if action is required, false if lock is working properly
*/
fun notifyLockSecurity(
context: Context,
prefs: PreferencesHelper = Injekt.get()
): Boolean {
return false
if (!prefs.eh_lockManually().getOrDefault() &&
!hasAccessToUsageStats(context)
) {
MaterialDialog(context)
.title(text = "Permission required")
.message(
text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
"This is required for the application lock to function properly. " +
"Press OK to grant this permission now."
)
.negativeButton(R.string.action_cancel)
.positiveButton(android.R.string.ok) {
try {
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
} catch (e: ActivityNotFoundException) {
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
MaterialDialog(context)
.title(text = "Grant permission manually")
.message(
text = "Failed to launch the window used to grant the usage stats permission. " +
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'."
)
.positiveButton(android.R.string.ok) { it.dismiss() }
.cancelable(true)
.cancelOnTouchOutside(false)
.show()
}
}
.cancelable(false)
.show()
return true
} else {
return false
}
}
fun hasAccessToUsageStats(context: Context): Boolean {
return try {
val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo(context.packageName, 0)
val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName)
(mode == AppOpsManager.MODE_ALLOWED)
} catch (e: PackageManager.NameNotFoundException) {
false
}
}