Skip to content

Commit d0fa3c3

Browse files
author
lucky
committed
1.1.2
1 parent a92e9d5 commit d0fa3c3

25 files changed

+191
-143
lines changed

PRIVACY.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Privacy Policy
22

3-
The app may store package names of apps without internet permission in internal database if
4-
Monitor > Internet is checked.
3+
The app Sentry (me.lucky.sentry) may store package names of apps without internet permission in internal database for monitoring permission changes.
4+
All the data is stored on your device and automatically deleted.

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ Tiny app to enforce security policies of your device.
1717
It can:
1818
* limit the maximum number of failed password attempts
1919
* disable USB data connections (Android 12, USB HAL 1.3, Device Owner)
20+
* disable safe boot mode (Android 7, Device Owner)
2021
* notify on failed password attempt
2122
* notify when an app without Internet permission got it after an update
2223

23-
Also you can grant it device & app notifications permission to turn off USB data connections
24-
automatically on screen off.
24+
Be aware that the app may not work in _safe_ mode.
2525

2626
## Permissions
2727

2828
* DEVICE_ADMIN - limit the maximum number of failed password attempts
2929
* DEVICE_OWNER - disable USB data connections
3030
* NOTIFICATION_LISTENER - receive lock/package events
31-
* QUERY_ALL_PACKAGES - receiver all package events
31+
* QUERY_ALL_PACKAGES - receive all package events
3232

3333
## Example
3434

app/build.gradle

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ plugins {
66

77
android {
88
compileSdk 32
9+
namespace 'me.lucky.sentry'
910

1011
defaultConfig {
1112
applicationId "me.lucky.sentry"
1213
minSdk 23
1314
targetSdk 32
14-
versionCode 8
15-
versionName "1.1.1"
15+
versionCode 9
16+
versionName "1.1.2"
1617

1718
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1819

@@ -47,14 +48,13 @@ android {
4748

4849
dependencies {
4950
implementation 'androidx.core:core-ktx:1.8.0'
50-
implementation 'androidx.appcompat:appcompat:1.5.0'
51+
implementation 'androidx.appcompat:appcompat:1.5.1'
5152
implementation 'com.google.android.material:material:1.6.1'
5253
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
5354
testImplementation 'junit:junit:4.13.2'
5455
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
5556
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
5657

57-
implementation 'androidx.security:security-crypto:1.0.0'
5858
// https://issuetracker.google.com/issues/238425626
5959
implementation('androidx.preference:preference-ktx:1.2.0') {
6060
exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel'

app/src/main/AndroidManifest.xml

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
xmlns:tools="http://schemas.android.com/tools"
4-
package="me.lucky.sentry">
3+
xmlns:tools="http://schemas.android.com/tools">
54

65
<uses-feature android:name="android.software.device_admin" android:required="false" />
76
<uses-permission

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

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
1010
import me.lucky.sentry.databinding.ActivityMainBinding
1111
import me.lucky.sentry.fragment.MainFragment
1212
import me.lucky.sentry.fragment.MonitorFragment
13+
import me.lucky.sentry.fragment.UserRestrictionsFragment
1314

1415
open class MainActivity : AppCompatActivity() {
1516
private lateinit var binding: ActivityMainBinding
@@ -48,6 +49,7 @@ open class MainActivity : AppCompatActivity() {
4849
private fun getFragment(id: Int) = when (id) {
4950
R.id.nav_main -> MainFragment()
5051
R.id.nav_monitor -> MonitorFragment()
52+
R.id.nav_user_restrictions -> UserRestrictionsFragment()
5153
else -> MainFragment()
5254
}
5355

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class NotificationListenerService : NotificationListenerService() {
4848

4949
private fun deinit() {
5050
val unregister = { it: BroadcastReceiver ->
51-
try { unregisterReceiver(it) } catch (exc: IllegalArgumentException) {}
51+
try { unregisterReceiver(it) } catch (_: IllegalArgumentException) {}
5252
}
5353
unregister(lockReceiver)
5454
unregister(packageReceiver)
@@ -73,7 +73,7 @@ class NotificationListenerService : NotificationListenerService() {
7373
@RequiresApi(Build.VERSION_CODES.S)
7474
private fun setUsbDataSignalingEnabled(ctx: Context, enabled: Boolean) {
7575
try { DeviceAdminManager(ctx).setUsbDataSignalingEnabled(enabled) }
76-
catch (exc: Exception) {}
76+
catch (_: Exception) {}
7777
}
7878
}
7979

@@ -100,12 +100,14 @@ class NotificationListenerService : NotificationListenerService() {
100100
val packageName = getPackageName(intent) ?: return
101101
if (Utils.hasInternet(ctx, packageName)) return
102102
try { db.insert(Package(0, packageName)) }
103-
catch (exc: SQLiteConstraintException) {}
103+
catch (_: SQLiteConstraintException) {}
104104
}
105105
Intent.ACTION_PACKAGE_REPLACED -> {
106106
val packageName = getPackageName(intent) ?: return
107107
db.select(packageName) ?: return
108108
if (!Utils.hasInternet(ctx, packageName)) return
109+
db.delete(packageName)
110+
if (!Preferences(ctx).isEnabled) return
109111
NotificationManager(ctx).notifyInternet(packageName)
110112
}
111113
Intent.ACTION_PACKAGE_FULLY_REMOVED ->

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class NotificationManager(private val ctx: Context) {
5858
try {
5959
app = ctx.packageManager
6060
.getApplicationLabel(ctx.packageManager.getApplicationInfo(packageName, 0))
61-
} catch (exc: PackageManager.NameNotFoundException) {}
61+
} catch (_: PackageManager.NameNotFoundException) {}
6262
return ctx.getString(R.string.notification_internet_text, app.toString(), packageName)
6363
}
6464
}

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

+14-42
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,28 @@
11
package me.lucky.sentry
22

33
import android.content.Context
4-
import android.content.SharedPreferences
54
import android.os.Build
65
import androidx.core.content.edit
76
import androidx.preference.PreferenceManager
8-
import androidx.security.crypto.EncryptedSharedPreferences
9-
import androidx.security.crypto.MasterKeys
107

11-
class Preferences(ctx: Context, encrypted: Boolean = true) {
8+
class Preferences(ctx: Context) {
129
companion object {
1310
private const val ENABLED = "enabled"
1411
private const val MAX_FAILED_PASSWORD_ATTEMPTS = "max_failed_password_attempts"
15-
private const val MAX_FAILED_PASSWORD_ATTEMPTS_WARNING =
16-
"max_failed_password_attempts_warning"
12+
private const val MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API =
13+
"max_failed_password_attempts_default_api"
1714
private const val USB_DATA_SIGNALING_CTL_ENABLED = "usb_data_signaling_ctl_enabled"
1815
private const val MONITOR = "monitor"
1916

20-
private const val FILE_NAME = "sec_shared_prefs"
2117
// migration
2218
private const val SERVICE_ENABLED = "service_enabled"
19+
private const val MAX_FAILED_PASSWORD_ATTEMPTS_WARNING =
20+
"max_failed_password_attempts_warning"
2321
}
2422

25-
private val prefs: SharedPreferences = if (encrypted) {
26-
val mk = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
27-
EncryptedSharedPreferences.create(
28-
FILE_NAME,
29-
mk,
30-
ctx,
31-
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
32-
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
33-
)
34-
} else {
35-
val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
36-
ctx.createDeviceProtectedStorageContext() else ctx
37-
PreferenceManager.getDefaultSharedPreferences(context)
38-
}
23+
private val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
24+
ctx.createDeviceProtectedStorageContext() else ctx
25+
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
3926

4027
var isEnabled: Boolean
4128
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
@@ -45,9 +32,12 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
4532
get() = prefs.getInt(MAX_FAILED_PASSWORD_ATTEMPTS, 0)
4633
set(value) = prefs.edit { putInt(MAX_FAILED_PASSWORD_ATTEMPTS, value) }
4734

48-
var isMaxFailedPasswordAttemptsWarningChecked: Boolean
49-
get() = prefs.getBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, false)
50-
set(value) = prefs.edit { putBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, value) }
35+
var isMaxFailedPasswordAttemptsDefaultApiChecked: Boolean
36+
get() = prefs.getBoolean(
37+
MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API,
38+
prefs.getBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, false),
39+
)
40+
set(value) = prefs.edit { putBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API, value) }
5141

5242
var isUsbDataSignalingCtlEnabled: Boolean
5343
get() = prefs.getBoolean(USB_DATA_SIGNALING_CTL_ENABLED, false)
@@ -56,24 +46,6 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
5646
var monitor: Int
5747
get() = prefs.getInt(MONITOR, 0)
5848
set(value) = prefs.edit { putInt(MONITOR, value) }
59-
60-
fun registerListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
61-
prefs.registerOnSharedPreferenceChangeListener(listener)
62-
63-
fun unregisterListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
64-
prefs.unregisterOnSharedPreferenceChangeListener(listener)
65-
66-
fun copyTo(dst: Preferences, key: String? = null) = dst.prefs.edit {
67-
for (entry in prefs.all.entries) {
68-
val k = entry.key
69-
if (key != null && k != key) continue
70-
val v = entry.value ?: continue
71-
when (v) {
72-
is Boolean -> putBoolean(k, v)
73-
is Int -> putInt(k, v)
74-
}
75-
}
76-
}
7749
}
7850

7951
enum class Monitor(val value: Int) {

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

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class DeviceAdminManager(private val ctx: Context) {
1414
fun remove() = dpm?.removeActiveAdmin(deviceAdmin)
1515
fun getCurrentFailedPasswordAttempts() = dpm?.currentFailedPasswordAttempts ?: 0
1616
fun isDeviceOwner() = dpm?.isDeviceOwnerApp(ctx.packageName) ?: false
17+
fun addUserRestriction(key: String) = dpm?.addUserRestriction(deviceAdmin, key)
18+
fun clearUserRestriction(key: String) = dpm?.clearUserRestriction(deviceAdmin, key)
19+
20+
@RequiresApi(Build.VERSION_CODES.N)
21+
fun getUserRestrictions() = dpm?.getUserRestrictions(deviceAdmin)
1722

1823
fun setMaximumFailedPasswordsForWipe(num: Int) =
1924
dpm?.setMaximumFailedPasswordsForWipe(deviceAdmin, num)

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

+5-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package me.lucky.sentry.admin
33
import android.app.admin.DeviceAdminReceiver
44
import android.content.Context
55
import android.content.Intent
6-
import android.os.Build
76
import android.os.UserHandle
8-
import android.os.UserManager
97

108
import me.lucky.sentry.Monitor
119
import me.lucky.sentry.NotificationManager
@@ -14,15 +12,15 @@ import me.lucky.sentry.Preferences
1412
class DeviceAdminReceiver : DeviceAdminReceiver() {
1513
override fun onPasswordFailed(context: Context, intent: Intent, user: UserHandle) {
1614
super.onPasswordFailed(context, intent, user)
17-
val prefs = Preferences(context, encrypted = Build.VERSION.SDK_INT < Build.VERSION_CODES.N
18-
|| context.getSystemService(UserManager::class.java)?.isUserUnlocked == true)
15+
val prefs = Preferences(context)
16+
if (!prefs.isEnabled) return
1917
if (prefs.monitor.and(Monitor.PASSWORD.value) != 0)
2018
NotificationManager(context).notifyPassword()
21-
if (prefs.isMaxFailedPasswordAttemptsWarningChecked) return
19+
if (prefs.isMaxFailedPasswordAttemptsDefaultApiChecked) return
2220
val maxFailedPasswordAttempts = prefs.maxFailedPasswordAttempts
23-
if (!prefs.isEnabled || maxFailedPasswordAttempts <= 0) return
21+
if (maxFailedPasswordAttempts <= 0) return
2422
val admin = DeviceAdminManager(context)
2523
if (admin.getCurrentFailedPasswordAttempts() >= maxFailedPasswordAttempts)
26-
try { admin.wipeData() } catch (exc: SecurityException) {}
24+
try { admin.wipeData() } catch (_: SecurityException) {}
2725
}
2826
}

app/src/main/java/me/lucky/sentry/fragment/MainFragment.kt

+18-33
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package me.lucky.sentry.fragment
22

33
import android.app.Activity
44
import android.content.Context
5-
import android.content.SharedPreferences
65
import android.os.Build
76
import android.os.Bundle
87
import android.view.LayoutInflater
@@ -22,7 +21,6 @@ class MainFragment : Fragment() {
2221
private lateinit var binding: FragmentMainBinding
2322
private lateinit var ctx: Context
2423
private lateinit var prefs: Preferences
25-
private lateinit var prefsdb: Preferences
2624
private val admin by lazy { DeviceAdminManager(ctx) }
2725

2826
override fun onCreateView(
@@ -38,24 +36,16 @@ class MainFragment : Fragment() {
3836

3937
override fun onStart() {
4038
super.onStart()
41-
prefs.registerListener(prefsListener)
4239
update()
4340
}
4441

45-
override fun onStop() {
46-
super.onStop()
47-
prefs.unregisterListener(prefsListener)
48-
}
49-
5042
private fun init() {
5143
ctx = requireContext()
5244
prefs = Preferences(ctx)
53-
prefsdb = Preferences(ctx, encrypted = false)
54-
prefs.copyTo(prefsdb)
5545
binding.apply {
5646
maxFailedPasswordAttempts.editText?.setText(prefs.maxFailedPasswordAttempts.toString())
57-
maxFailedPasswordAttemptsWarning.isChecked =
58-
prefs.isMaxFailedPasswordAttemptsWarningChecked
47+
maxFailedPasswordAttemptsDefaultApi.isChecked =
48+
prefs.isMaxFailedPasswordAttemptsDefaultApiChecked
5949
val canChangeUsbDataSignaling = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
6050
admin.canUsbDataSignalingBeDisabled() &&
6151
admin.isDeviceOwner()
@@ -69,21 +59,17 @@ class MainFragment : Fragment() {
6959

7060
private fun setup() = binding.apply {
7161
maxFailedPasswordAttempts.editText?.doAfterTextChanged {
72-
val i = it?.toString()?.toIntOrNull() ?: return@doAfterTextChanged
73-
prefs.maxFailedPasswordAttempts = i
74-
if (prefs.isMaxFailedPasswordAttemptsWarningChecked)
75-
try { admin.setMaximumFailedPasswordsForWipe(i) } catch (exc: SecurityException) {}
62+
prefs.maxFailedPasswordAttempts = it?.toString()?.toIntOrNull() ?:
63+
return@doAfterTextChanged
64+
setMaximumFailedPasswordAttempts()
7665
}
77-
maxFailedPasswordAttemptsWarning.setOnCheckedChangeListener { _, isChecked ->
78-
prefs.isMaxFailedPasswordAttemptsWarningChecked = isChecked
79-
try {
80-
admin.setMaximumFailedPasswordsForWipe(
81-
if (isChecked) prefs.maxFailedPasswordAttempts else 0)
82-
} catch (exc: SecurityException) {}
66+
maxFailedPasswordAttemptsDefaultApi.setOnCheckedChangeListener { _, isChecked ->
67+
prefs.isMaxFailedPasswordAttemptsDefaultApiChecked = isChecked
68+
setMaximumFailedPasswordAttempts()
8369
}
8470
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
8571
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return@setOnCheckedChangeListener
86-
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (exc: Exception) {
72+
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (_: Exception) {
8773
Snackbar.make(
8874
usbDataSignaling,
8975
R.string.usb_data_signaling_change_failed_popup,
@@ -101,19 +87,14 @@ class MainFragment : Fragment() {
10187
}
10288

10389
private fun setOn() {
104-
try {
105-
admin.setMaximumFailedPasswordsForWipe(
106-
if (prefs.isMaxFailedPasswordAttemptsWarningChecked) prefs.maxFailedPasswordAttempts
107-
else 0
108-
)
109-
} catch (exc: SecurityException) {}
90+
setMaximumFailedPasswordAttempts()
11091
prefs.isEnabled = true
11192
binding.toggle.isChecked = true
11293
}
11394

11495
private fun setOff() {
11596
prefs.isEnabled = false
116-
try { admin.remove() } catch (exc: SecurityException) {}
97+
try { admin.remove() } catch (_: SecurityException) {}
11798
binding.toggle.isChecked = false
11899
}
119100

@@ -128,7 +109,11 @@ class MainFragment : Fragment() {
128109
if (it.resultCode != Activity.RESULT_OK) setOff() else setOn()
129110
}
130111

131-
private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
132-
prefs.copyTo(prefsdb, key)
133-
}
112+
private fun setMaximumFailedPasswordAttempts() = try {
113+
admin.setMaximumFailedPasswordsForWipe(
114+
if (prefs.isMaxFailedPasswordAttemptsDefaultApiChecked)
115+
prefs.maxFailedPasswordAttempts
116+
else 0
117+
)
118+
} catch (_: SecurityException) {}
134119
}

0 commit comments

Comments
 (0)