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
This commit is contained in:
lucky 2022-01-03 02:33:01 +03:00
parent 547578d8e8
commit d64d290187
22 changed files with 316 additions and 110 deletions

View File

@ -8,11 +8,14 @@ Lock device and wipe data on panic trigger.
<img src="https://user-images.githubusercontent.com/53379023/147702625-7aef3d46-b819-40df-86e9-c3de418f2b58.png" width="30%" height="30%"> <img src="https://user-images.githubusercontent.com/53379023/147702625-7aef3d46-b819-40df-86e9-c3de418f2b58.png" width="30%" height="30%">
You can use [PanicKit](https://guardianproject.info/code/panickit/), Tile or send broadcast message You can use [PanicKit](https://guardianproject.info/code/panickit/), tile, shortcut or send
with authentication code. On trigger, using broadcast message with authentication code. On trigger, using
[Device Administration API](https://developer.android.com/guide/topics/admin/device-admin), it [Device Administration API](https://developer.android.com/guide/topics/admin/device-admin), it
locks device and optionally runs wipe. 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 ## Example
Broadcast message: Broadcast message:

View File

@ -4,8 +4,8 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 1.1.x | :white_check_mark: | | 1.2.x | :white_check_mark: |
| < 1.1 | :x: | | < 1.2 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -10,8 +10,8 @@ android {
applicationId "me.lucky.wasted" applicationId "me.lucky.wasted"
minSdk 23 minSdk 23
targetSdk 31 targetSdk 31
versionCode 10 versionCode 12
versionName "1.1.3" versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -65,6 +65,17 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ShortcutActivity"
android:noHistory="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="me.lucky.wasted.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service <service
android:name=".QSTileService" android:name=".QSTileService"
android:icon="@android:drawable/ic_lock_lock" android:icon="@android:drawable/ic_lock_lock"

View File

@ -6,20 +6,25 @@ import android.content.Intent
class CodeReceiver : BroadcastReceiver() { class CodeReceiver : BroadcastReceiver() {
companion object { companion object {
private const val TRIGGER = "me.lucky.wasted.action.TRIGGER" const val KEY = "code"
} const val ACTION = "me.lucky.wasted.action.TRIGGER"
override fun onReceive(context: Context, intent: Intent) { fun panic(context: Context, intent: Intent) {
val prefs by lazy { Preferences(context) } val prefs = Preferences(context)
val code = prefs.code val code = prefs.code
if (!prefs.isServiceEnabled || if (!prefs.isServiceEnabled ||
code == "" || code == "" ||
intent.action != TRIGGER || intent.action != ACTION ||
intent.getStringExtra("code") != code) return intent.getStringExtra(KEY) != code) return
val admin = DeviceAdmin(context) val admin = DeviceAdminManager(context)
try { try {
admin.dpm.lockNow() admin.lockNow()
if (prefs.doWipe) admin.wipeData() if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {} } catch (exc: SecurityException) {}
} }
}
override fun onReceive(context: Context, intent: Intent) {
panic(context, intent)
}
} }

View File

@ -1,21 +0,0 @@
package me.lucky.wasted
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.os.Build
class DeviceAdmin(ctx: Context) {
val dpm by lazy {
ctx.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
}
val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }
fun remove() = dpm.removeActiveAdmin(deviceAdmin)
fun isActive(): Boolean = dpm.isAdminActive(deviceAdmin)
fun wipeData() {
dpm.wipeData(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
DevicePolicyManager.WIPE_SILENTLY else 0)
}
}

View File

@ -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),
)
}
}

View File

@ -1,5 +1,30 @@
package me.lucky.wasted package me.lucky.wasted
import android.app.admin.DeviceAdminReceiver 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()
}
}

View File

@ -1,11 +1,12 @@
package me.lucky.wasted package me.lucky.wasted
import android.app.admin.DevicePolicyManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -17,7 +18,8 @@ open class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val prefs by lazy { Preferences(this) } 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 = private val requestAdminPolicy =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -51,23 +53,54 @@ open class MainActivity : AppCompatActivity() {
private fun init() { private fun init() {
if (prefs.code == "") prefs.code = makeCode() if (prefs.code == "") prefs.code = makeCode()
updateCodeColorState()
binding.apply { binding.apply {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) wipeESIM.visibility = View.GONE
code.text = prefs.code 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 toggle.isChecked = prefs.isServiceEnabled
} }
} }
private fun setup() { private fun setup() {
binding.apply { binding.apply {
code.setOnClickListener {
prefs.isCodeEnabled = !prefs.isCodeEnabled
updateCodeColorState()
setCodeReceiverState(
this@MainActivity,
prefs.isServiceEnabled && prefs.isCodeEnabled,
)
}
code.setOnLongClickListener { code.setOnLongClickListener {
prefs.code = makeCode() prefs.code = makeCode()
code.text = prefs.code code.text = prefs.code
true true
} }
wipeDataCheckBox.setOnCheckedChangeListener { _, isChecked -> wipeData.setOnCheckedChangeListener { _, isChecked ->
prefs.doWipe = 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 -> toggle.setOnCheckedChangeListener { _, isChecked ->
when (isChecked) { when (isChecked) {
true -> if (!admin.isActive()) requestAdmin() else setOn() 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() { private fun setOn() {
prefs.isServiceEnabled = true prefs.isServiceEnabled = true
setControlReceiverState(this, true) setCodeReceiverState(this, prefs.isCodeEnabled)
shortcut.push()
} }
private fun setOff() { private fun setOff() {
admin.remove() admin.remove()
setControlReceiverState(this, false) setCodeReceiverState(this, false)
shortcut.remove()
prefs.isServiceEnabled = false prefs.isServiceEnabled = false
} }
private fun requestAdmin() { private fun requestAdmin() = requestAdminPolicy.launch(admin.makeRequestIntent())
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 makeCode(): String = UUID.randomUUID().toString() private fun makeCode(): String = UUID.randomUUID().toString()
private fun setControlReceiverState(ctx: Context, value: Boolean) { private fun setCodeReceiverState(ctx: Context, value: Boolean) {
ctx.packageManager.setComponentEnabledSetting( ctx.packageManager.setComponentEnabledSetting(
ComponentName(ctx, CodeReceiver::class.java), ComponentName(ctx, CodeReceiver::class.java),
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else

View File

@ -31,18 +31,17 @@ class PanicConnectionActivity : MainActivity() {
} catch (exc: PackageManager.NameNotFoundException) {} } catch (exc: PackageManager.NameNotFoundException) {}
} }
AlertDialog.Builder(this).apply { AlertDialog.Builder(this)
setTitle(getString(R.string.panic_app_dialog_title)) .setTitle(getString(R.string.panic_app_dialog_title))
setMessage(String.format(getString(R.string.panic_app_dialog_message), app)) .setMessage(String.format(getString(R.string.panic_app_dialog_message), app))
setNegativeButton(R.string.allow) { _, _ -> .setNegativeButton(R.string.allow) { _, _ ->
PanicResponder.setTriggerPackageName(this@PanicConnectionActivity) PanicResponder.setTriggerPackageName(this@PanicConnectionActivity)
setResult(RESULT_OK) setResult(RESULT_OK)
} }
setPositiveButton(R.string.cancel) { _, _ -> .setPositiveButton(R.string.cancel) { _, _ ->
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
} }
show() .show()
}
} }
} }

View File

@ -8,19 +8,19 @@ import info.guardianproject.panic.PanicResponder
class PanicResponderActivity : AppCompatActivity() { class PanicResponderActivity : AppCompatActivity() {
private val prefs by lazy { Preferences(this) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!Panic.isTriggerIntent(intent) || !prefs.isServiceEnabled) { if (!Panic.isTriggerIntent(intent) || !prefs.isServiceEnabled) {
finish() finishAndRemoveTask()
return return
} }
try { try {
admin.dpm.lockNow() admin.lockNow()
if (PanicResponder.receivedTriggerFromConnectedApp(this) && if (PanicResponder.receivedTriggerFromConnectedApp(this) &&
prefs.doWipe) admin.wipeData() prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {} } catch (exc: SecurityException) {}
finish() finishAndRemoveTask()
} }
} }

View File

@ -9,7 +9,10 @@ class Preferences(ctx: Context) {
companion object { companion object {
private const val SERVICE_ENABLED = "service_enabled" private const val SERVICE_ENABLED = "service_enabled"
private const val CODE = "code" 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) private val mk = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
@ -29,7 +32,19 @@ class Preferences(ctx: Context) {
get() = prefs.getString(CODE, "") get() = prefs.getString(CODE, "")
set(value) = prefs.edit { putString(CODE, value) } set(value) = prefs.edit { putString(CODE, value) }
var doWipe: Boolean var isCodeEnabled: Boolean
get() = prefs.getBoolean(DO_WIPE, false) get() = prefs.getBoolean(CODE_ENABLED, false)
set(value) = prefs.edit { putBoolean(DO_WIPE, value) } 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) }
} }

View File

@ -11,7 +11,7 @@ import kotlin.concurrent.timerTask
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
class QSTileService : TileService() { class QSTileService : TileService() {
private val prefs by lazy { Preferences(this) } 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 val counter = AtomicInteger(0)
private var timer: Timer? = null private var timer: Timer? = null
@ -26,9 +26,9 @@ class QSTileService : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
if (!prefs.isServiceEnabled) return if (!prefs.isServiceEnabled) return
if (!prefs.doWipe) { if (!prefs.isWipeData) {
try { try {
admin.dpm.lockNow() admin.lockNow()
} catch (exc: SecurityException) {} } catch (exc: SecurityException) {}
return return
} }
@ -39,7 +39,7 @@ class QSTileService : TileService() {
timer = Timer() timer = Timer()
timer?.schedule(timerTask { timer?.schedule(timerTask {
try { try {
admin.dpm.lockNow() admin.lockNow()
admin.wipeData() admin.wipeData()
} catch (exc: SecurityException) {} } catch (exc: SecurityException) {}
}, 2000) }, 2000)
@ -53,9 +53,7 @@ class QSTileService : TileService() {
} }
private fun update(tileState: Int) { private fun update(tileState: Int) {
qsTile.apply { qsTile.state = tileState
state = tileState qsTile.updateTile()
updateTile()
}
} }
} }

View File

@ -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()
}
}

View File

@ -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))
}

View File

@ -16,20 +16,75 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginVertical="16dp"
app:layout_constraintBottom_toTopOf="@+id/toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/description">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/code" android:id="@+id/code"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:background="@color/code_receiver_off"
android:background="#FFD600"
android:padding="16dp" android:padding="16dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" <CheckBox
app:layout_constraintTop_toBottomOf="@+id/description" /> android:id="@+id/wipeData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_data_check_box"
android:textSize="16sp" />
<CheckBox
android:id="@+id/wipeESIM"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:layoutDirection="rtl"
android:text="@string/wipe_esim_check_box"
android:textSize="16sp" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="16dp"
android:background="?android:attr/listDivider" />
<TextView
android:id="@+id/description2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/max_failed_password_attempts_description" />
<SeekBar
android:id="@+id/maxFailedPasswordAttempts"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:max="10"
android:progress="0" />
</LinearLayout>
</ScrollView>
<ToggleButton <ToggleButton
android:id="@+id/toggle" android:id="@+id/toggle"
@ -41,16 +96,4 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<CheckBox
android:id="@+id/wipeDataCheckBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_data_check_box"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -7,4 +7,6 @@
<color name="teal_700">#FF018786</color> <color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="code_receiver_on">#FFD600</color>
<color name="code_receiver_off">#E13741</color>
</resources> </resources>

View File

@ -1,14 +1,17 @@
<resources> <resources>
<string name="app_name">Wasted</string> <string name="app_name">Wasted</string>
<string name="description">Turn on Wasted to lock device on panic trigger. You can use PanicKit, Tile or send broadcast message with this authentication code.</string> <string name="description">Turn on Wasted to lock device on panic trigger. You can use PanicKit, tile, shortcut or send broadcast message with this authentication code.</string>
<string name="device_admin_label">Wasted</string> <string name="device_admin_label">Wasted</string>
<string name="device_admin_description">Allow Wasted to lock device and optionally wipe data on panic trigger</string> <string name="device_admin_description">Allow Wasted to lock device and optionally wipe data on panic trigger</string>
<string name="service_unavailable_toast">Admin service unavailable</string> <string name="service_unavailable_toast">Admin service unavailable</string>
<string name="wipe_data_check_box">Wipe data</string> <string name="wipe_data_check_box">Wipe data</string>
<string name="panic_app_dialog_title">Confirm Panic App</string> <string name="wipe_esim_check_box">Wipe eSIM</string>
<string name="panic_app_dialog_title">Confirm panic app</string>
<string name="panic_app_dialog_message">Are you sure that you want to allow %1$s to trigger destructive panic actions\?</string> <string name="panic_app_dialog_message">Are you sure that you want to allow %1$s to trigger destructive panic actions\?</string>
<string name="panic_app_unknown_app">an unknown app</string> <string name="panic_app_unknown_app">an unknown app</string>
<string name="allow">Allow</string> <string name="allow">Allow</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="tile_label">Unlock device</string> <string name="tile_label">Unlock device</string>
<string name="shortcut_label">Panic</string>
<string name="max_failed_password_attempts_description">Also you can limit the maximum number of failed password attempts. After the limit exceeded device will be wiped.</string>
</resources> </resources>

View File

@ -2,6 +2,7 @@
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies> <uses-policies>
<force-lock /> <force-lock />
<watch-login />
<wipe-data /> <wipe-data />
</uses-policies> </uses-policies>
</device-admin> </device-admin>

View File

@ -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

View File

@ -1,4 +1,7 @@
Lock device and wipe data on panic trigger. 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. 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 105 KiB