diff --git a/README.md b/README.md
index 62f03d0..5d68d1e 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,14 @@ Lock device and wipe data on panic trigger.
-You can use [PanicKit](https://guardianproject.info/code/panickit/), Tile or send broadcast message
-with authentication code. On trigger, using
+You can use [PanicKit](https://guardianproject.info/code/panickit/), tile, shortcut or send
+broadcast message with authentication code. On trigger, using
[Device Administration API](https://developer.android.com/guide/topics/admin/device-admin), it
locks device and optionally runs wipe.
+Also you can limit the maximum number of failed password attempts. After the limit exceeded
+device will be wiped.
+
## Example
Broadcast message:
diff --git a/SECURITY.md b/SECURITY.md
index 1dc2545..d8e03f2 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
-| 1.1.x | :white_check_mark: |
-| < 1.1 | :x: |
+| 1.2.x | :white_check_mark: |
+| < 1.2 | :x: |
## Reporting a Vulnerability
diff --git a/app/build.gradle b/app/build.gradle
index 78f20c4..697a948 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId "me.lucky.wasted"
minSdk 23
targetSdk 31
- versionCode 10
- versionName "1.1.3"
+ versionCode 12
+ versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e745f7d..352730d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -65,6 +65,17 @@
+
+
+
+
+
+
+
= Build.VERSION_CODES.Q)
- DevicePolicyManager.WIPE_SILENTLY else 0)
- }
-}
diff --git a/app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt b/app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt
new file mode 100644
index 0000000..fa1385f
--- /dev/null
+++ b/app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt
@@ -0,0 +1,40 @@
+package me.lucky.wasted
+
+import android.app.admin.DevicePolicyManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+
+class DeviceAdminManager(private val ctx: Context) {
+ private val dpm by lazy {
+ ctx.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
+ }
+ private val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }
+ private val prefs by lazy { Preferences(ctx) }
+
+ fun remove() = dpm.removeActiveAdmin(deviceAdmin)
+ fun isActive(): Boolean = dpm.isAdminActive(deviceAdmin)
+ fun getCurrentFailedPasswordAttempts(): Int = dpm.currentFailedPasswordAttempts
+ fun lockNow() = dpm.lockNow()
+
+ fun wipeData() {
+ var flags = 0
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ flags = flags.or(DevicePolicyManager.WIPE_SILENTLY)
+ }
+ if (prefs.isWipeESIM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ flags = flags.or(DevicePolicyManager.WIPE_EUICC)
+ }
+ dpm.wipeData(flags)
+ }
+
+ fun makeRequestIntent(): Intent {
+ return Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
+ .putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdmin)
+ .putExtra(
+ DevicePolicyManager.EXTRA_ADD_EXPLANATION,
+ ctx.getString(R.string.device_admin_description),
+ )
+ }
+}
diff --git a/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt b/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt
index b8068c4..993c10f 100644
--- a/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt
+++ b/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt
@@ -1,5 +1,30 @@
package me.lucky.wasted
import android.app.admin.DeviceAdminReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.UserHandle
+import androidx.annotation.RequiresApi
-class DeviceAdminReceiver : DeviceAdminReceiver()
+class DeviceAdminReceiver : DeviceAdminReceiver() {
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onPasswordFailed(context: Context, intent: Intent, user: UserHandle) {
+ super.onPasswordFailed(context, intent, user)
+ onPasswordFailedInternal(context)
+ }
+
+ override fun onPasswordFailed(context: Context, intent: Intent) {
+ super.onPasswordFailed(context, intent)
+ onPasswordFailedInternal(context)
+ }
+
+ private fun onPasswordFailedInternal(ctx: Context) {
+ val prefs = Preferences(ctx)
+ if (!prefs.isServiceEnabled || prefs.maxFailedPasswordAttempts == 0) return
+ val admin = DeviceAdminManager(ctx)
+ if (admin.getCurrentFailedPasswordAttempts() >= prefs.maxFailedPasswordAttempts)
+ admin.wipeData()
+ }
+}
diff --git a/app/src/main/java/me/lucky/wasted/MainActivity.kt b/app/src/main/java/me/lucky/wasted/MainActivity.kt
index 23b621e..29a6c07 100644
--- a/app/src/main/java/me/lucky/wasted/MainActivity.kt
+++ b/app/src/main/java/me/lucky/wasted/MainActivity.kt
@@ -1,11 +1,12 @@
package me.lucky.wasted
-import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
-import android.content.Intent
import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.view.View
+import android.widget.SeekBar
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
@@ -17,7 +18,8 @@ open class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val prefs by lazy { Preferences(this) }
- private val admin by lazy { DeviceAdmin(this) }
+ private val admin by lazy { DeviceAdminManager(this) }
+ private val shortcut by lazy { ShortcutManager(this) }
private val requestAdminPolicy =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -51,23 +53,54 @@ open class MainActivity : AppCompatActivity() {
private fun init() {
if (prefs.code == "") prefs.code = makeCode()
+ updateCodeColorState()
binding.apply {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) wipeESIM.visibility = View.GONE
code.text = prefs.code
- wipeDataCheckBox.isChecked = prefs.doWipe
+ wipeData.isChecked = prefs.isWipeData
+ wipeESIM.isChecked = prefs.isWipeESIM
+ wipeESIM.isEnabled = wipeData.isChecked
+ maxFailedPasswordAttempts.progress = prefs.maxFailedPasswordAttempts
toggle.isChecked = prefs.isServiceEnabled
}
}
private fun setup() {
binding.apply {
+ code.setOnClickListener {
+ prefs.isCodeEnabled = !prefs.isCodeEnabled
+ updateCodeColorState()
+ setCodeReceiverState(
+ this@MainActivity,
+ prefs.isServiceEnabled && prefs.isCodeEnabled,
+ )
+ }
code.setOnLongClickListener {
prefs.code = makeCode()
code.text = prefs.code
true
}
- wipeDataCheckBox.setOnCheckedChangeListener { _, isChecked ->
- prefs.doWipe = isChecked
+ wipeData.setOnCheckedChangeListener { _, isChecked ->
+ prefs.isWipeData = isChecked
+ wipeESIM.isEnabled = isChecked
}
+ wipeESIM.setOnCheckedChangeListener { _, isChecked ->
+ prefs.isWipeESIM = isChecked
+ }
+ maxFailedPasswordAttempts.setOnSeekBarChangeListener(
+ object : SeekBar.OnSeekBarChangeListener {
+
+ override fun onProgressChanged(
+ seekBar: SeekBar?,
+ progress: Int,
+ fromUser: Boolean,
+ ) {
+ prefs.maxFailedPasswordAttempts = progress
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {}
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {}
+ })
toggle.setOnCheckedChangeListener { _, isChecked ->
when (isChecked) {
true -> if (!admin.isActive()) requestAdmin() else setOn()
@@ -77,31 +110,29 @@ open class MainActivity : AppCompatActivity() {
}
}
+ private fun updateCodeColorState() {
+ binding.code.setBackgroundColor(getColor(
+ if (prefs.isCodeEnabled) R.color.code_receiver_on else R.color.code_receiver_off
+ ))
+ }
+
private fun setOn() {
prefs.isServiceEnabled = true
- setControlReceiverState(this, true)
+ setCodeReceiverState(this, prefs.isCodeEnabled)
+ shortcut.push()
}
private fun setOff() {
admin.remove()
- setControlReceiverState(this, false)
+ setCodeReceiverState(this, false)
+ shortcut.remove()
prefs.isServiceEnabled = false
}
- private fun requestAdmin() {
- val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
- putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, admin.deviceAdmin)
- putExtra(
- DevicePolicyManager.EXTRA_ADD_EXPLANATION,
- getString(R.string.device_admin_description),
- )
- }
- requestAdminPolicy.launch(intent)
- }
-
+ private fun requestAdmin() = requestAdminPolicy.launch(admin.makeRequestIntent())
private fun makeCode(): String = UUID.randomUUID().toString()
- private fun setControlReceiverState(ctx: Context, value: Boolean) {
+ private fun setCodeReceiverState(ctx: Context, value: Boolean) {
ctx.packageManager.setComponentEnabledSetting(
ComponentName(ctx, CodeReceiver::class.java),
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else
diff --git a/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt b/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt
index 045c73e..eaabf83 100644
--- a/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt
+++ b/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt
@@ -31,18 +31,17 @@ class PanicConnectionActivity : MainActivity() {
} catch (exc: PackageManager.NameNotFoundException) {}
}
- AlertDialog.Builder(this).apply {
- setTitle(getString(R.string.panic_app_dialog_title))
- setMessage(String.format(getString(R.string.panic_app_dialog_message), app))
- setNegativeButton(R.string.allow) { _, _ ->
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.panic_app_dialog_title))
+ .setMessage(String.format(getString(R.string.panic_app_dialog_message), app))
+ .setNegativeButton(R.string.allow) { _, _ ->
PanicResponder.setTriggerPackageName(this@PanicConnectionActivity)
setResult(RESULT_OK)
}
- setPositiveButton(R.string.cancel) { _, _ ->
+ .setPositiveButton(R.string.cancel) { _, _ ->
setResult(RESULT_CANCELED)
finish()
}
- show()
- }
+ .show()
}
}
diff --git a/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt b/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt
index 7950436..830ee62 100644
--- a/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt
+++ b/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt
@@ -8,19 +8,19 @@ import info.guardianproject.panic.PanicResponder
class PanicResponderActivity : AppCompatActivity() {
private val prefs by lazy { Preferences(this) }
- private val admin by lazy { DeviceAdmin(this) }
+ private val admin by lazy { DeviceAdminManager(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!Panic.isTriggerIntent(intent) || !prefs.isServiceEnabled) {
- finish()
+ finishAndRemoveTask()
return
}
try {
- admin.dpm.lockNow()
+ admin.lockNow()
if (PanicResponder.receivedTriggerFromConnectedApp(this) &&
- prefs.doWipe) admin.wipeData()
+ prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
- finish()
+ finishAndRemoveTask()
}
}
diff --git a/app/src/main/java/me/lucky/wasted/Preferences.kt b/app/src/main/java/me/lucky/wasted/Preferences.kt
index 7749f70..a7d878a 100644
--- a/app/src/main/java/me/lucky/wasted/Preferences.kt
+++ b/app/src/main/java/me/lucky/wasted/Preferences.kt
@@ -9,7 +9,10 @@ class Preferences(ctx: Context) {
companion object {
private const val SERVICE_ENABLED = "service_enabled"
private const val CODE = "code"
- private const val DO_WIPE = "do_wipe"
+ private const val CODE_ENABLED = "code_enabled"
+ private const val WIPE_DATA = "wipe_data"
+ private const val WIPE_ESIM = "wipe_esim"
+ private const val MAX_FAILED_PASSWORD_ATTEMPTS = "max_failed_password_attempts"
}
private val mk = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
@@ -29,7 +32,19 @@ class Preferences(ctx: Context) {
get() = prefs.getString(CODE, "")
set(value) = prefs.edit { putString(CODE, value) }
- var doWipe: Boolean
- get() = prefs.getBoolean(DO_WIPE, false)
- set(value) = prefs.edit { putBoolean(DO_WIPE, value) }
+ var isCodeEnabled: Boolean
+ get() = prefs.getBoolean(CODE_ENABLED, false)
+ set(value) = prefs.edit { putBoolean(CODE_ENABLED, value) }
+
+ var isWipeData: Boolean
+ get() = prefs.getBoolean(WIPE_DATA, false)
+ set(value) = prefs.edit { putBoolean(WIPE_DATA, value) }
+
+ var isWipeESIM: Boolean
+ get() = prefs.getBoolean(WIPE_ESIM, false)
+ set(value) = prefs.edit { putBoolean(WIPE_ESIM, value) }
+
+ var maxFailedPasswordAttempts: Int
+ get() = prefs.getInt(MAX_FAILED_PASSWORD_ATTEMPTS, 0)
+ set(value) = prefs.edit { putInt(MAX_FAILED_PASSWORD_ATTEMPTS, value) }
}
diff --git a/app/src/main/java/me/lucky/wasted/QSTileService.kt b/app/src/main/java/me/lucky/wasted/QSTileService.kt
index e792ad5..9e901fc 100644
--- a/app/src/main/java/me/lucky/wasted/QSTileService.kt
+++ b/app/src/main/java/me/lucky/wasted/QSTileService.kt
@@ -11,7 +11,7 @@ import kotlin.concurrent.timerTask
@RequiresApi(Build.VERSION_CODES.N)
class QSTileService : TileService() {
private val prefs by lazy { Preferences(this) }
- private val admin by lazy { DeviceAdmin(this) }
+ private val admin by lazy { DeviceAdminManager(this) }
private val counter = AtomicInteger(0)
private var timer: Timer? = null
@@ -26,9 +26,9 @@ class QSTileService : TileService() {
override fun onClick() {
super.onClick()
if (!prefs.isServiceEnabled) return
- if (!prefs.doWipe) {
+ if (!prefs.isWipeData) {
try {
- admin.dpm.lockNow()
+ admin.lockNow()
} catch (exc: SecurityException) {}
return
}
@@ -39,7 +39,7 @@ class QSTileService : TileService() {
timer = Timer()
timer?.schedule(timerTask {
try {
- admin.dpm.lockNow()
+ admin.lockNow()
admin.wipeData()
} catch (exc: SecurityException) {}
}, 2000)
@@ -53,9 +53,7 @@ class QSTileService : TileService() {
}
private fun update(tileState: Int) {
- qsTile.apply {
- state = tileState
- updateTile()
- }
+ qsTile.state = tileState
+ qsTile.updateTile()
}
}
diff --git a/app/src/main/java/me/lucky/wasted/ShortcutActivity.kt b/app/src/main/java/me/lucky/wasted/ShortcutActivity.kt
new file mode 100644
index 0000000..1f663e8
--- /dev/null
+++ b/app/src/main/java/me/lucky/wasted/ShortcutActivity.kt
@@ -0,0 +1,12 @@
+package me.lucky.wasted
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+class ShortcutActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ CodeReceiver.panic(this, intent)
+ finishAndRemoveTask()
+ }
+}
diff --git a/app/src/main/java/me/lucky/wasted/ShortcutManager.kt b/app/src/main/java/me/lucky/wasted/ShortcutManager.kt
new file mode 100644
index 0000000..94d1362
--- /dev/null
+++ b/app/src/main/java/me/lucky/wasted/ShortcutManager.kt
@@ -0,0 +1,32 @@
+package me.lucky.wasted
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+
+class ShortcutManager(private val ctx: Context) {
+ companion object {
+ private const val SHORTCUT_ID = "panic"
+ }
+
+ private val prefs by lazy { Preferences(ctx) }
+
+ fun push() {
+ ShortcutManagerCompat.pushDynamicShortcut(
+ ctx,
+ ShortcutInfoCompat.Builder(ctx, SHORTCUT_ID)
+ .setShortLabel(ctx.getString(R.string.shortcut_label))
+ .setIcon(IconCompat.createWithResource(ctx, android.R.drawable.ic_delete))
+ .setIntent(
+ Intent(CodeReceiver.ACTION)
+ .setClass(ctx, ShortcutActivity::class.java)
+ .putExtra(CodeReceiver.KEY, prefs.code)
+ )
+ .build(),
+ )
+ }
+
+ fun remove() = ShortcutManagerCompat.removeDynamicShortcuts(ctx, arrayListOf(SHORTCUT_ID))
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index ccafcf0..0d95510 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -16,20 +16,75 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
+ app:layout_constraintTop_toBottomOf="@+id/description">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f8c6127..ded1e85 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,6 @@
#FF018786
#FF000000
#FFFFFFFF
+ #FFD600
+ #E13741
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3b5e521..d101e22 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,14 +1,17 @@
Wasted
- Turn on Wasted to lock device on panic trigger. You can use PanicKit, Tile or send broadcast message with this authentication code.
+ Turn on Wasted to lock device on panic trigger. You can use PanicKit, tile, shortcut or send broadcast message with this authentication code.
Wasted
Allow Wasted to lock device and optionally wipe data on panic trigger
Admin service unavailable
Wipe data
- Confirm Panic App
+ Wipe eSIM
+ Confirm panic app
Are you sure that you want to allow %1$s to trigger destructive panic actions\?
an unknown app
Allow
Cancel
Unlock device
+ Panic
+ Also you can limit the maximum number of failed password attempts. After the limit exceeded device will be wiped.
diff --git a/app/src/main/res/xml/device_admin.xml b/app/src/main/res/xml/device_admin.xml
index 37b37b2..91b3993 100644
--- a/app/src/main/res/xml/device_admin.xml
+++ b/app/src/main/res/xml/device_admin.xml
@@ -2,6 +2,7 @@
+
diff --git a/fastlane/metadata/android/en-US/changelogs/12.txt b/fastlane/metadata/android/en-US/changelogs/12.txt
new file mode 100644
index 0000000..775dd09
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/12.txt
@@ -0,0 +1,4 @@
+add option to wipe eSIM
+add option to limit the maximum number of failed password attempts
+add option to disable code receiver
+add app panic shortcut
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index b42677a..0875e7b 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1,4 +1,7 @@
Lock device and wipe data on panic trigger.
-You can use PanicKit, Tile or send broadcast message with authentication code.
+You can use PanicKit, tile, shortcut or send broadcast message with authentication code.
On trigger, using Device Administration API, it locks device and optionally runs wipe.
+
+Also you can limit the maximum number of failed password attempts.
+After the limit exceeded device will be wiped.
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index e335dea..bdcb99f 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ