usb trigger

This commit is contained in:
lucky 2022-07-07 03:15:25 +03:00
parent 31b343b7d8
commit 3876640954
57 changed files with 1193 additions and 758 deletions

View File

@ -17,14 +17,14 @@ Lock a device and wipe its data on emergency.
height="30%">
You can use [PanicKit](https://guardianproject.info/code/panickit/), tile, shortcut or send a
message with authentication code. On trigger, using
message with a secret code. On trigger, using
[Device Administration API](https://developer.android.com/guide/topics/admin/device-admin), it
locks a device and optionally runs wipe.
Also you can:
* wipe a device when it was not unlocked for N time
* wipe a device using a duress password (companion app: [Duress](https://github.com/x13a/Duress))
* wipe a device on USB connection event (companion app: [USBKill](https://github.com/x13a/USBKill))
* fire when a device was not unlocked for N time
* fire when a USB data connection is made while a device is locked
* fire when a duress password is entered (companion app: [Duress](https://github.com/x13a/Duress))
The app works in `Work Profile` too. Use [Shelter](https://github.com/PeterCxy/Shelter) to install
risky apps and `Wasted` in it. Then you can wipe this profile data with one click without wiping
@ -34,9 +34,9 @@ Only encrypted device may guarantee that the data will not be recoverable.
## Permissions
* DEVICE_ADMIN - lock and optionally wipe a device
* FOREGROUND_SERVICE - receive unlock events
* RECEIVE_BOOT_COMPLETED - persist wipe job across reboots
* DEVICE_ADMIN - lock and optionally wipe a device
* FOREGROUND_SERVICE - receive lock and USB state events
* RECEIVE_BOOT_COMPLETED - persist lock job and foreground service across reboots
## Example

View File

@ -10,8 +10,8 @@ android {
applicationId "me.lucky.wasted"
minSdk 23
targetSdk 32
versionCode 28
versionName "1.4.3"
versionCode 29
versionName "1.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-feature android:name="android.software.device_admin" android:required="true" />
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<uses-feature android:name="android.hardware.usb.accessory" android:required="false" />
<application
android:allowBackup="false"
@ -29,7 +31,7 @@
</activity>
<receiver
android:name=".DeviceAdminReceiver"
android:name=".admin.DeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<meta-data android:name="android.app.device_admin"
@ -39,32 +41,8 @@
</intent-filter>
</receiver>
<receiver
android:name=".TriggerReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="me.lucky.wasted.action.TRIGGER" />
</intent-filter>
</receiver>
<activity
android:name=".PanicResponderActivity"
android:directBootAware="true"
android:noHistory="true"
android:enabled="false"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".PanicConnectionActivity"
android:name=".trigger.panic.PanicConnectionActivity"
android:noHistory="true"
android:enabled="false"
android:exported="true">
@ -76,18 +54,20 @@
</activity>
<activity
android:name=".ShortcutActivity"
android:name=".trigger.panic.PanicResponderActivity"
android:directBootAware="true"
android:noHistory="true"
android:enabled="false"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="me.lucky.wasted.action.TRIGGER" />
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".TileService"
android:name=".trigger.tile.TileService"
android:directBootAware="true"
android:icon="@drawable/ic_baseline_airplanemode_active_24"
android:label="@string/tile_label"
@ -102,21 +82,70 @@
android:value="true" />
</service>
<activity
android:name=".trigger.shortcut.ShortcutActivity"
android:noHistory="true"
android:enabled="false"
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>
<receiver
android:name=".TriggerReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="me.lucky.wasted.action.TRIGGER" />
</intent-filter>
</receiver>
<service
android:name=".WipeJobService"
android:name=".trigger.notification.NotificationListenerService"
android:directBootAware="true"
android:enabled="false"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".trigger.lock.LockJobService"
android:directBootAware="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
<receiver
android:name=".trigger.usb.UsbReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
android:resource="@xml/accessory_filter" />
</receiver>
<service
android:name=".ForegroundService"
android:name=".trigger.shared.ForegroundService"
android:directBootAware="true"
android:exported="false">
</service>
<receiver
android:name=".RestartReceiver"
android:name=".trigger.shared.RestartReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true">
@ -127,16 +156,5 @@
</intent-filter>
</receiver>
<service
android:name=".NotificationListenerService"
android:directBootAware="true"
android:enabled="false"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -3,10 +3,9 @@ package me.lucky.wasted
import android.app.Application
import com.google.android.material.color.DynamicColors
@Suppress("unused")
class Application : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}
}

View File

@ -1,14 +0,0 @@
package me.lucky.wasted
import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class DeviceAdminReceiver : DeviceAdminReceiver() {
override fun onDisabled(context: Context, intent: Intent) {
super.onDisabled(context, intent)
if (Preferences(context).isEnabled)
Toast.makeText(context, R.string.service_unavailable_popup, Toast.LENGTH_SHORT).show()
}
}

View File

@ -1,93 +0,0 @@
package me.lucky.wasted
import android.app.KeyguardManager
import android.app.Service
import android.app.job.JobScheduler
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import androidx.core.app.NotificationCompat
class ForegroundService : Service() {
companion object {
private const val NOTIFICATION_ID = 1000
}
private val lockReceiver = LockReceiver()
override fun onCreate() {
super.onCreate()
init()
}
override fun onDestroy() {
super.onDestroy()
deinit()
}
private fun init() {
registerReceiver(lockReceiver, IntentFilter().apply {
addAction(Intent.ACTION_USER_PRESENT)
addAction(Intent.ACTION_SCREEN_OFF)
})
}
private fun deinit() {
unregisterReceiver(lockReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startForeground(
NOTIFICATION_ID,
NotificationCompat.Builder(this, NotificationManager.CHANNEL_DEFAULT_ID)
.setContentTitle(getString(R.string.foreground_service_notification_title))
.setSmallIcon(android.R.drawable.ic_delete)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
)
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private class LockReceiver : BroadcastReceiver() {
private var unlocked = false
override fun onReceive(context: Context?, intent: Intent?) {
if (!Preferences.new(context ?: return).isWipeOnInactivity) return
when (intent?.action) {
Intent.ACTION_USER_PRESENT -> {
if (context.getSystemService(KeyguardManager::class.java)
?.isDeviceSecure != true) return
unlocked = true
WipeJobManager(context).cancel()
}
Intent.ACTION_SCREEN_OFF -> {
if (!unlocked) return
unlocked = false
Thread(Runner(context, goAsync())).start()
}
}
}
private class Runner(
private val ctx: Context,
private val pendingResult: PendingResult,
) : Runnable {
override fun run() {
val job = WipeJobManager(ctx)
var delay = 1000L
while (job.schedule() != JobScheduler.RESULT_SUCCESS) {
Thread.sleep(delay)
delay = delay.shl(1)
}
pendingResult.finish()
}
}
}
}

View File

@ -1,55 +1,23 @@
package me.lucky.wasted
import android.content.*
import android.content.pm.PackageManager
import android.os.Build
import android.content.SharedPreferences
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import java.util.*
import java.util.regex.Pattern
import kotlin.concurrent.timerTask
import androidx.fragment.app.Fragment
import me.lucky.wasted.databinding.ActivityMainBinding
import me.lucky.wasted.fragment.*
import me.lucky.wasted.trigger.shared.NotificationManager
open class MainActivity : AppCompatActivity() {
companion object {
private const val CLIPBOARD_CLEAR_DELAY = 30_000L
private const val MODIFIER_DAYS = 'd'
private const val MODIFIER_HOURS = 'h'
private const val MODIFIER_MINUTES = 'm'
}
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: Preferences
private lateinit var prefsdb: Preferences
private lateinit var admin: DeviceAdminManager
private val shortcut by lazy { ShortcutManager(this) }
private val job by lazy { WipeJobManager(this) }
private val wipeOnInactivityTimeRegex by lazy {
Pattern.compile("^[1-9]\\d*[$MODIFIER_DAYS$MODIFIER_HOURS$MODIFIER_MINUTES]$") }
private var clipboardManager: ClipboardManager? = null
private var clipboardClearTask: Timer? = null
private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
prefs.copyTo(prefsdb, key)
}
private val registerForDeviceAdmin =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
when (it.resultCode) {
RESULT_OK -> setOn()
else -> binding.toggle.isChecked = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@ -58,260 +26,62 @@ open class MainActivity : AppCompatActivity() {
setup()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return super.onCreateOptionsMenu(menu)
private fun init() {
prefs = Preferences(this)
prefsdb = Preferences(this, encrypted = false)
prefs.copyTo(prefsdb)
NotificationManager(this).createNotificationChannels()
replaceFragment(MainFragment())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.triggers) showTriggers()
return super.onOptionsItemSelected(item)
private fun setup() {
binding.apply {
appBar.setNavigationOnClickListener {
drawer.open()
}
appBar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.top_settings -> {
replaceFragment(when (supportFragmentManager.fragments.last()) {
is SettingsFragment ->
getFragment(navigation.checkedItem?.itemId ?: R.id.nav_main)
else -> SettingsFragment()
})
true
}
else -> false
}
}
navigation.setNavigationItemSelectedListener {
replaceFragment(getFragment(it.itemId))
it.isChecked = true
drawer.close()
true
}
}
}
private fun replaceFragment(f: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(binding.fragment.id, f)
.commit()
}
private fun getFragment(id: Int) = when (id) {
R.id.nav_main -> MainFragment()
R.id.nav_trigger_lock -> LockFragment()
R.id.top_settings -> SettingsFragment()
else -> MainFragment()
}
override fun onStart() {
super.onStart()
prefs.registerListener(prefsListener)
update()
}
override fun onStop() {
super.onStop()
prefs.unregisterListener(prefsListener)
}
private fun update() {
if (prefs.isEnabled && !admin.isActive())
Snackbar.make(
binding.toggle,
R.string.service_unavailable_popup,
Snackbar.LENGTH_SHORT,
).show()
}
private fun init() {
prefs = Preferences(this)
prefsdb = Preferences(this, encrypted = false)
prefs.copyTo(prefsdb)
admin = DeviceAdminManager(this)
clipboardManager = getSystemService(ClipboardManager::class.java)
NotificationManager(this).createNotificationChannels()
if (prefs.authenticationCode.isEmpty()) prefs.authenticationCode = makeAuthenticationCode()
updateCodeColorState()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) hideEmbeddedSim()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
!packageManager.hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN))
hideSecureLockScreenRequired()
binding.apply {
authenticationCode.text = prefs.authenticationCode
wipeData.isChecked = prefs.isWipeData
wipeEmbeddedSim.isChecked = prefs.isWipeEmbeddedSim
wipeEmbeddedSim.isEnabled = wipeData.isChecked
wipeOnInactivitySwitch.isChecked = prefs.isWipeOnInactivity
toggle.isChecked = prefs.isEnabled
}
initWipeOnInactivityTime()
}
private fun hideEmbeddedSim() {
binding.wipeSpace.visibility = View.GONE
binding.wipeEmbeddedSim.visibility = View.GONE
}
private fun hideSecureLockScreenRequired() {
binding.apply {
divider.visibility = View.GONE
wipeOnInactivitySwitch.visibility = View.GONE
wipeOnInactivityDescription.visibility = View.GONE
}
}
private fun initWipeOnInactivityTime() {
val count = prefs.wipeOnInactivityCount
val time = when {
count % (24 * 60) == 0 -> "${count / 24 / 60}$MODIFIER_DAYS"
count % 60 == 0 -> "${count / 60}$MODIFIER_HOURS"
else -> "$count$MODIFIER_MINUTES"
}
binding.wipeOnInactivityTime.editText?.setText(time)
}
private fun setup() {
binding.apply {
authenticationCode.setOnLongClickListener {
copyAuthenticationCode()
true
}
wipeData.setOnCheckedChangeListener { _, isChecked ->
prefs.isWipeData = isChecked
wipeEmbeddedSim.isEnabled = isChecked
}
wipeEmbeddedSim.setOnCheckedChangeListener { _, isChecked ->
prefs.isWipeEmbeddedSim = isChecked
}
wipeOnInactivitySwitch.setOnCheckedChangeListener { _, isChecked ->
setWipeOnInactivityState(prefs.isEnabled && isChecked)
prefs.isWipeOnInactivity = isChecked
}
wipeOnInactivityTime.editText?.doAfterTextChanged {
if (wipeOnInactivityTimeRegex.matcher(it?.toString() ?: "").matches()) {
wipeOnInactivityTime.error = null
} else {
wipeOnInactivityTime.error = getString(R.string.wipe_on_inactivity_time_error)
}
}
wipeOnInactivityTime.setEndIconOnClickListener {
if (wipeOnInactivityTime.error != null) return@setEndIconOnClickListener
val time = wipeOnInactivityTime.editText?.text?.toString() ?: ""
if (time.length < 2) return@setEndIconOnClickListener
val modifier = time.last()
val i: Int
try {
i = time.dropLast(1).toInt()
} catch (exc: NumberFormatException) { return@setEndIconOnClickListener }
prefs.wipeOnInactivityCount = when (modifier) {
MODIFIER_DAYS -> i * 24 * 60
MODIFIER_HOURS -> i * 60
MODIFIER_MINUTES -> i
else -> return@setEndIconOnClickListener
}
}
toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) requestAdmin() else setOff()
}
}
}
private fun copyAuthenticationCode() {
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", prefs.authenticationCode))
if (clipboardManager != null) {
scheduleClipboardClear()
Snackbar.make(
binding.authenticationCode,
R.string.copied_popup,
Snackbar.LENGTH_SHORT,
).show()
}
}
private fun scheduleClipboardClear() {
clipboardClearTask?.cancel()
clipboardClearTask = Timer()
clipboardClearTask?.schedule(timerTask {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
clipboardManager?.clearPrimaryClip()
} else {
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", ""))
}
}, CLIPBOARD_CLEAR_DELAY)
}
private fun showTriggers() {
var triggers = prefs.triggers
val values = Trigger.values().toMutableList()
val strings = resources.getStringArray(R.array.triggers).toMutableList()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
strings.removeAt(values.indexOf(Trigger.TILE))
values.remove(Trigger.TILE)
}
MaterialAlertDialogBuilder(this)
.setTitle(R.string.triggers)
.setMultiChoiceItems(
strings.toTypedArray(),
values.map { triggers.and(it.value) != 0 }.toBooleanArray(),
) { _, index, isChecked ->
val flag = values[index]
triggers = when (isChecked) {
true -> triggers.or(flag.value)
false -> triggers.and(flag.value.inv())
}
}
.setPositiveButton(android.R.string.ok) { _, _ ->
prefs.triggers = triggers
setTriggersState(prefs.isEnabled)
}
.show()
}
private fun updateCodeColorState() {
binding.authenticationCode.setBackgroundColor(getColor(
if (prefs.triggers != 0) R.color.code_on else R.color.code_off
))
}
private fun setOn() {
prefs.isEnabled = true
setWipeOnInactivityState(prefs.isWipeOnInactivity)
setTriggersState(true)
}
private fun setTriggersState(value: Boolean) {
if (value) {
val triggers = prefs.triggers
setPanicKitState(triggers.and(Trigger.PANIC_KIT.value) != 0)
setTileState(triggers.and(Trigger.TILE.value) != 0)
shortcut.setState(triggers.and(Trigger.SHORTCUT.value) != 0)
setTriggerReceiverState(triggers.and(Trigger.BROADCAST.value) != 0)
setNotificationListenerState(triggers.and(Trigger.NOTIFICATION.value) != 0)
} else {
setPanicKitState(false)
setTileState(false)
shortcut.setState(false)
setTriggerReceiverState(false)
setNotificationListenerState(false)
}
updateCodeColorState()
}
private fun setOff() {
prefs.isEnabled = false
setWipeOnInactivityState(false)
setTriggersState(false)
try {
admin.remove()
} catch (exc: SecurityException) {}
}
private fun requestAdmin() = registerForDeviceAdmin.launch(admin.makeRequestIntent())
private fun makeAuthenticationCode() = UUID.randomUUID().toString()
private fun setTriggerReceiverState(value: Boolean) =
setComponentState(TriggerReceiver::class.java, value)
private fun setRestartReceiverState(value: Boolean) =
setComponentState(RestartReceiver::class.java, value)
private fun setTileState(value: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
setComponentState(TileService::class.java, value)
}
private fun setNotificationListenerState(value: Boolean) =
setComponentState(NotificationListenerService::class.java, value)
private fun setPanicKitState(value: Boolean) {
setComponentState(PanicConnectionActivity::class.java, value)
setComponentState(PanicResponderActivity::class.java, value)
}
private fun setWipeOnInactivityState(value: Boolean) {
if (!value) job.cancel()
setForegroundState(value)
}
private fun setComponentState(cls: Class<*>, value: Boolean) {
packageManager.setComponentEnabledSetting(
ComponentName(this, cls),
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP,
)
}
private fun setForegroundServiceState(value: Boolean) {
Intent(this.applicationContext, ForegroundService::class.java).also {
if (value) ContextCompat.startForegroundService(this.applicationContext, it)
else stopService(it)
}
}
private fun setForegroundState(value: Boolean) {
setForegroundServiceState(value)
setRestartReceiverState(value)
}
}
}

View File

@ -11,19 +11,22 @@ import androidx.security.crypto.MasterKeys
class Preferences(ctx: Context, encrypted: Boolean = true) {
companion object {
private const val DEFAULT_WIPE_ON_INACTIVITY_COUNT = 7 * 24 * 60
private const val DEFAULT_TRIGGER_LOCK_COUNT = 7 * 24 * 60
private const val ENABLED = "enabled"
private const val AUTHENTICATION_CODE = "authentication_code"
private const val SECRET = "secret"
private const val WIPE_DATA = "wipe_data"
private const val WIPE_EMBEDDED_SIM = "wipe_embedded_sim"
private const val WIPE_ON_INACTIVITY = "wipe_on_inactivity"
private const val TRIGGERS = "triggers"
private const val WIPE_ON_INACTIVITY_COUNT = "wipe_on_inactivity_count"
private const val TRIGGER_LOCK_COUNT = "trigger_lock_count"
private const val FILE_NAME = "sec_shared_prefs"
// migration
private const val AUTHENTICATION_CODE = "authentication_code"
private const val WIPE_ON_INACTIVITY_COUNT = "wipe_on_inactivity_count"
fun new(ctx: Context) = Preferences(
ctx,
encrypted = Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
@ -54,9 +57,12 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
get() = prefs.getInt(TRIGGERS, 0)
set(value) = prefs.edit { putInt(TRIGGERS, value) }
var authenticationCode: String
get() = prefs.getString(AUTHENTICATION_CODE, "") ?: ""
set(value) = prefs.edit { putString(AUTHENTICATION_CODE, value) }
var secret: String
get() = prefs.getString(
SECRET,
prefs.getString(AUTHENTICATION_CODE, "") ?: "",
) ?: ""
set(value) = prefs.edit { putString(SECRET, value) }
var isWipeData: Boolean
get() = prefs.getBoolean(WIPE_DATA, false)
@ -66,13 +72,12 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
get() = prefs.getBoolean(WIPE_EMBEDDED_SIM, false)
set(value) = prefs.edit { putBoolean(WIPE_EMBEDDED_SIM, value) }
var isWipeOnInactivity: Boolean
get() = prefs.getBoolean(WIPE_ON_INACTIVITY, false)
set(value) = prefs.edit { putBoolean(WIPE_ON_INACTIVITY, value) }
var wipeOnInactivityCount: Int
get() = prefs.getInt(WIPE_ON_INACTIVITY_COUNT, DEFAULT_WIPE_ON_INACTIVITY_COUNT)
set(value) = prefs.edit { putInt(WIPE_ON_INACTIVITY_COUNT, value) }
var triggerLockCount: Int
get() = prefs.getInt(
TRIGGER_LOCK_COUNT,
prefs.getInt(WIPE_ON_INACTIVITY_COUNT, DEFAULT_TRIGGER_LOCK_COUNT),
)
set(value) = prefs.edit { putInt(TRIGGER_LOCK_COUNT, value) }
fun registerListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.registerOnSharedPreferenceChangeListener(listener)
@ -100,4 +105,6 @@ enum class Trigger(val value: Int) {
SHORTCUT(1 shl 2),
BROADCAST(1 shl 3),
NOTIFICATION(1 shl 4),
}
LOCK(1 shl 5),
USB(1 shl 6),
}

View File

@ -5,28 +5,7 @@ import android.content.Context
import android.content.Intent
class TriggerReceiver : BroadcastReceiver() {
companion object {
const val KEY = "code"
const val ACTION = "me.lucky.wasted.action.TRIGGER"
fun panic(context: Context, intent: Intent?) {
if (intent?.action != ACTION) return
val prefs = Preferences.new(context)
if (!prefs.isEnabled) return
val code = prefs.authenticationCode
assert(code.isNotEmpty())
if (intent.getStringExtra(KEY) != code) return
val admin = DeviceAdminManager(context)
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
}
}
override fun onReceive(context: Context?, intent: Intent?) {
if (Preferences.new(context ?: return).triggers.and(Trigger.BROADCAST.value) == 0)
return
panic(context, intent)
me.lucky.wasted.trigger.broadcast.BroadcastReceiver().onReceive(context, intent)
}
}
}

View File

@ -0,0 +1,87 @@
package me.lucky.wasted
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import me.lucky.wasted.trigger.notification.NotificationListenerService
import me.lucky.wasted.trigger.panic.PanicConnectionActivity
import me.lucky.wasted.trigger.panic.PanicResponderActivity
import me.lucky.wasted.trigger.shared.ForegroundService
import me.lucky.wasted.trigger.shared.RestartReceiver
import me.lucky.wasted.trigger.shortcut.ShortcutActivity
import me.lucky.wasted.trigger.shortcut.ShortcutManager
import me.lucky.wasted.trigger.tile.TileService
import me.lucky.wasted.trigger.usb.UsbReceiver
class Utils(private val ctx: Context) {
companion object {
fun setFlag(key: Int, value: Int, enabled: Boolean) =
when(enabled) {
true -> key.or(value)
false -> key.and(value.inv())
}
}
private val shortcut by lazy { ShortcutManager(ctx) }
fun setEnabled(enabled: Boolean) {
val triggers = Preferences(ctx).triggers
setPanicKitEnabled(enabled && triggers.and(Trigger.PANIC_KIT.value) != 0)
setTileEnabled(enabled && triggers.and(Trigger.TILE.value) != 0)
setShortcutEnabled(enabled && triggers.and(Trigger.SHORTCUT.value) != 0)
setBroadcastEnabled(enabled && triggers.and(Trigger.BROADCAST.value) != 0)
setNotificationEnabled(enabled && triggers.and(Trigger.NOTIFICATION.value) != 0)
updateForegroundRequiredEnabled()
}
fun setPanicKitEnabled(enabled: Boolean) {
setComponentEnabled(PanicConnectionActivity::class.java, enabled)
setComponentEnabled(PanicResponderActivity::class.java, enabled)
}
fun setTileEnabled(enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
setComponentEnabled(TileService::class.java, enabled)
}
fun setShortcutEnabled(enabled: Boolean) {
if (!enabled) shortcut.remove()
setComponentEnabled(ShortcutActivity::class.java, enabled)
if (enabled) shortcut.push()
}
fun setBroadcastEnabled(enabled: Boolean) =
setComponentEnabled(TriggerReceiver::class.java, enabled)
fun setNotificationEnabled(enabled: Boolean) =
setComponentEnabled(NotificationListenerService::class.java, enabled)
fun updateForegroundRequiredEnabled() {
val prefs = Preferences(ctx)
val enabled = prefs.isEnabled
val triggers = prefs.triggers
val isLock = triggers.and(Trigger.LOCK.value) != 0
val isUSB = triggers.and(Trigger.USB.value) != 0
setForegroundEnabled(enabled && (isLock || isUSB))
setComponentEnabled(RestartReceiver::class.java, enabled && (isLock || isUSB))
setComponentEnabled(UsbReceiver::class.java, enabled && isUSB)
}
private fun setForegroundEnabled(enabled: Boolean) =
Intent(ctx.applicationContext, ForegroundService::class.java).also {
if (enabled) ContextCompat.startForegroundService(ctx.applicationContext, it)
else ctx.stopService(it)
}
private fun setComponentEnabled(cls: Class<*>, enabled: Boolean) =
ctx.packageManager.setComponentEnabledSetting(
ComponentName(ctx, cls),
if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP,
)
}

View File

@ -1,19 +0,0 @@
package me.lucky.wasted
import android.app.job.JobParameters
import android.app.job.JobService
class WipeJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val prefs = Preferences.new(this)
if (!prefs.isEnabled || !prefs.isWipeOnInactivity) return false
try {
DeviceAdminManager(this).wipeData()
} catch (exc: SecurityException) {}
return false
}
override fun onStopJob(params: JobParameters?): Boolean {
return true
}
}

View File

@ -1,4 +1,4 @@
package me.lucky.wasted
package me.lucky.wasted.admin
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
@ -7,6 +7,8 @@ import android.content.Intent
import android.os.Build
import java.lang.Exception
import me.lucky.wasted.Preferences
class DeviceAdminManager(private val ctx: Context) {
private val dpm = ctx.getSystemService(DevicePolicyManager::class.java)
private val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }
@ -42,4 +44,4 @@ class DeviceAdminManager(private val ctx: Context) {
fun makeRequestIntent() =
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdmin)
}
}

View File

@ -0,0 +1,5 @@
package me.lucky.wasted.admin
import android.app.admin.DeviceAdminReceiver
class DeviceAdminReceiver : DeviceAdminReceiver()

View File

@ -0,0 +1,74 @@
package me.lucky.wasted.fragment
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import java.util.regex.Pattern
import me.lucky.wasted.Preferences
import me.lucky.wasted.R
import me.lucky.wasted.databinding.FragmentLockBinding
class LockFragment : Fragment() {
companion object {
private const val MODIFIER_DAYS = 'd'
private const val MODIFIER_HOURS = 'h'
private const val MODIFIER_MINUTES = 'm'
}
private lateinit var binding: FragmentLockBinding
private lateinit var ctx: Context
private lateinit var prefs: Preferences
private val lockCountPattern by lazy {
Pattern.compile("^[1-9]\\d*[$MODIFIER_DAYS$MODIFIER_HOURS$MODIFIER_MINUTES]$") }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentLockBinding.inflate(inflater, container, false)
init()
setup()
return binding.root
}
private fun init() {
ctx = requireContext()
prefs = Preferences(ctx)
val count = prefs.triggerLockCount
val time = when {
count % (24 * 60) == 0 -> "${count / 24 / 60}$MODIFIER_DAYS"
count % 60 == 0 -> "${count / 60}$MODIFIER_HOURS"
else -> "$count$MODIFIER_MINUTES"
}
binding.time.editText?.setText(time)
}
private fun setup() = binding.apply {
time.editText?.doAfterTextChanged {
val str = it?.toString() ?: ""
if (!lockCountPattern.matcher(str).matches()) {
time.error = ctx.getString(R.string.trigger_lock_time_error)
return@doAfterTextChanged
}
if (str.length < 2) return@doAfterTextChanged
val modifier = str.last()
val i: Int
try {
i = str.dropLast(1).toInt()
} catch (exc: NumberFormatException) { return@doAfterTextChanged }
prefs.triggerLockCount = when (modifier) {
MODIFIER_DAYS -> i * 24 * 60
MODIFIER_HOURS -> i * 60
MODIFIER_MINUTES -> i
else -> return@doAfterTextChanged
}
time.error = null
}
}
}

View File

@ -0,0 +1,104 @@
package me.lucky.wasted.fragment
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import java.util.*
import me.lucky.wasted.Preferences
import me.lucky.wasted.R
import me.lucky.wasted.Utils
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.databinding.FragmentMainBinding
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private lateinit var ctx: Context
private lateinit var prefs: Preferences
private val clipboardManager by lazy { ctx.getSystemService(ClipboardManager::class.java) }
private val admin by lazy { DeviceAdminManager(ctx) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentMainBinding.inflate(inflater, container, false)
init()
setup()
return binding.root
}
override fun onStart() {
super.onStart()
updateSecretColor()
}
private fun init() {
ctx = requireContext()
prefs = Preferences(ctx)
if (prefs.secret.isEmpty()) prefs.secret = makeSecret()
binding.apply {
secret.text = prefs.secret
wipeData.isChecked = prefs.isWipeData
wipeEmbeddedSim.isChecked = prefs.isWipeEmbeddedSim
wipeEmbeddedSim.isEnabled = wipeData.isChecked
toggle.isChecked = prefs.isEnabled
}
}
private fun setup() = binding.apply {
secret.setOnLongClickListener {
copySecret()
true
}
wipeData.setOnCheckedChangeListener { _, isChecked ->
prefs.isWipeData = isChecked
wipeEmbeddedSim.isEnabled = isChecked
}
wipeEmbeddedSim.setOnCheckedChangeListener { _, isChecked ->
prefs.isWipeEmbeddedSim = isChecked
}
toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) requestAdmin() else setOff()
}
}
private fun copySecret() {
clipboardManager.setPrimaryClip(ClipData.newPlainText("", prefs.secret))
Snackbar.make(binding.secret, R.string.copied_popup, Snackbar.LENGTH_SHORT).show()
}
private fun updateSecretColor() = binding.secret.setBackgroundColor(ctx.getColor(
if (prefs.triggers != 0) R.color.secret_1 else R.color.secret_0
))
private fun setOn() {
prefs.isEnabled = true
Utils(ctx).setEnabled(true)
binding.toggle.isChecked = true
}
private fun setOff() {
prefs.isEnabled = false
Utils(ctx).setEnabled(false)
try { admin.remove() } catch (exc: SecurityException) {}
binding.toggle.isChecked = false
}
private val registerForDeviceAdmin =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) setOn() else setOff()
}
private fun requestAdmin() = registerForDeviceAdmin.launch(admin.makeRequestIntent())
private fun makeSecret() = UUID.randomUUID().toString()
}

View File

@ -0,0 +1,80 @@
package me.lucky.wasted.fragment
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.Utils
import me.lucky.wasted.databinding.FragmentSettingsBinding
class SettingsFragment : Fragment() {
private lateinit var binding: FragmentSettingsBinding
private lateinit var ctx: Context
private lateinit var prefs: Preferences
private val utils by lazy { Utils(ctx) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentSettingsBinding.inflate(inflater, container, false)
init()
setup()
return binding.root
}
private fun init() {
ctx = requireContext()
prefs = Preferences(ctx)
binding.apply {
val triggers = prefs.triggers
panicKit.isChecked = triggers.and(Trigger.PANIC_KIT.value) != 0
tile.isChecked = triggers.and(Trigger.TILE.value) != 0
tile.isEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
shortcut.isChecked = triggers.and(Trigger.SHORTCUT.value) != 0
broadcast.isChecked = triggers.and(Trigger.BROADCAST.value) != 0
notification.isChecked = triggers.and(Trigger.NOTIFICATION.value) != 0
lock.isChecked = triggers.and(Trigger.LOCK.value) != 0
usb.isChecked = triggers.and(Trigger.USB.value) != 0
}
}
private fun setup() = binding.apply {
panicKit.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.PANIC_KIT.value, isChecked)
utils.setPanicKitEnabled(isChecked && prefs.isEnabled)
}
tile.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.TILE.value, isChecked)
utils.setTileEnabled(isChecked && prefs.isEnabled)
}
shortcut.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.SHORTCUT.value, isChecked)
utils.setShortcutEnabled(isChecked && prefs.isEnabled)
}
broadcast.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.BROADCAST.value, isChecked)
utils.setBroadcastEnabled(isChecked && prefs.isEnabled)
}
notification.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers =
Utils.setFlag(prefs.triggers, Trigger.NOTIFICATION.value, isChecked)
utils.setNotificationEnabled(isChecked && prefs.isEnabled)
}
lock.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.LOCK.value, isChecked)
utils.updateForegroundRequiredEnabled()
}
usb.setOnCheckedChangeListener { _, isChecked ->
prefs.triggers = Utils.setFlag(prefs.triggers, Trigger.USB.value, isChecked)
utils.updateForegroundRequiredEnabled()
}
}
}

View File

@ -0,0 +1,35 @@
package me.lucky.wasted.trigger.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.admin.DeviceAdminManager
class BroadcastReceiver : BroadcastReceiver() {
companion object {
const val KEY = "code"
const val ACTION = "me.lucky.wasted.action.TRIGGER"
fun panic(context: Context, intent: Intent?) {
if (intent?.action != ACTION) return
val prefs = Preferences.new(context)
if (!prefs.isEnabled) return
val secret = prefs.secret
assert(secret.isNotEmpty())
if (intent.getStringExtra(KEY) != secret) return
val admin = DeviceAdminManager(context)
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
}
}
override fun onReceive(context: Context?, intent: Intent?) {
if (Preferences.new(context ?: return).triggers.and(Trigger.BROADCAST.value) != 0)
panic(context, intent)
}
}

View File

@ -1,4 +1,4 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.lock
import android.app.job.JobInfo
import android.app.job.JobScheduler
@ -6,7 +6,9 @@ import android.content.ComponentName
import android.content.Context
import java.util.concurrent.TimeUnit
class WipeJobManager(private val ctx: Context) {
import me.lucky.wasted.Preferences
class LockJobManager(private val ctx: Context) {
companion object {
private const val JOB_ID = 1000
}
@ -15,8 +17,8 @@ class WipeJobManager(private val ctx: Context) {
fun schedule(): Int {
return scheduler?.schedule(
JobInfo.Builder(JOB_ID, ComponentName(ctx, WipeJobService::class.java))
.setMinimumLatency(TimeUnit.MINUTES.toMillis(prefs.wipeOnInactivityCount.toLong()))
JobInfo.Builder(JOB_ID, ComponentName(ctx, LockJobService::class.java))
.setMinimumLatency(TimeUnit.MINUTES.toMillis(prefs.triggerLockCount.toLong()))
.setBackoffCriteria(0, JobInfo.BACKOFF_POLICY_LINEAR)
.setPersisted(true)
.build()
@ -24,4 +26,4 @@ class WipeJobManager(private val ctx: Context) {
}
fun cancel() = scheduler?.cancel(JOB_ID)
}
}

View File

@ -0,0 +1,23 @@
package me.lucky.wasted.trigger.lock
import android.app.job.JobParameters
import android.app.job.JobService
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
class LockJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val prefs = Preferences.new(this)
if (!prefs.isEnabled || prefs.triggers.and(Trigger.LOCK.value) == 0) return false
val admin = DeviceAdminManager(this)
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
return false
}
override fun onStopJob(params: JobParameters?): Boolean { return true }
}

View File

@ -1,10 +1,14 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.notification
import android.app.Notification
import android.os.Build
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
class NotificationListenerService : NotificationListenerService() {
private lateinit var prefs: Preferences
private lateinit var admin: DeviceAdminManager
@ -24,9 +28,9 @@ class NotificationListenerService : NotificationListenerService() {
if (sbn == null ||
!prefs.isEnabled ||
prefs.triggers.and(Trigger.NOTIFICATION.value) == 0) return
val code = prefs.authenticationCode
assert(code.isNotEmpty())
if (sbn.notification.extras[Notification.EXTRA_TEXT]?.toString() != code) return
val secret = prefs.secret
assert(secret.isNotEmpty())
if (sbn.notification.extras[Notification.EXTRA_TEXT]?.toString() != secret) return
cancelAllNotifications()
try {
admin.lockNow()
@ -39,4 +43,4 @@ class NotificationListenerService : NotificationListenerService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
migrateNotificationFilter(0, null)
}
}
}

View File

@ -1,10 +1,12 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.panic
import android.content.pm.PackageManager
import android.os.Bundle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import info.guardianproject.panic.PanicResponder
import me.lucky.wasted.MainActivity
import me.lucky.wasted.R
class PanicConnectionActivity : MainActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -42,4 +44,4 @@ class PanicConnectionActivity : MainActivity() {
}
.show()
}
}
}

View File

@ -1,10 +1,13 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.panic
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import info.guardianproject.panic.Panic
import info.guardianproject.panic.PanicResponder
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
class PanicResponderActivity : AppCompatActivity() {
private val prefs by lazy { Preferences.new(this) }
@ -26,4 +29,4 @@ class PanicResponderActivity : AppCompatActivity() {
} catch (exc: SecurityException) {}
finishAndRemoveTask()
}
}
}

View File

@ -0,0 +1,128 @@
package me.lucky.wasted.trigger.shared
import android.app.KeyguardManager
import android.app.Service
import android.app.job.JobScheduler
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import androidx.core.app.NotificationCompat
import me.lucky.wasted.Preferences
import me.lucky.wasted.R
import me.lucky.wasted.Trigger
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.trigger.lock.LockJobManager
class ForegroundService : Service() {
companion object {
private const val NOTIFICATION_ID = 1000
private const val ACTION_USB_STATE = "android.hardware.usb.action.USB_STATE"
}
private lateinit var prefs: Preferences
private lateinit var lockReceiver: LockReceiver
private val usbReceiver = UsbReceiver()
override fun onCreate() {
super.onCreate()
init()
}
override fun onDestroy() {
super.onDestroy()
deinit()
}
private fun init() {
prefs = Preferences.new(this)
lockReceiver = LockReceiver(getSystemService(KeyguardManager::class.java).isDeviceLocked)
val triggers = prefs.triggers
if (triggers.and(Trigger.LOCK.value) != 0)
registerReceiver(lockReceiver, IntentFilter().apply {
addAction(Intent.ACTION_USER_PRESENT)
addAction(Intent.ACTION_SCREEN_OFF)
})
if (triggers.and(Trigger.USB.value) != 0)
registerReceiver(usbReceiver, IntentFilter(ACTION_USB_STATE))
}
private fun deinit() {
try {
unregisterReceiver(lockReceiver)
unregisterReceiver(usbReceiver)
} catch (exc: IllegalArgumentException) {}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startForeground(
NOTIFICATION_ID,
NotificationCompat.Builder(this, NotificationManager.CHANNEL_DEFAULT_ID)
.setContentTitle(getString(R.string.foreground_service_notification_title))
.setSmallIcon(android.R.drawable.ic_delete)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
)
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? { return null }
private class LockReceiver(private var locked: Boolean) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (Preferences.new(context ?: return).triggers.and(Trigger.LOCK.value) == 0)
return
when (intent?.action) {
Intent.ACTION_USER_PRESENT -> {
locked = false
LockJobManager(context).cancel()
}
Intent.ACTION_SCREEN_OFF -> {
if (locked) return
locked = true
Thread(Runner(context, goAsync())).start()
}
}
}
private class Runner(
private val ctx: Context,
private val pendingResult: PendingResult,
) : Runnable {
override fun run() {
val job = LockJobManager(ctx)
var delay = 1000L
while (job.schedule() != JobScheduler.RESULT_SUCCESS) {
Thread.sleep(delay)
delay = delay.shl(1)
}
pendingResult.finish()
}
}
}
private class UsbReceiver : BroadcastReceiver() {
companion object {
private const val KEY_1 = "connected"
private const val KEY_2 = "host_connected"
}
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != ACTION_USB_STATE) return
val prefs = Preferences.new(context ?: return)
if (!prefs.isEnabled ||
prefs.triggers.and(Trigger.USB.value) == 0 ||
!context.getSystemService(KeyguardManager::class.java).isDeviceLocked) return
val extras = intent.extras ?: return
if (!extras.getBoolean(KEY_1) && !extras.getBoolean(KEY_2)) return
val admin = DeviceAdminManager(context)
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
}
}
}

View File

@ -1,9 +1,11 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.shared
import android.content.Context
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import me.lucky.wasted.R
class NotificationManager(private val ctx: Context) {
companion object {
const val CHANNEL_DEFAULT_ID = "default"
@ -17,4 +19,4 @@ class NotificationManager(private val ctx: Context) {
NotificationManagerCompat.IMPORTANCE_LOW,
).setName(ctx.getString(R.string.notification_channel_default_name)).build())
}
}
}

View File

@ -1,20 +1,26 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.shared
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
class RestartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_LOCKED_BOOT_COMPLETED &&
intent?.action != Intent.ACTION_BOOT_COMPLETED &&
intent?.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
val prefs = Preferences.new(context ?: return)
if (!prefs.isEnabled || !prefs.isWipeOnInactivity) return
val triggers = prefs.triggers
if (!prefs.isEnabled || (
triggers.and(Trigger.LOCK.value) == 0 &&
triggers.and(Trigger.USB.value) == 0)) return
ContextCompat.startForegroundService(
context.applicationContext,
Intent(context.applicationContext, ForegroundService::class.java),
)
}
}
}

View File

@ -1,8 +1,12 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.trigger.broadcast.BroadcastReceiver
class ShortcutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -10,7 +14,7 @@ class ShortcutActivity : AppCompatActivity() {
finishAndRemoveTask()
return
}
TriggerReceiver.panic(this, intent)
BroadcastReceiver.panic(this, intent)
finishAndRemoveTask()
}
}
}

View File

@ -1,4 +1,4 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.shortcut
import android.content.Context
import android.content.Intent
@ -6,6 +6,10 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import me.lucky.wasted.Preferences
import me.lucky.wasted.R
import me.lucky.wasted.trigger.broadcast.BroadcastReceiver
class ShortcutManager(private val ctx: Context) {
companion object {
private const val SHORTCUT_ID = "panic"
@ -13,22 +17,19 @@ class ShortcutManager(private val ctx: Context) {
private val prefs by lazy { Preferences(ctx) }
private fun push() {
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(TriggerReceiver.ACTION)
Intent(BroadcastReceiver.ACTION)
.setClass(ctx, ShortcutActivity::class.java)
.putExtra(TriggerReceiver.KEY, prefs.authenticationCode)
.putExtra(BroadcastReceiver.KEY, prefs.secret)
)
.build(),
)
}
private fun remove() =
ShortcutManagerCompat.removeDynamicShortcuts(ctx, arrayListOf(SHORTCUT_ID))
fun setState(value: Boolean) = if (value) push() else remove()
}
fun remove() = ShortcutManagerCompat.removeDynamicShortcuts(ctx, arrayListOf(SHORTCUT_ID))
}

View File

@ -1,4 +1,4 @@
package me.lucky.wasted
package me.lucky.wasted.trigger.tile
import android.os.Build
import android.service.quicksettings.Tile
@ -7,6 +7,10 @@ import androidx.annotation.RequiresApi
import java.util.*
import kotlin.concurrent.timerTask
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
@RequiresApi(Build.VERSION_CODES.N)
class TileService : TileService() {
companion object {
@ -71,4 +75,4 @@ class TileService : TileService() {
qsTile.state = tileState
qsTile.updateTile()
}
}
}

View File

@ -0,0 +1,27 @@
package me.lucky.wasted.trigger.usb
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.hardware.usb.UsbManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.admin.DeviceAdminManager
class UsbReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != UsbManager.ACTION_USB_DEVICE_ATTACHED &&
intent?.action != UsbManager.ACTION_USB_ACCESSORY_ATTACHED) return
val prefs = Preferences.new(context ?: return)
if (!prefs.isEnabled ||
prefs.triggers.and(Trigger.USB.value) == 0 ||
!context.getSystemService(KeyguardManager::class.java).isDeviceLocked) return
val admin = DeviceAdminManager(context)
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
}
}

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>

View File

@ -1,119 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp"
tools:openDrawer="start"
tools:context=".MainActivity">
<ScrollView
android:id="@+id/scrollView"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:isScrollContainer="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<TextView
android:id="@+id/authenticationCode"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/code_off"
android:padding="16dp"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold" />
android:layout_height="?attr/actionBarSize"
app:menu="@menu/top"
app:navigationIcon="@drawable/ic_baseline_menu_24" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"/>
</com.google.android.material.appbar.AppBarLayout>
<CheckBox
android:id="@+id/wipeData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_data_check_box"
android:textSize="16sp" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<Space
android:id="@+id/wipeSpace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="2dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<CheckBox
android:id="@+id/wipeEmbeddedSim"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_embedded_sim_check_box"
android:textSize="16sp" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="8dp"
android:background="?android:attr/listDivider" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/wipeOnInactivitySwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="@string/wipe_on_inactivity_switch" />
<TextView
android:id="@+id/wipeOnInactivityDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wipe_on_inactivity_description" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/wipeOnInactivityTime"
app:helperText="@string/wipe_on_inactivity_time_helper_text"
app:helperTextEnabled="true"
app:errorEnabled="true"
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_baseline_check_circle_24"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/wipe_on_inactivity_time_hint" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<ToggleButton
android:id="@+id/toggle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.LockFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/time"
app:helperText="@string/trigger_lock_time_helper_text"
app:helperTextEnabled="true"
app:errorEnabled="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/trigger_lock_time_hint" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.MainFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:isScrollContainer="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/secret_0"
android:padding="16dp"
android:textAlignment="center"
android:textStyle="bold"
android:textAppearance="?attr/textAppearanceTitleLarge" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"/>
<CheckBox
android:id="@+id/wipeData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_data_checkbox"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<Space
android:id="@+id/space1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="2dp" />
<CheckBox
android:id="@+id/wipeEmbeddedSim"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layoutDirection="rtl"
android:text="@string/wipe_embedded_sim_checkbox"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout>
</ScrollView>
<ToggleButton
android:id="@+id/toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.SettingsFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/panicKit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_panic_kit" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_panic_kit_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/tile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_tile" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_tile_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/shortcut"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_shortcut" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_shortcut_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/broadcast"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_broadcast" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_broadcast_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_notification" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_notification_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/lock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_lock" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_lock_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<CheckBox
android:id="@+id/usb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="rtl"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/trigger_usb" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trigger_usb_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_main"
android:title="@string/main"
android:checkable="true"
android:checked="true" />
<group
android:id="@+id/nav_group_options"
android:checkableBehavior="all">
<item
android:id="@+id/nav_trigger_lock"
android:title="@string/trigger_lock" />
</group>
</menu>

View File

@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/triggers"
android:title="@string/triggers"
android:id="@+id/top_settings"
android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/settings"
app:showAsAction="always" />
</menu>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Geräteadministrator nicht verfügbar</string>
<string name="wipe_data_check_box">Daten löschen</string>
<string name="wipe_embedded_sim_check_box">eSim löschen</string>
<string name="wipe_data_checkbox">Daten löschen</string>
<string name="wipe_embedded_sim_checkbox">eSim löschen</string>
<string name="panic_app_dialog_title">Bestätige Panik App</string>
<string name="panic_app_dialog_message">Bist du sicher, dass du %1$s erlauben willst, destruktive Aktionen auslösen zu lassen\?</string>
<string name="panic_app_unknown_app">eine unbekannte App</string>
<string name="allow">Zulassen</string>
<string name="tile_label">Flugmodus</string>
<string name="shortcut_label">Panik</string>
<string name="wipe_on_inactivity_switch">Bei Inaktivität löschen</string>
<string name="wipe_on_inactivity_description">Den Speicher des Geräts löschen, wenn es für N Tage nicht entsperrt wurde.</string>
<string name="trigger_lock_description">Den Speicher des Geräts löschen, wenn es für N Tage nicht entsperrt wurde.</string>
<string name="notification_channel_default_name">Standard</string>
<string name="foreground_service_notification_title">Guard</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Tile</string>
<string name="triggers_array_shortcut">Shortcut</string>
<string name="triggers_array_broadcast">Broadcast</string>
<string name="triggers_array_notification">Benachrichtigung</string>
<string name="triggers">Auslöser</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Tile</string>
<string name="trigger_shortcut">Shortcut</string>
<string name="trigger_broadcast">Broadcast</string>
<string name="trigger_notification">Benachrichtigung</string>
<string name="copied_popup">Kopiert</string>
</resources>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Administrador de dispositivos no disponible</string>
<string name="wipe_data_check_box">Borrar datos</string>
<string name="wipe_embedded_sim_check_box">Borrar eSIM</string>
<string name="wipe_data_checkbox">Borrar datos</string>
<string name="wipe_embedded_sim_checkbox">Borrar eSIM</string>
<string name="panic_app_dialog_title">Confirmar aplicación de pánico</string>
<string name="panic_app_dialog_message">¿Está seguro de que desea permitir que %1$s active acciones de pánico destructivas\?</string>
<string name="panic_app_unknown_app">una aplicación desconocida</string>
<string name="allow">Permitir</string>
<string name="tile_label">Modo avión</string>
<string name="shortcut_label">Pánico</string>
<string name="wipe_on_inactivity_switch">Borrar por inactividad</string>
<string name="wipe_on_inactivity_description">Limpia un dispositivo cuando no fue desbloqueado por N días.</string>
<string name="trigger_lock_description">Limpia un dispositivo cuando no fue desbloqueado por N días.</string>
<string name="notification_channel_default_name">Predeterminado</string>
<string name="foreground_service_notification_title">Guardia</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Título</string>
<string name="triggers_array_shortcut">Acceso directo</string>
<string name="triggers_array_broadcast">Transmisión</string>
<string name="triggers_array_notification">Notificación</string>
<string name="triggers">Activadores</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Título</string>
<string name="trigger_shortcut">Acceso directo</string>
<string name="trigger_broadcast">Transmisión</string>
<string name="trigger_notification">Notificación</string>
<string name="copied_popup">Copiado</string>
</resources>

View File

@ -1,27 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Amministrazione dispositivo non disponibile</string>
<string name="wipe_data_check_box">Cancella i dati</string>
<string name="wipe_embedded_sim_check_box">Cancella eSIM</string>
<string name="wipe_data_checkbox">Cancella i dati</string>
<string name="wipe_embedded_sim_checkbox">Cancella eSIM</string>
<string name="panic_app_dialog_title">Conferma app di panico</string>
<string name="panic_app_dialog_message">Sei sicuro di voler consentire a %1$s di attivare azioni di panico distruttive\?</string>
<string name="panic_app_unknown_app">un\'app sconosciuta</string>
<string name="allow">Consenti</string>
<string name="tile_label">Modalità aereo</string>
<string name="shortcut_label">Panico</string>
<string name="wipe_on_inactivity_switch">Cancella in caso di inattività</string>
<string name="wipe_on_inactivity_description">Cancella i dati quando il dispositivo non viene sbloccato per N tempo.</string>
<string name="wipe_on_inactivity_time_hint">tempo</string>
<string name="wipe_on_inactivity_time_error">7d / 48h / 120m</string>
<string name="wipe_on_inactivity_time_helper_text">[d]giorni [h]ore [m]minuti</string>
<string name="trigger_lock_description">Cancella i dati quando il dispositivo non viene sbloccato per N tempo.</string>
<string name="trigger_lock_time_hint">tempo</string>
<string name="trigger_lock_time_error">7d / 48h / 120m</string>
<string name="trigger_lock_time_helper_text">[d]giorni [h]ore [m]minuti</string>
<string name="notification_channel_default_name">Predefinito</string>
<string name="foreground_service_notification_title">Guardia</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Toggle</string>
<string name="triggers_array_shortcut">Scorciatoia</string>
<string name="triggers_array_broadcast">Broadcast</string>
<string name="triggers_array_notification">Notifica</string>
<string name="triggers">Attivatori</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Toggle</string>
<string name="trigger_shortcut">Scorciatoia</string>
<string name="trigger_broadcast">Broadcast</string>
<string name="trigger_notification">Notifica</string>
<string name="copied_popup">Copiato</string>
</resources>

View File

@ -1,6 +1,6 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Wasted" parent="Theme.Material3.DayNight">
<style name="Theme.Wasted" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorOnPrimary">@color/black</item>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Потрачено</string>
<string name="service_unavailable_popup">Администратор устройства недоступен</string>
<string name="wipe_data_check_box">Стереть данные</string>
<string name="wipe_embedded_sim_check_box">Стереть eSIM</string>
<string name="wipe_data_checkbox">Стереть данные</string>
<string name="wipe_embedded_sim_checkbox">Стереть eSIM</string>
<string name="panic_app_dialog_title">Подтвердите приложение тревожной кнопки</string>
<string name="panic_app_dialog_message">Вы уверены, что хотите разрешить %1$s запускать действия тревожной кнопки\?</string>
<string name="panic_app_unknown_app">неизвестное приложение</string>
<string name="allow">Разрешить</string>
<string name="tile_label">Режим полета</string>
<string name="shortcut_label">Тревога</string>
<string name="wipe_on_inactivity_switch">Стереть при неактивности</string>
<string name="wipe_on_inactivity_description">Стереть данные когда устройство не разблокируется N дней.</string>
<string name="trigger_lock_description">Стереть данные когда устройство не разблокируется N дней.</string>
<string name="notification_channel_default_name">По умолчанию</string>
<string name="foreground_service_notification_title">Охрана</string>
<string name="triggers_array_panic_kit">Тревожная кнопка</string>
<string name="triggers_array_tile">Плитка</string>
<string name="triggers_array_shortcut">Ярлык</string>
<string name="triggers_array_broadcast">Широковещательное сообщение</string>
<string name="triggers_array_notification">Уведомление</string>
<string name="triggers">Триггеры</string>
<string name="trigger_panic_kit">Тревожная кнопка</string>
<string name="trigger_tile">Плитка</string>
<string name="trigger_shortcut">Ярлык</string>
<string name="trigger_broadcast">Широковещательное сообщение</string>
<string name="trigger_notification">Уведомление</string>
<string name="copied_popup">Скопировано</string>
</resources>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Cihaz yöneticisi mevcut değil</string>
<string name="wipe_data_check_box">Verileri sil</string>
<string name="wipe_embedded_sim_check_box">eSIM\'i sil</string>
<string name="wipe_data_checkbox">Verileri sil</string>
<string name="wipe_embedded_sim_checkbox">eSIM\'i sil</string>
<string name="panic_app_dialog_title">Panik uygulamasını onayla</string>
<string name="panic_app_dialog_message">Yıkıcı panik eylemlerini tetiklemek için %1$s\'e izin vermek istediğinizden emin misiniz\?</string>
<string name="panic_app_unknown_app">bilinmeyen bir uygulama</string>
<string name="allow">İzin ver</string>
<string name="tile_label">Uçak modu</string>
<string name="shortcut_label">Panik</string>
<string name="wipe_on_inactivity_switch">Kullanılmadığında sil</string>
<string name="wipe_on_inactivity_description">N gün boyunca kilidi açılmamış bir cihazı silin.</string>
<string name="trigger_lock_description">N gün boyunca kilidi açılmamış bir cihazı silin.</string>
<string name="notification_channel_default_name">Varsayılan</string>
<string name="foreground_service_notification_title">Guard</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Tile</string>
<string name="triggers_array_shortcut">Kısayol</string>
<string name="triggers_array_broadcast">Yayın</string>
<string name="triggers_array_notification">Bildirim</string>
<string name="triggers">Tetikleyiciler</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Tile</string>
<string name="trigger_shortcut">Kısayol</string>
<string name="trigger_broadcast">Yayın</string>
<string name="trigger_notification">Bildirim</string>
<string name="copied_popup">Kopyalandı</string>
</resources>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Адміністратор пристрою недоступний</string>
<string name="wipe_data_check_box">Стерти дані</string>
<string name="wipe_embedded_sim_check_box">Стерти дані eSIM</string>
<string name="wipe_data_checkbox">Стерти дані</string>
<string name="wipe_embedded_sim_checkbox">Стерти дані 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_unknown_app">невідомий додаток</string>
<string name="allow">Allow</string>
<string name="tile_label">Режим польоту</string>
<string name="shortcut_label">Тривога</string>
<string name="wipe_on_inactivity_switch">Wipe on inactivity</string>
<string name="wipe_on_inactivity_description">Видалення пристрою, коли його не було розблоковано протягом N днів.</string>
<string name="trigger_lock_description">Видалення пристрою, коли його не було розблоковано протягом N днів.</string>
<string name="notification_channel_default_name">За замовчуванням</string>
<string name="foreground_service_notification_title">Захисник</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Tile</string>
<string name="triggers_array_shortcut">Гарячі клавіші</string>
<string name="triggers_array_broadcast">Трансляція</string>
<string name="triggers_array_notification">Сповіщення</string>
<string name="triggers">Тріггери</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Tile</string>
<string name="trigger_shortcut">Гарячі клавіші</string>
<string name="trigger_broadcast">Трансляція</string>
<string name="trigger_notification">Сповіщення</string>
<string name="copied_popup">Скопійовано</string>
</resources>

View File

@ -1,24 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">自毁</string>
<string name="service_unavailable_popup">设备管理员不可用</string>
<string name="wipe_data_check_box">擦除数据</string>
<string name="wipe_embedded_sim_check_box">擦除eSIM</string>
<string name="wipe_data_checkbox">擦除数据</string>
<string name="wipe_embedded_sim_checkbox">擦除eSIM</string>
<string name="panic_app_dialog_title">确认需要使用的紧急应用程序</string>
<string name="panic_app_dialog_message">你是否确定允许%1$s触发紧急自毁行为</string>
<string name="panic_app_unknown_app">未知应用</string>
<string name="allow">允许</string>
<string name="tile_label">飞行模式</string>
<string name="shortcut_label">紧急</string>
<string name="wipe_on_inactivity_switch">在不使用时擦除</string>
<string name="wipe_on_inactivity_description">当设备在N天内没被解锁时擦除</string>
<string name="trigger_lock_description">当设备在N天内没被解锁时擦除</string>
<string name="notification_channel_default_name">默认设置</string>
<string name="foreground_service_notification_title">守卫</string>
<string name="triggers_array_panic_kit">恐慌触发器</string>
<string name="triggers_array_tile">追踪器</string>
<string name="triggers_array_shortcut">快捷方式</string>
<string name="triggers_array_broadcast">广播</string>
<string name="triggers_array_notification">通知</string>
<string name="triggers">触发器</string>
<string name="trigger_panic_kit">恐慌触发器</string>
<string name="trigger_tile">追踪器</string>
<string name="trigger_shortcut">快捷方式</string>
<string name="trigger_broadcast">广播</string>
<string name="trigger_notification">通知</string>
<string name="copied_popup">复制成功</string>
</resources>

View File

@ -6,6 +6,6 @@
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="code_on">#FFD600</color>
<color name="code_off">#E13741</color>
<color name="secret_1">#FFD600</color>
<color name="secret_0">#E13741</color>
</resources>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="triggers">
<item>@string/triggers_array_panic_kit</item>
<item>@string/triggers_array_tile</item>
<item>@string/triggers_array_shortcut</item>
<item>@string/triggers_array_broadcast</item>
<item>@string/triggers_array_notification</item>
</string-array>
</resources>

View File

@ -1,27 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wasted</string>
<string name="service_unavailable_popup">Device admin unavailable</string>
<string name="wipe_data_check_box">Wipe data</string>
<string name="wipe_embedded_sim_check_box">Wipe eSIM</string>
<string name="wipe_data_checkbox">Wipe data</string>
<string name="wipe_embedded_sim_checkbox">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 you want to allow %1$s to trigger destructive panic actions\\?</string>
<string name="panic_app_unknown_app">an unknown app</string>
<string name="allow">Allow</string>
<string name="tile_label">Airplane mode</string>
<string name="shortcut_label">Panic</string>
<string name="wipe_on_inactivity_switch">Wipe on inactivity</string>
<string name="wipe_on_inactivity_description">Wipe a device when it was not unlocked for N time.</string>
<string name="wipe_on_inactivity_time_hint">time</string>
<string name="wipe_on_inactivity_time_error">7d / 48h / 120m</string>
<string name="wipe_on_inactivity_time_helper_text">[d]ays [h]ours [m]inutes</string>
<string name="trigger_lock_time_hint">time</string>
<string name="trigger_lock_time_error">7d / 48h / 120m</string>
<string name="trigger_lock_time_helper_text">How much time to wait. Modifiers: [d]ays [h]ours [m]inutes</string>
<string name="notification_channel_default_name">Default</string>
<string name="foreground_service_notification_title">Guard</string>
<string name="triggers_array_panic_kit">PanicKit</string>
<string name="triggers_array_tile">Tile</string>
<string name="triggers_array_shortcut">Shortcut</string>
<string name="triggers_array_broadcast">Broadcast</string>
<string name="triggers_array_notification">Notification</string>
<string name="triggers">Triggers</string>
<string name="trigger_panic_kit">PanicKit</string>
<string name="trigger_tile">Tile</string>
<string name="trigger_shortcut">Shortcut</string>
<string name="trigger_broadcast">Broadcast</string>
<string name="trigger_notification">Notification</string>
<string name="trigger_lock">Lock</string>
<string name="trigger_usb">USB</string>
<string name="copied_popup">Copied</string>
</resources>
<string name="main">Main</string>
<string name="settings">Settings</string>
<string name="trigger_panic_kit_description">Enable panic responder. PanicKit is a collection of tools for creating “panic buttons” that can trigger a system-wide response when the user is in an anxious or dangerous situation. It enables trigger apps and responder apps to safely and easily connect to each other. The user engages with the trigger app when in a panic situation. The responder apps receive that trigger signal, and individually execute the steps that they were configured to do.</string>
<string name="trigger_tile_description">Enable tile service. It is a button in quick settings panel when you swipe from the top of the screen. This button will mimic to the airplane mode. It has 2s delay if you will tap it accidentally.</string>
<string name="trigger_shortcut_description">Enable icon shortcut. It is a button you will see when you make a long tap on the Wasted icon.</string>
<string name="trigger_broadcast_description">Enable broadcast receiver. It is useful to communicate with another Android apps. For example you can fire Wasted from Tasker using this.</string>
<string name="trigger_notification_description">Enable device notification listener. It will scan all notifications it has access to for the secret code. When found it will fire. You have to give Wasted necessary permissions in Settings > Notifications > Device and app notifications.</string>
<string name="trigger_lock_description">Enable lock job scheduler. It will schedule a job every time you lock a device and cancel it every time you unlock a device. When you do not unlock a device for X time a job will fire.</string>
<string name="trigger_usb_description">Enable USB state receiver. When you make a USB data connection while a device is locked it will fire. It must not fire on charger, only on device and accessory.</string>
</resources>

View File

@ -1,6 +1,6 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Wasted" parent="Theme.Material3.DayNight">
<style name="Theme.Wasted" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorOnPrimary">@color/white</item>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-accessory />
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device />
</resources>

View File

@ -0,0 +1,5 @@
refresh ui
"Wipe on inactivity" is now a trigger named "Lock", it will respect "Wipe data/eSIM" checkboxes, you have to re-enable it if you used "Wipe on inactivity" before
merge USBKill to Wasted
there were many internal changes, if you will find bugs please let me know, thx

View File

@ -1,12 +1,12 @@
Lock a device and wipe its data on emergency.
You can use PanicKit, tile, shortcut or send a message with authentication code. On trigger, using
You can use PanicKit, tile, shortcut or send a message with a secret code. On trigger, using
Device Administration API, it locks a device and optionally runs wipe.
Also you can:
* wipe a device when it was not unlocked for N time
* wipe a device using a duress password (companion app: [Duress](https://github.com/x13a/Duress))
* wipe a device on USB connection event (companion app: [USBKill](https://github.com/x13a/USBKill))
* fire when a device was not unlocked for N time
* fire when a USB data connection is made while a device is locked
* fire when a duress password is entered (companion app: [Duress](https://github.com/x13a/Duress))
The app works in Work Profile too. Use Shelter to install risky apps and Wasted in it. Then you can
wipe this profile data with one click without wiping the whole device.
@ -15,8 +15,8 @@ Only encrypted device may guarantee that the data will not be recoverable.
Permissions:
* DEVICE_ADMIN - lock and optionally wipe a device
* FOREGROUND_SERVICE - receive unlock events
* RECEIVE_BOOT_COMPLETED - persist wipe job across reboots
* FOREGROUND_SERVICE - receive lock and USB state events
* RECEIVE_BOOT_COMPLETED - persist lock job and foreground service across reboots
It is Free Open Source Software.
License: GPL-3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStoreBase=GRADLE_USER_HOME

View File

@ -6,4 +6,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Wasted"
include ':app'
include ':app'