diff --git a/app/build.gradle b/app/build.gradle
index 55d9f61fe..98db6dc05 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -211,6 +211,9 @@ dependencies {
//JVE (Regex) (EH)
compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
+ //Pin lock view
+ compile 'com.andrognito.pinlockview:pinlockview:1.0.1'
+
// Tests
//Paper DB screws up tests
/*testCompile 'junit:junit:4.12'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 078873d5b..8c55ef3af 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,6 +13,9 @@
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 6ec68c858..b8321b408 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -14,6 +14,8 @@ import java.io.File
fun Preference.getOrDefault(): T = get() ?: defaultValue()!!
+fun Preference.nullGetOrDefault(): T? = get() ?: defaultValue()
+
fun Preference.invert(): Boolean = getOrDefault().let { set(!it); !it }
class PreferencesHelper(val context: Context) {
@@ -176,4 +178,11 @@ class PreferencesHelper(val context: Context) {
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
fun igneousVal() = rxPrefs.getString("eh_igneous", null)
+
+ //Lock
+ fun lockHash() = rxPrefs.getString("lock_hash", null)
+
+ fun lockSalt() = rxPrefs.getString("lock_salt", null)
+
+ fun lockLength() = rxPrefs.getInteger("lock_length", -1)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
index 38a4568d0..59a2c317a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
@@ -2,6 +2,15 @@ package eu.kanade.tachiyomi.ui.base.activity
import android.support.v7.app.AppCompatActivity
import eu.kanade.tachiyomi.util.LocaleHelper
+import exh.ui.lock.lockEnabled
+import exh.ui.lock.showLockActivity
+import android.app.ActivityManager
+import android.app.Service
+import android.app.usage.UsageStats
+import android.app.usage.UsageStatsManager
+import android.os.Build
+import java.util.*
+
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
@@ -23,4 +32,48 @@ abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
super.onPause()
}
+ var willLock = false
+ var disableLock = false
+ override fun onRestart() {
+ super.onRestart()
+ if(willLock && lockEnabled() && !disableLock) {
+ showLockActivity(this)
+ }
+
+ willLock = false
+ }
+
+ override fun onStop() {
+ super.onStop()
+ tryLock()
+ }
+
+ fun tryLock() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val mUsageStatsManager = getSystemService("usagestats") as UsageStatsManager
+ val time = System.currentTimeMillis()
+ // We get usage stats for the last 20 seconds
+ val stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 20, time)
+ // Sort the stats by the last time used
+ if (stats != null) {
+ val mySortedMap = TreeMap()
+ for (usageStats in stats) {
+ mySortedMap.put(usageStats.lastTimeUsed, usageStats)
+ }
+ if (!mySortedMap.isEmpty()) {
+ if(mySortedMap[mySortedMap.lastKey()]?.packageName != packageName) {
+ willLock = true
+ }
+ }
+ }
+ } else {
+ val am = getSystemService(Service.ACTIVITY_SERVICE) as ActivityManager
+ val tasks: List
+ tasks = am.getRunningTasks(1)
+ val running = tasks[0]
+ if (running.topActivity.packageName != packageName) {
+ willLock = true
+ }
+ }
+ }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 4bda35e74..3ab75713e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -19,6 +19,9 @@ 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.batchadd.BatchAddFragment
+import exh.ui.lock.lockEnabled
+import exh.ui.lock.notifyLockSecurity
+import exh.ui.lock.showLockActivity
import exh.ui.migration.LibraryMigrationManager
import exh.ui.migration.SourceMigrator
import exh.ui.migration.UrlMigrator
@@ -90,9 +93,12 @@ class MainActivity : BaseActivity() {
}
if (savedState == null) {
+ //Show lock
+ if(lockEnabled(preferences)) {
+ showLockActivity(this)
+ }
//Perform source migration
SourceMigrator().tryMigrationWithDialog(this, {
-
// Set start screen
try {
setSelectedDrawerItem(startScreenId)
@@ -114,6 +120,9 @@ class MainActivity : BaseActivity() {
//Migrate URLs if necessary
UrlMigrator().tryMigration()
+
+ //Check lock security
+ notifyLockSecurity(this)
})
}
}
diff --git a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt
index fa1a7e605..d46811b7d 100644
--- a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt
+++ b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt
@@ -43,7 +43,7 @@ class InterceptActivity : BaseActivity() {
.canceledOnTouchOutside(true)
.cancelListener { onBackPressed() }
.positiveText("Ok")
- .onPositive { materialDialog, dialogAction -> onBackPressed() }
+ .onPositive { _, _ -> onBackPressed() }
.dismissListener { onBackPressed() }
.show()
}
diff --git a/app/src/main/java/exh/ui/lock/LockActivity.kt b/app/src/main/java/exh/ui/lock/LockActivity.kt
new file mode 100644
index 000000000..e1730e574
--- /dev/null
+++ b/app/src/main/java/exh/ui/lock/LockActivity.kt
@@ -0,0 +1,60 @@
+package exh.ui.lock
+
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.andrognito.pinlockview.PinLockListener
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.nullGetOrDefault
+import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import kotlinx.android.synthetic.main.activity_lock.*
+import uy.kohesive.injekt.injectLazy
+
+class LockActivity : BaseActivity() {
+
+ val prefs: PreferencesHelper by injectLazy()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ disableLock = true
+
+ setTheme(R.style.Theme_Tachiyomi_Dark)
+ super.onCreate(savedInstanceState)
+
+ if(!lockEnabled(prefs)) {
+ finish()
+ return
+ }
+
+ setContentView(R.layout.activity_lock)
+
+ pin_lock_view.attachIndicatorDots(indicator_dots)
+
+ pin_lock_view.pinLength = prefs.lockLength().nullGetOrDefault()!!
+ pin_lock_view.setPinLockListener(object : PinLockListener {
+ override fun onEmpty() {}
+
+ override fun onComplete(pin: String) {
+ if(sha512(pin, prefs.lockSalt().nullGetOrDefault()!!) == prefs.lockHash().nullGetOrDefault()) {
+ //Yay!
+ finish()
+ } else {
+ MaterialDialog.Builder(this@LockActivity)
+ .title("PIN code incorrect")
+ .content("The PIN code you entered is incorrect. Please try again.")
+ .cancelable(true)
+ .canceledOnTouchOutside(true)
+ .positiveText("Ok")
+ .autoDismiss(true)
+ .show()
+ pin_lock_view.resetPinLockView()
+ }
+ }
+
+ override fun onPinChange(pinLength: Int, intermediatePin: String?) {}
+ })
+ }
+
+ override fun onBackPressed() {
+ moveTaskToBack(true)
+ }
+}
diff --git a/app/src/main/java/exh/ui/lock/LockPreference.kt b/app/src/main/java/exh/ui/lock/LockPreference.kt
new file mode 100644
index 000000000..4f6aa09b4
--- /dev/null
+++ b/app/src/main/java/exh/ui/lock/LockPreference.kt
@@ -0,0 +1,84 @@
+package exh.ui.lock
+
+import android.content.Context
+import android.support.v7.preference.Preference
+import android.text.InputType
+import android.util.AttributeSet
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.injectLazy
+import java.math.BigInteger
+import java.security.SecureRandom
+
+class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ Preference(context, attrs) {
+
+ val secureRandom by lazy { SecureRandom() }
+
+ val prefs: PreferencesHelper by injectLazy()
+
+ override fun onAttached() {
+ super.onAttached()
+ updateSummary()
+ }
+
+ fun updateSummary() {
+ if(lockEnabled(prefs)) {
+ summary = "Application is locked"
+ } else {
+ summary = "Application is not locked, tap to lock"
+ }
+ }
+
+ override fun onClick() {
+ super.onClick()
+ if(!notifyLockSecurity(context)) {
+ MaterialDialog.Builder(context)
+ .title("Lock application")
+ .content("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("Password", "", { _, c ->
+ val progressDialog = MaterialDialog.Builder(context)
+ .title("Saving password")
+ .progress(true, 0)
+ .cancelable(false)
+ .show()
+ Observable.fromCallable {
+ savePassword(c.toString())
+ }.observeOn(Schedulers.computation())
+ .subscribeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ progressDialog.dismiss()
+ updateSummary()
+ }
+ })
+ .autoDismiss(true)
+ .cancelable(true)
+ .canceledOnTouchOutside(true)
+ .show()
+ }
+ }
+
+ 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.lockSalt().set(salt)
+ prefs.lockHash().set(hash)
+ prefs.lockLength().set(length)
+ }
+}
diff --git a/app/src/main/java/exh/ui/lock/LockUtils.kt b/app/src/main/java/exh/ui/lock/LockUtils.kt
new file mode 100644
index 000000000..37edae2bf
--- /dev/null
+++ b/app/src/main/java/exh/ui/lock/LockUtils.kt
@@ -0,0 +1,91 @@
+package exh.ui.lock
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.provider.Settings
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.nullGetOrDefault
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.security.MessageDigest
+import kotlin.experimental.and
+
+
+/**
+ * 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.lockHash().nullGetOrDefault() != null
+ && prefs.lockSalt().nullGetOrDefault() != null
+ && prefs.lockLength().nullGetOrDefault() != -1
+
+/**
+ * Lock the screen
+ */
+fun showLockActivity(activity: Activity) {
+ activity.startActivity(Intent(activity, LockActivity::class.java))
+}
+
+/**
+ * Check if the lock will function properly
+ *
+ * @return true if action is required, false if lock is working properly
+ */
+fun notifyLockSecurity(context: Context): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !hasAccessToUsageStats(context)) {
+ MaterialDialog.Builder(context)
+ .title("Permission required")
+ .content("${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.")
+ .negativeText("Cancel")
+ .positiveText("Ok")
+ .onPositive { _, _ ->
+ context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
+ }
+ .autoDismiss(true)
+ .cancelable(false)
+ .show()
+ return true
+ } else {
+ return false
+ }
+}
+
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+fun hasAccessToUsageStats(context: Context): Boolean {
+ 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)
+ return (mode == AppOpsManager.MODE_ALLOWED)
+ } catch (e: PackageManager.NameNotFoundException) {
+ return false
+ }
+}
diff --git a/app/src/main/res/layout/activity_lock.xml b/app/src/main/res/layout/activity_lock.xml
new file mode 100644
index 000000000..e6898f560
--- /dev/null
+++ b/app/src/main/res/layout/activity_lock.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml
index 23285b8ad..95dc6702e 100644
--- a/app/src/main/res/xml/pref_general.xml
+++ b/app/src/main/res/xml/pref_general.xml
@@ -63,6 +63,11 @@
android:key="@string/pref_update_only_non_completed_key"
android:title="@string/pref_update_only_non_completed" />
+
+
\ No newline at end of file