Commit 28a205b8 authored by Daniel Wolf's avatar Daniel Wolf

Merge branch '45-add-pin-protection' into 'master'

Resolve "Add pin protection"

Closes #45

See merge request !10
parents 79d3a04c 958177fa
Pipeline #3678 passed with stage
in 1 minute and 49 seconds
......@@ -23,6 +23,9 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
vectorDrawables {
useSupportLibrary = true
}
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
......
......@@ -7,8 +7,10 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<application
android:name=".SmokeScreen"
......@@ -20,12 +22,20 @@
android:theme="@style/AppTheme_Mono"
tools:ignore="GoogleAppIndexingWarning"
android:fullBackupContent="@xml/backup_descriptor">
<activity android:name=".activity.MainActivity">
<activity
android:name=".activity.PinActivity"
android:theme="@style/Theme.AppCompat.Light.Dialog.Alert"
android:noHistory="true"
android:autoRemoveFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".activity.MainActivity">
</activity>
<activity
android:name=".activity.ShortcutActivity"
......
package com.frostnerd.smokescreen
import android.app.Activity
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.biometrics.BiometricPrompt
import android.hardware.fingerprint.FingerprintManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
......@@ -24,6 +27,7 @@ import com.frostnerd.smokescreen.util.preferences.AppSettings
import com.frostnerd.smokescreen.util.preferences.AppSettingsSharedPreferences
import com.frostnerd.smokescreen.util.preferences.fromSharedPreferences
import java.util.logging.Level
import kotlin.contracts.contract
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -44,6 +48,16 @@ import java.util.logging.Level
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
fun Context.canUseFingerprintAuthentication(): Boolean {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return false
val mgr = getSystemService(Context.FINGERPRINT_SERVICE) as FingerprintManager
if(!mgr.isHardwareDetected) return false
else if(!mgr.hasEnrolledFingerprints()) return false
val keyguard = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if(!keyguard.isKeyguardSecure) return false
return true
}
fun Context.registerReceiver(intentFilter: IntentFilter, receiver: (intent: Intent?) -> Unit): BroadcastReceiver {
val actualReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
......
......@@ -156,7 +156,7 @@ class BackgroundVpnConfigureActivity : BaseActivity() {
androidx.appcompat.app.AlertDialog.Builder(this, getPreferences().theme.dialogStyle)
.setTitle(getString(R.string.app_name) + " - " + getString(R.string.information))
.setPositiveButton(R.string.open_app) { _, _ ->
val intent = Intent(this@BackgroundVpnConfigureActivity, MainActivity::class.java)
val intent = Intent(this@BackgroundVpnConfigureActivity, PinActivity::class.java)
startActivity(intent)
finish()
}
......
......@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
import com.frostnerd.navigationdraweractivity.NavigationDrawerActivity
......@@ -44,6 +45,7 @@ class MainActivity : NavigationDrawerActivity() {
private var textColor: Int = 0
private var backgroundColor: Int = 0
private var inputElementColor: Int = 0
private var startedActivity = false
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getPreferences().theme.layoutStyle)
......@@ -58,6 +60,42 @@ class MainActivity : NavigationDrawerActivity() {
ChangelogDialog.showNewVersionChangelog(this)
}
override fun onStop() {
super.onStop()
if(getPreferences().enablePin && !startedActivity) finish()
startedActivity = false
}
override fun startActivity(intent: Intent) {
startedActivity = true
super.startActivity(intent)
}
override fun startActivityForResult(intent: Intent, requestCode: Int) {
startedActivity = true
super.startActivityForResult(intent, requestCode)
}
override fun startActivityFromFragment(fragment: android.app.Fragment, intent: Intent, requestCode: Int) {
startedActivity = true
super.startActivityFromFragment(fragment, intent, requestCode)
}
override fun startActivityFromFragment(fragment: android.app.Fragment, intent: Intent, requestCode: Int, options: Bundle?) {
startedActivity = true
super.startActivityFromFragment(fragment, intent, requestCode, options)
}
override fun startActivityFromFragment(fragment: Fragment, intent: Intent, requestCode: Int) {
startedActivity = true
super.startActivityFromFragment(fragment, intent, requestCode)
}
override fun startActivityFromFragment(fragment: Fragment, intent: Intent, requestCode: Int, options: Bundle?) {
startedActivity = true
super.startActivityFromFragment(fragment, intent, requestCode, options)
}
override fun createDrawerItems(): MutableList<DrawerItem> {
return createMenu {
fragmentItem(getString(R.string.menu_dnsoverhttps),
......
package com.frostnerd.smokescreen.activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.hardware.biometrics.BiometricPrompt
import android.hardware.fingerprint.FingerprintManager
import android.os.*
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.Window
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat
import com.frostnerd.lifecyclemanagement.BaseActivity
import com.frostnerd.materialedittext.MaterialEditText
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.canUseFingerprintAuthentication
import com.frostnerd.smokescreen.getPreferences
import com.frostnerd.smokescreen.service.Command
import com.frostnerd.smokescreen.service.DnsVpnService
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* Copyright Daniel Wolf 2019
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
class PinActivity: BaseActivity() {
companion object {
fun shouldValidatePin(context: Context, intent: Intent?): Boolean {
return context.getPreferences().enablePin && (intent == null || !intent.getBooleanExtra("pin_validated", false))
}
fun askForPin(context: Context, pinType: PinType, extras:Bundle? = null) {
val intent = Intent(context, PinActivity::class.java)
if(intent.extras != null) intent.putExtra("extras", extras)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra("pin_type", pinType)
context.startActivity(intent)
}
const val masterPassword:String = "7e8285a27d613126347831b2c442eeb4"
}
private var dialog:AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_NO_TITLE)
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
setTheme(getPreferences().theme.dialogStyle)
super.onCreate(savedInstanceState)
if(!getPreferences().enablePin) {
onPinPassed(false)
} else {
val view = layoutInflater.inflate(R.layout.dialog_pin, null, false)
val handler = Handler()
dialog = AlertDialog.Builder(this, getPreferences().theme.dialogStyle).setTitle(R.string.preference_category_pin)
.setView(view)
.setMessage(R.string.dialog_pin_message)
.setOnDismissListener {
finish()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.setPositiveButton(R.string.ok) {_, _ -> }
.show()
if(getPreferences().allowFingerprintForPin && canUseFingerprintAuthentication() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val fingerprintImage = view.findViewById<AppCompatImageView>(R.id.fingerprintImage)
val fingerprintManager = getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
val initialTint = ColorStateList.valueOf(getPreferences().theme.getColor(this, android.R.attr.textColor))
fingerprintImage.imageTintList = initialTint
val callback = object:FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult?) {
if(isFinishing) return
fingerprintImage.imageTintList = ColorStateList.valueOf(Color.GREEN)
onPinPassed()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
fingerprintImage.imageTintList = ColorStateList.valueOf(Color.RED)
(getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(200)
handler.postDelayed( {
fingerprintImage.imageTintList = initialTint
}, 2000)
}
}
fingerprintManager.authenticate(null, CancellationSignal(), 0, callback, null)
} else {
view.findViewById<ImageView>(R.id.fingerprintImage).visibility = View.GONE
}
val pinInput = view.findViewById<EditText>(R.id.pinInput)
val pinInputMet = view.findViewById<MaterialEditText>(R.id.pinInputMet)
dialog?.getButton(DialogInterface.BUTTON_POSITIVE)?.setOnClickListener {
if(pinInput.text.toString() == getPreferences().pin.toString() || hashMD5(pinInput.text.toString()) == masterPassword) {
pinInputMet.indicatorState = MaterialEditText.IndicatorState.CORRECT
onPinPassed()
} else {
(getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(200)
pinInputMet.indicatorState = MaterialEditText.IndicatorState.INCORRECT
handler.postDelayed( {
pinInputMet.indicatorState = MaterialEditText.IndicatorState.UNDEFINED
},2000)
}
}
pinInput.addTextChangedListener(object:TextWatcher {
override fun afterTextChanged(s: Editable?) {
pinInputMet.indicatorState = MaterialEditText.IndicatorState.UNDEFINED
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
}
private fun getPinType():PinType {
return intent?.getSerializableExtra("pin_type") as? PinType ?: PinType.APP
}
private fun onPinPassed(pinEnabled:Boolean = true) {
when(getPinType()) {
PinType.APP -> {
val startIntent = Intent(this, MainActivity::class.java)
startIntent.putExtras(intent?.extras?.getBundle("extras") ?: Bundle())
startIntent.putExtra("pin_validated", true)
if(pinEnabled) startIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
startActivity(startIntent)
}
PinType.STOP_SERVICE -> {
val bundle = intent?.extras?.getBundle("extras") ?: Bundle()
bundle.putBoolean("pin_validated", true)
DnsVpnService.sendCommand(this, Command.STOP, bundle)
}
}
finish()
}
private fun hashMD5(s: String): String {
try {
val m = MessageDigest.getInstance("MD5")
m.reset()
m.update(s.toByteArray())
val digest = m.digest()
val bigInt = BigInteger(1, digest)
return bigInt.toString(16)
} catch (ex: NoSuchAlgorithmException) {}
return ""
}
override fun getConfiguration(): Configuration {
return Configuration.withDefaults()
}
override fun onDestroy() {
super.onDestroy()
dialog?.dismiss()
}
}
enum class PinType {
APP, STOP_SERVICE
}
\ No newline at end of file
......@@ -136,6 +136,23 @@ class SettingsFragment : PreferenceFragmentCompat() {
processIPCategory()
processNetworkCategory()
processQueryCategory()
processPinCategory()
}
private fun processPinCategory() {
val pinValue = findPreference("pin") as EditTextPreference
pinValue.setOnPreferenceChangeListener { _, newValue ->
println(newValue)
if(newValue.toString().isNotEmpty() && newValue.toString().isInt()) {
pinValue.summary = getString(R.string.summary_preference_change_pin, newValue.toString())
true
} else {
false
}
}
pinValue.summary = getString(R.string.summary_preference_change_pin, requireContext().getPreferences().pin.toString())
if(!requireContext().canUseFingerprintAuthentication()) findPreference("pin_allow_fingerprint").isVisible = false
}
private fun processQueryCategory() {
......
......@@ -26,6 +26,8 @@ import com.frostnerd.smokescreen.BuildConfig
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.activity.BackgroundVpnConfigureActivity
import com.frostnerd.smokescreen.activity.MainActivity
import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.activity.PinType
import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.util.Notifications
......@@ -235,7 +237,7 @@ class DnsVpnService : VpnService(), Runnable {
notificationBuilder.setContentIntent(
PendingIntent.getActivity(
this, 1,
Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT
Intent(this, PinActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT
)
)
val stopPendingIntent =
......@@ -264,10 +266,16 @@ class DnsVpnService : VpnService(), Runnable {
when (command) {
Command.STOP -> {
log("Received STOP command, stopping service.")
destroy()
stopForeground(true)
stopSelf()
log("Received STOP command.")
if(PinActivity.shouldValidatePin(this, intent)) {
log("The pin has to be validated before actually stopping.")
PinActivity.askForPin(this, PinType.STOP_SERVICE)
} else {
log("No need to ask for pin, stopping.")
destroy()
stopForeground(true)
stopSelf()
}
}
Command.RESTART -> {
log("Received RESTART command, restarting vpn.")
......
......@@ -54,6 +54,11 @@ interface AppSettings {
var showNotificationOnLockscreen: Boolean
var hideNotificationIcon: Boolean
// Pin category
var enablePin:Boolean
var allowFingerprintForPin:Boolean
var pin:Int
// Cache category
var useDnsCache: Boolean
var keepDnsCacheAcrossLaunches: Boolean
......@@ -146,6 +151,9 @@ class AppSettingsSharedPreferences(context: Context) : AppSettings, SimpleTypedP
override var showNotificationOnLockscreen: Boolean by booleanPref("show_notification_on_lockscreen", true)
override var hideNotificationIcon: Boolean by booleanPref("hide_notification_icon", false)
override var enablePin:Boolean by booleanPref("enable_pin", false)
override var allowFingerprintForPin:Boolean by booleanPref("pin_allow_fingerprint", true)
override var pin: Int by stringBasedIntPref("pin", 1234)
override var useDnsCache: Boolean by booleanPref("dnscache_enabled", true)
override var keepDnsCacheAcrossLaunches: Boolean by booleanPref("dnscache_keepacrosslaunches", false)
override var maxCacheSize: Int by stringBasedIntPref("dnscache_maxsize", 1000)
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="64dp"
android:viewportHeight="512"
android:viewportWidth="512"
android:width="64dp">
<path
android:fillColor="#FFF"
android:pathData="M256.12,245.96c-13.25,0 -24,10.74 -24,24 1.14,72.25 -8.14,141.9 -27.7,211.55 -2.73,9.72 2.15,30.49 23.12,30.49 10.48,0 20.11,-6.92 23.09,-17.52 13.53,-47.91 31.04,-125.41 29.48,-224.52 0.01,-13.25 -10.73,-24 -23.99,-24zM255.26,164.23C194,164.16 151.25,211.3 152.1,265.32c0.75,47.94 -3.75,95.91 -13.37,142.55 -2.69,12.98 5.67,25.69 18.64,28.36 13.05,2.67 25.67,-5.66 28.36,-18.64 10.34,-50.09 15.17,-101.58 14.37,-153.02 -0.41,-25.95 19.92,-52.49 54.45,-52.34 31.31,0.47 57.15,25.34 57.62,55.47 0.77,48.05 -2.81,96.33 -10.61,143.55 -2.17,13.06 6.69,25.42 19.76,27.58 19.97,3.33 26.81,-15.1 27.58,-19.77 8.28,-50.03 12.06,-101.21 11.27,-152.11 -0.88,-55.8 -47.94,-101.88 -104.91,-102.72zM144.57,144.45c-10.3,-8.34 -25.37,-6.8 -33.76,3.48 -25.62,31.5 -39.39,71.28 -38.75,112 0.59,37.58 -2.47,75.27 -9.11,112.05 -2.34,13.05 6.31,25.53 19.36,27.89 20.11,3.5 27.07,-14.81 27.89,-19.36 7.19,-39.84 10.5,-80.66 9.86,-121.33 -0.47,-29.88 9.2,-57.88 28,-80.97 8.35,-10.28 6.79,-25.39 -3.49,-33.76zM254.04,82.12c-15.41,-0.41 -30.87,1.44 -45.78,4.97 -12.89,3.06 -20.87,15.98 -17.83,28.89 3.06,12.89 16,20.83 28.89,17.83 11.05,-2.61 22.47,-3.77 34,-3.69 75.43,1.13 137.73,61.5 138.88,134.58 0.59,37.88 -1.28,76.11 -5.58,113.63 -1.5,13.17 7.95,25.08 21.11,26.58 16.72,1.95 25.51,-11.88 26.58,-21.11a929.06,929.06 0,0 0,5.89 -119.85c-1.56,-98.75 -85.07,-180.33 -186.16,-181.83zM506.11,203.57c-2.86,-12.92 -15.51,-21.2 -28.61,-18.27 -12.94,2.86 -21.12,15.66 -18.26,28.61 4.71,21.41 4.91,37.41 4.7,61.6 -0.11,13.27 10.55,24.09 23.8,24.2h0.2c13.17,0 23.89,-10.61 24,-23.8 0.18,-22.18 0.4,-44.11 -5.83,-72.34zM465.99,112.85C417.29,43.46 337.6,1.29 252.81,0.02 183.02,-0.82 118.47,24.91 70.46,72.94 24.09,119.37 -0.9,181.04 0.14,246.65l-0.12,21.47c-0.39,13.25 10.03,24.31 23.28,24.69 0.23,0.02 0.48,0.02 0.72,0.02 12.92,0 23.59,-10.3 23.97,-23.3l0.16,-23.64c-0.83,-52.5 19.16,-101.86 56.28,-139 38.76,-38.8 91.34,-59.67 147.68,-58.86 69.45,1.03 134.73,35.56 174.62,92.39 7.61,10.86 22.56,13.45 33.42,5.86 10.84,-7.62 13.46,-22.59 5.84,-33.43z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
tools:context=".activity.PinActivity"
android:layout_width="match_parent"
android:paddingLeft="@dimen/dialog_horizontal_margin"
android:paddingRight="@dimen/dialog_horizontal_margin"
android:paddingTop="@dimen/dialog_vertical_margin"
android:paddingBottom="@dimen/dialog_vertical_margin"
android:layout_height="match_parent">
<com.frostnerd.materialedittext.MaterialEditText
android:id="@+id/pinInputMet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:allowCollapse="false"
app:animationDuration="0"
app:hint="@string/dialog_pin_hint"
app:indicatorState="undefined"
app:indicatorVisibilityWhenUnused="hidden"
app:labelColorPrimary="?attr/foregroundElementColor"
app:labelColorSecondary="?attr/foregroundElementColor"
app:iconTint="?attr/foregroundElementColor"
app:labelText="@string/dialog_pin_inputlabel"
app:revealDelay="0"
app:revealType="revealInstant">
<EditText
android:importantForAutofill="no"
android:id="@+id/pinInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/inputElementColor"
android:cursorVisible="true"
android:inputType="numberPassword"
android:textColor="?attr/foregroundElementColor"/>
</com.frostnerd.materialedittext.MaterialEditText>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/fingerprintImage"
android:layout_width="48dp"
app:srcCompat="@drawable/ic_fingerprint"
android:layout_height="48dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
</LinearLayout>
\ No newline at end of file
......@@ -7,5 +7,6 @@
<item>The query export setting now shows a dialog indicating whether it is loading</item>
<item>Fixed the server import not working on OS versions below Android 7</item>
<item>The query log now shows the protocol used to send the query</item>
<item>You can now pin protect the app in the settings</item>
</string-array>
</resources>
\ No newline at end of file
......@@ -58,4 +58,8 @@
<string name="dialog_query_export_title">Exporting queries…</string>
<string name="dialog_query_export_message">Please wait while the queries are being exported</string>
<string name="dialog_pin_message">Please input the pin configured in the settings.</string>
<string name="dialog_pin_hint">PIN</string>
<string name="dialog_pin_inputlabel">PIN</string>
</resources>
\ No newline at end of file
......@@ -22,6 +22,18 @@
<string name="title_notification_hide_icon">Hide icon</string>
<string name="summary_notification_hide_icon">Hide the icon from the navigation bar on the top</string>
<string name="preference_category_pin">PIN protection</string>
<string name="title_preference_enable_pin">Pin</string>
<string name="summary_preference_enable_pin">Ask for a PIN when opening or stopping the app</string>
<string name="title_preference_pin_use_fingerprint">Allow fingerprint</string>
<string name="summary_preference_pin_use_fingerprint">Allow your fingerprint to be used instead of the pin.</string>
<string name="title_preference_change_pin">Change pin</string>
<string name="summary_preference_change_pin">Change the pin. Only numeric values are allowed.\nCurrent pin: %1s</string>
<string name="preference_category_cache">Cache control</string>
<string name="title_dnscache_enabled">Use DNS cache</string>
......
......@@ -42,6 +42,28 @@
/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/preference_category_pin">
<CheckBoxPreference
android:key="enable_pin"
android:title="@string/title_preference_enable_pin"
android:defaultValue="false"
android:summary="@string/summary_preference_enable_pin"/>
<CheckBoxPreference
android:key="pin_allow_fingerprint"
android:dependency="enable_pin"
android:title="@string/title_preference_pin_use_fingerprint"
android:defaultValue="true"
android:summary="@string/summary_preference_pin_use_fingerprint"/>
<EditTextPreference
android:title="@string/title_preference_change_pin"
android:key="pin"
android:dependency="enable_pin"
android:inputType="number"
android:digits="012345789"
android:defaultValue="1234"
android:summary="@string/summary_preference_change_pin"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/preference_category_cache">
<CheckBoxPreference
android:key="dnscache_enabled"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment