Skip to content

Commit e68d9ca

Browse files
author
lucky
committed
biometric
1 parent 3182199 commit e68d9ca

File tree

13 files changed

+142
-154
lines changed

13 files changed

+142
-154
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ It can:
2222
* limit the maximum number of failed password attempts
2323
* disable USB data connections (Android 12, USB HAL 1.3, Device Owner)
2424

25-
Also you can grant it device and app notifications permission to turn off USB data connections
25+
Also you can grant it device & app notifications permission to turn off USB data connections
2626
automatically on screen off.
2727

2828
## Permissions

app/build.gradle

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ android {
1010
applicationId "me.lucky.sentry"
1111
minSdk 23
1212
targetSdk 32
13-
versionCode 4
14-
versionName "1.0.3"
13+
versionCode 5
14+
versionName "1.0.4"
1515

1616
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1717
}
@@ -49,4 +49,5 @@ dependencies {
4949

5050
implementation 'androidx.security:security-crypto:1.0.0'
5151
implementation 'androidx.preference:preference-ktx:1.2.0'
52+
implementation 'androidx.biometric:biometric:1.1.0'
5253
}

app/src/main/java/me/lucky/sentry/Application.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package me.lucky.sentry
33
import android.app.Application
44
import com.google.android.material.color.DynamicColors
55

6-
@Suppress("unused")
76
class Application : Application() {
87
override fun onCreate() {
98
super.onCreate()
109
DynamicColors.applyToActivitiesIfAvailable(this)
1110
}
12-
}
11+
}

app/src/main/java/me/lucky/sentry/DeviceAdminManager.kt

+2-5
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class DeviceAdminManager(private val ctx: Context) {
1212
private val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }
1313

1414
fun remove() = dpm?.removeActiveAdmin(deviceAdmin)
15-
fun isActive() = dpm?.isAdminActive(deviceAdmin) ?: false
1615
fun getCurrentFailedPasswordAttempts() = dpm?.currentFailedPasswordAttempts ?: 0
16+
fun isDeviceOwner() = dpm?.isDeviceOwnerApp(ctx.packageName) ?: false
1717

1818
@RequiresApi(Build.VERSION_CODES.S)
1919
fun canUsbDataSignalingBeDisabled() = dpm?.canUsbDataSignalingBeDisabled() ?: false
@@ -33,10 +33,7 @@ class DeviceAdminManager(private val ctx: Context) {
3333
dpm?.wipeData(flags)
3434
}
3535

36-
fun setMaximumFailedPasswordsForWipe(num: Int) =
37-
dpm?.setMaximumFailedPasswordsForWipe(deviceAdmin, num)
38-
3936
fun makeRequestIntent() =
4037
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
4138
.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdmin)
42-
}
39+
}

app/src/main/java/me/lucky/sentry/DeviceAdminReceiver.kt

+3-5
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@ import android.os.UserManager
1010
class DeviceAdminReceiver : DeviceAdminReceiver() {
1111
override fun onPasswordFailed(context: Context, intent: Intent, user: UserHandle) {
1212
super.onPasswordFailed(context, intent, user)
13-
val prefs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
14-
context.getSystemService(UserManager::class.java)?.isUserUnlocked != true)
15-
16-
PreferencesDirectBoot(context) else Preferences(context)
13+
val prefs = Preferences(context, encrypted = Build.VERSION.SDK_INT < Build.VERSION_CODES.N
14+
|| context.getSystemService(UserManager::class.java)?.isUserUnlocked == true)
1715
val maxFailedPasswordAttempts = prefs.maxFailedPasswordAttempts
1816
if (!prefs.isEnabled || maxFailedPasswordAttempts <= 0) return
1917
val admin = DeviceAdminManager(context)
2018
if (admin.getCurrentFailedPasswordAttempts() >= maxFailedPasswordAttempts)
2119
try { admin.wipeData() } catch (exc: SecurityException) {}
2220
}
23-
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,139 @@
11
package me.lucky.sentry
22

3-
import android.content.pm.PackageManager
3+
import android.content.SharedPreferences
44
import android.os.Build
55
import android.os.Bundle
6-
import android.view.View
76
import androidx.activity.result.contract.ActivityResultContracts
87
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.biometric.BiometricManager
9+
import androidx.biometric.BiometricPrompt
10+
import androidx.core.content.ContextCompat
911
import com.google.android.material.snackbar.Snackbar
1012

1113
import me.lucky.sentry.databinding.ActivityMainBinding
1214

1315
class MainActivity : AppCompatActivity() {
1416
private lateinit var binding: ActivityMainBinding
15-
private lateinit var prefs: PreferencesProxy
17+
private lateinit var prefs: Preferences
18+
private lateinit var prefsdb: Preferences
1619
private lateinit var admin: DeviceAdminManager
1720

1821
private val registerForDeviceAdmin =
1922
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
20-
if (it.resultCode != RESULT_OK) binding.toggle.isChecked = false else setOn()
23+
if (it.resultCode != RESULT_OK) setOff() else setOn()
2124
}
2225

26+
private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
27+
prefs.copyTo(prefsdb, key)
28+
}
29+
2330
override fun onCreate(savedInstanceState: Bundle?) {
2431
super.onCreate(savedInstanceState)
2532
binding = ActivityMainBinding.inflate(layoutInflater)
2633
setContentView(binding.root)
2734
init()
35+
if (initBiometric()) return
2836
setup()
2937
}
3038

3139
override fun onStart() {
3240
super.onStart()
41+
prefs.registerListener(prefsListener)
3342
update()
3443
}
3544

45+
override fun onStop() {
46+
super.onStop()
47+
prefs.unregisterListener(prefsListener)
48+
}
49+
50+
private fun initBiometric(): Boolean {
51+
val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or
52+
BiometricManager.Authenticators.DEVICE_CREDENTIAL
53+
when (BiometricManager
54+
.from(this)
55+
.canAuthenticate(authenticators))
56+
{
57+
BiometricManager.BIOMETRIC_SUCCESS -> {}
58+
else -> return false
59+
}
60+
val executor = ContextCompat.getMainExecutor(this)
61+
val prompt = BiometricPrompt(
62+
this,
63+
executor,
64+
object : BiometricPrompt.AuthenticationCallback()
65+
{
66+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
67+
super.onAuthenticationError(errorCode, errString)
68+
finishAndRemoveTask()
69+
}
70+
71+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
72+
super.onAuthenticationSucceeded(result)
73+
setup()
74+
}
75+
})
76+
prompt.authenticate(BiometricPrompt.PromptInfo.Builder()
77+
.setTitle(getString(R.string.biometric_title))
78+
.setConfirmationRequired(false)
79+
.setAllowedAuthenticators(authenticators)
80+
.build())
81+
return true
82+
}
83+
3684
private fun init() {
37-
prefs = PreferencesProxy(this)
38-
prefs.clone()
85+
prefs = Preferences(this)
86+
prefsdb = Preferences(this, encrypted = false)
87+
prefs.copyTo(prefsdb)
3988
admin = DeviceAdminManager(this)
40-
if (prefs.isEnabled && prefs.maxFailedPasswordAttempts > 0)
41-
try { admin.setMaximumFailedPasswordsForWipe(0) } catch (exc: SecurityException) {}
42-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
43-
!packageManager.hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN))
44-
hideSecureLockScreenRequired()
45-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !admin.canUsbDataSignalingBeDisabled())
46-
hideUsbDataSignaling()
89+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
90+
!admin.canUsbDataSignalingBeDisabled() ||
91+
!admin.isDeviceOwner())
92+
disableUsbDataSignaling()
4793
binding.apply {
4894
maxFailedPasswordAttempts.value = prefs.maxFailedPasswordAttempts.toFloat()
4995
usbDataSignaling.isChecked = isUsbDataSignalingEnabled()
5096
toggle.isChecked = prefs.isEnabled
5197
}
5298
}
5399

54-
private fun setup() {
55-
binding.apply {
56-
maxFailedPasswordAttempts.addOnChangeListener { _, value, _ ->
57-
prefs.maxFailedPasswordAttempts = value.toInt()
58-
}
59-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
60-
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
61-
try {
62-
admin.setUsbDataSignalingEnabled(isChecked)
63-
} catch (exc: Exception) {
64-
Snackbar.make(
65-
usbDataSignaling,
66-
R.string.usb_data_signaling_change_failed_popup,
67-
Snackbar.LENGTH_SHORT,
68-
).show()
69-
usbDataSignaling.isChecked = !isChecked
70-
}
100+
private fun setup() = binding.apply {
101+
maxFailedPasswordAttempts.addOnChangeListener { _, value, _ ->
102+
prefs.maxFailedPasswordAttempts = value.toInt()
103+
}
104+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
105+
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
106+
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (exc: Exception) {
107+
Snackbar.make(
108+
usbDataSignaling,
109+
R.string.usb_data_signaling_change_failed_popup,
110+
Snackbar.LENGTH_SHORT,
111+
).show()
112+
usbDataSignaling.isChecked = !isChecked
71113
}
72-
toggle.setOnCheckedChangeListener { _, isChecked ->
73-
if (isChecked) requestAdmin() else setOff()
74114
}
115+
toggle.setOnCheckedChangeListener { _, isChecked ->
116+
if (isChecked) requestAdmin() else setOff()
75117
}
76118
}
77119

78-
private fun hideSecureLockScreenRequired() {
79-
binding.apply {
80-
maxFailedPasswordAttempts.visibility = View.GONE
81-
maxFailedPasswordAttemptsDescription.visibility = View.GONE
82-
space.visibility = View.GONE
83-
}
84-
}
85-
86-
private fun hideUsbDataSignaling() {
87-
binding.apply {
88-
usbDataSignaling.visibility = View.GONE
89-
usbDataSignalingDescription.visibility = View.GONE
90-
}
91-
}
120+
private fun disableUsbDataSignaling() { binding.usbDataSignaling.isEnabled = false }
92121

93122
private fun setOn() {
94123
prefs.isEnabled = true
124+
binding.toggle.isChecked = true
95125
}
96126

97127
private fun setOff() {
98128
prefs.isEnabled = false
99129
try { admin.remove() } catch (exc: SecurityException) {}
130+
binding.toggle.isChecked = false
100131
}
101132

102-
private fun update() {
103-
binding.usbDataSignaling.isChecked = isUsbDataSignalingEnabled()
104-
if (prefs.isEnabled && !admin.isActive())
105-
Snackbar.make(
106-
binding.toggle,
107-
R.string.service_unavailable_popup,
108-
Snackbar.LENGTH_SHORT,
109-
).show()
110-
}
133+
private fun update() { binding.usbDataSignaling.isChecked = isUsbDataSignalingEnabled() }
111134

112135
private fun isUsbDataSignalingEnabled() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
113136
admin.isUsbDataSignalingEnabled() else true
114137

115138
private fun requestAdmin() = registerForDeviceAdmin.launch(admin.makeRequestIntent())
116-
}
139+
}

app/src/main/java/me/lucky/sentry/NotificationListenerService.kt

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package me.lucky.sentry
22

3-
import android.app.KeyguardManager
43
import android.content.BroadcastReceiver
54
import android.content.Context
65
import android.content.Intent
@@ -23,14 +22,14 @@ class NotificationListenerService : NotificationListenerService() {
2322
}
2423

2524
private fun init() {
26-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
27-
DeviceAdminManager(this).canUsbDataSignalingBeDisabled())
28-
{
29-
registerReceiver(lockReceiver, IntentFilter().apply {
30-
addAction(Intent.ACTION_USER_PRESENT)
31-
addAction(Intent.ACTION_SCREEN_OFF)
32-
})
33-
}
25+
val admin = DeviceAdminManager(this)
26+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
27+
!admin.canUsbDataSignalingBeDisabled() ||
28+
!admin.isDeviceOwner()) { return }
29+
registerReceiver(lockReceiver, IntentFilter().apply {
30+
addAction(Intent.ACTION_USER_PRESENT)
31+
addAction(Intent.ACTION_SCREEN_OFF)
32+
})
3433
}
3534

3635
private fun deinit() {
@@ -48,11 +47,7 @@ class NotificationListenerService : NotificationListenerService() {
4847
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
4948
!Preferences(context ?: return).isEnabled) return
5049
when (intent?.action) {
51-
Intent.ACTION_USER_PRESENT -> {
52-
if (context.getSystemService(KeyguardManager::class.java)
53-
?.isDeviceSecure != true) return
54-
setUsbDataSignalingEnabled(context, true)
55-
}
50+
Intent.ACTION_USER_PRESENT -> setUsbDataSignalingEnabled(context, true)
5651
Intent.ACTION_SCREEN_OFF -> setUsbDataSignalingEnabled(context, false)
5752
}
5853
}
@@ -63,4 +58,4 @@ class NotificationListenerService : NotificationListenerService() {
6358
catch (exc: Exception) {}
6459
}
6560
}
66-
}
61+
}

0 commit comments

Comments
 (0)