Add application lock functionality.
This commit is contained in:
parent
5f48bb8e7d
commit
f36327ecc9
@ -211,6 +211,9 @@ dependencies {
|
|||||||
//JVE (Regex) (EH)
|
//JVE (Regex) (EH)
|
||||||
compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
|
compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
|
||||||
|
|
||||||
|
//Pin lock view
|
||||||
|
compile 'com.andrognito.pinlockview:pinlockview:1.0.1'
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
//Paper DB screws up tests
|
//Paper DB screws up tests
|
||||||
/*testCompile 'junit:junit:4.12'
|
/*testCompile 'junit:junit:4.12'
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
android:name="android.permission.READ_PHONE_STATE"
|
android:name="android.permission.READ_PHONE_STATE"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
<uses-permission android:name="android.permission.GET_TASKS"/>
|
||||||
|
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -158,6 +161,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="exh.ui.lock.LockActivity"
|
||||||
|
android:label="Application locked"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -14,6 +14,8 @@ import java.io.File
|
|||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
|
|
||||||
|
fun <T> Preference<T>.nullGetOrDefault(): T? = get() ?: defaultValue()
|
||||||
|
|
||||||
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
@ -176,4 +178,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
|
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
|
||||||
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
|
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
|
||||||
fun igneousVal() = rxPrefs.getString("eh_igneous", 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)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,15 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
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 {
|
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||||
|
|
||||||
@ -23,4 +32,48 @@ abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
|||||||
super.onPause()
|
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<Long, UsageStats>()
|
||||||
|
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<ActivityManager.RunningTaskInfo>
|
||||||
|
tasks = am.getRunningTasks(1)
|
||||||
|
val running = tasks[0]
|
||||||
|
if (running.topActivity.packageName != packageName) {
|
||||||
|
willLock = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.recently_read.RecentlyReadFragment
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
||||||
import exh.ui.batchadd.BatchAddFragment
|
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.LibraryMigrationManager
|
||||||
import exh.ui.migration.SourceMigrator
|
import exh.ui.migration.SourceMigrator
|
||||||
import exh.ui.migration.UrlMigrator
|
import exh.ui.migration.UrlMigrator
|
||||||
@ -90,9 +93,12 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
|
//Show lock
|
||||||
|
if(lockEnabled(preferences)) {
|
||||||
|
showLockActivity(this)
|
||||||
|
}
|
||||||
//Perform source migration
|
//Perform source migration
|
||||||
SourceMigrator().tryMigrationWithDialog(this, {
|
SourceMigrator().tryMigrationWithDialog(this, {
|
||||||
|
|
||||||
// Set start screen
|
// Set start screen
|
||||||
try {
|
try {
|
||||||
setSelectedDrawerItem(startScreenId)
|
setSelectedDrawerItem(startScreenId)
|
||||||
@ -114,6 +120,9 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
//Migrate URLs if necessary
|
//Migrate URLs if necessary
|
||||||
UrlMigrator().tryMigration()
|
UrlMigrator().tryMigration()
|
||||||
|
|
||||||
|
//Check lock security
|
||||||
|
notifyLockSecurity(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class InterceptActivity : BaseActivity() {
|
|||||||
.canceledOnTouchOutside(true)
|
.canceledOnTouchOutside(true)
|
||||||
.cancelListener { onBackPressed() }
|
.cancelListener { onBackPressed() }
|
||||||
.positiveText("Ok")
|
.positiveText("Ok")
|
||||||
.onPositive { materialDialog, dialogAction -> onBackPressed() }
|
.onPositive { _, _ -> onBackPressed() }
|
||||||
.dismissListener { onBackPressed() }
|
.dismissListener { onBackPressed() }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
60
app/src/main/java/exh/ui/lock/LockActivity.kt
Normal file
60
app/src/main/java/exh/ui/lock/LockActivity.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
84
app/src/main/java/exh/ui/lock/LockPreference.kt
Normal file
84
app/src/main/java/exh/ui/lock/LockPreference.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
91
app/src/main/java/exh/ui/lock/LockUtils.kt
Normal file
91
app/src/main/java/exh/ui/lock/LockUtils.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
29
app/src/main/res/layout/activity_lock.xml
Normal file
29
app/src/main/res/layout/activity_lock.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.constraint.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.andrognito.pinlockview.PinLockView
|
||||||
|
android:id="@+id/pin_lock_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/indicator_dots"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent" />
|
||||||
|
|
||||||
|
<com.andrognito.pinlockview.IndicatorDots
|
||||||
|
android:id="@+id/indicator_dots"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/pin_lock_view"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent" />
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
@ -63,6 +63,11 @@
|
|||||||
android:key="@string/pref_update_only_non_completed_key"
|
android:key="@string/pref_update_only_non_completed_key"
|
||||||
android:title="@string/pref_update_only_non_completed" />
|
android:title="@string/pref_update_only_non_completed" />
|
||||||
|
|
||||||
|
<exh.ui.lock.LockPreference
|
||||||
|
android:title="Application lock"
|
||||||
|
android:key="pref_app_lock"
|
||||||
|
android:persistent="false"/>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
x
Reference in New Issue
Block a user