recast option

This commit is contained in:
lucky 2022-08-30 02:43:07 +03:00
parent 6fc8fa382b
commit 957bec3a33
20 changed files with 295 additions and 101 deletions

View File

@ -19,7 +19,8 @@ Lock a device and wipe its data on emergency.
You can use [PanicKit](https://guardianproject.info/code/panickit/), tile, shortcut or send a
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.
locks a device and optionally runs wipe (factory reset). Also it can send a broadcast message
instead of the wipe.
Also you can:
* fire when a device was not unlocked for X time

View File

@ -10,8 +10,8 @@ android {
applicationId "me.lucky.wasted"
minSdk 23
targetSdk 32
versionCode 38
versionName "1.5.9"
versionCode 39
versionName "1.5.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -144,7 +144,7 @@ open class MainActivity : AppCompatActivity() {
R.id.nav_trigger_notification -> NotificationFragment()
R.id.nav_trigger_lock -> LockFragment()
R.id.nav_trigger_application -> ApplicationFragment()
R.id.top_settings -> SettingsFragment()
R.id.nav_recast -> RecastFragment()
else -> MainFragment()
}

View File

@ -19,6 +19,12 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
private const val WIPE_DATA = "wipe_data"
private const val WIPE_EMBEDDED_SIM = "wipe_embedded_sim"
private const val RECAST_ENABLED = "recast_enabled"
private const val RECAST_ACTION = "recast_action"
private const val RECAST_RECEIVER = "recast_receiver"
private const val RECAST_EXTRA_KEY = "recast_extra_key"
private const val RECAST_EXTRA_VALUE = "recast_extra_value"
private const val TRIGGERS = "triggers"
private const val TRIGGER_LOCK_COUNT = "trigger_lock_count"
private const val TRIGGER_TILE_DELAY = "trigger_tile_delay"
@ -90,6 +96,26 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
get() = prefs.getInt(TRIGGER_APPLICATION_OPTIONS, 0)
set(value) = prefs.edit { putInt(TRIGGER_APPLICATION_OPTIONS, value) }
var isRecastEnabled: Boolean
get() = prefs.getBoolean(RECAST_ENABLED, false)
set(value) = prefs.edit { putBoolean(RECAST_ENABLED, value) }
var recastAction: String
get() = prefs.getString(RECAST_ACTION, "") ?: ""
set(value) = prefs.edit { putString(RECAST_ACTION, value) }
var recastReceiver: String
get() = prefs.getString(RECAST_RECEIVER, "") ?: ""
set(value) = prefs.edit { putString(RECAST_RECEIVER, value) }
var recastExtraKey: String
get() = prefs.getString(RECAST_EXTRA_KEY, "") ?: ""
set(value) = prefs.edit { putString(RECAST_EXTRA_KEY, value) }
var recastExtraValue: String
get() = prefs.getString(RECAST_EXTRA_VALUE, "") ?: ""
set(value) = prefs.edit { putString(RECAST_EXTRA_VALUE, value) }
fun registerListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.registerOnSharedPreferenceChangeListener(listener)

View File

@ -1,5 +1,6 @@
package me.lucky.wasted
import android.app.KeyguardManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -7,6 +8,7 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.trigger.notification.NotificationListenerService
import me.lucky.wasted.trigger.panic.PanicConnectionActivity
import me.lucky.wasted.trigger.panic.PanicResponderActivity
@ -26,10 +28,10 @@ class Utils(private val ctx: Context) {
}
}
private val shortcut by lazy { ShortcutManager(ctx) }
private val prefs by lazy { Preferences.new(ctx) }
fun setEnabled(enabled: Boolean) {
val triggers = Preferences(ctx).triggers
val triggers = prefs.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)
@ -50,6 +52,7 @@ class Utils(private val ctx: Context) {
}
fun setShortcutEnabled(enabled: Boolean) {
val shortcut = ShortcutManager(ctx)
if (!enabled) shortcut.remove()
setComponentEnabled(ShortcutActivity::class.java, enabled)
if (enabled) shortcut.push()
@ -63,7 +66,6 @@ class Utils(private val ctx: Context) {
fun updateApplicationEnabled() {
val prefix = "${ctx.packageName}.trigger.application"
val prefs = Preferences(ctx)
val options = prefs.triggerApplicationOptions
val enabled = prefs.isEnabled && prefs.triggers.and(Trigger.APPLICATION.value) != 0
setComponentEnabled(
@ -85,7 +87,6 @@ class Utils(private val ctx: Context) {
}
fun updateForegroundRequiredEnabled() {
val prefs = Preferences(ctx)
val enabled = prefs.isEnabled
val triggers = prefs.triggers
val isUSB = triggers.and(Trigger.USB.value) != 0
@ -114,4 +115,36 @@ class Utils(private val ctx: Context) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP,
)
fun fire(trigger: Trigger, safe: Boolean = true) {
if (!prefs.isEnabled || prefs.triggers.and(trigger.value) == 0) return
val admin = DeviceAdminManager(ctx)
try {
admin.lockNow()
if (prefs.isWipeData && safe) admin.wipeData()
} catch (exc: SecurityException) {}
if (prefs.isRecastEnabled && safe) recast()
}
fun isDeviceLocked() = ctx.getSystemService(KeyguardManager::class.java).isDeviceLocked
private fun recast() {
val action = prefs.recastAction
if (action.isEmpty()) return
ctx.sendBroadcast(Intent(action).apply {
val cls = prefs.recastReceiver.split('/')
val packageName = cls.firstOrNull() ?: ""
if (packageName.isNotEmpty()) {
setPackage(packageName)
if (cls.size == 2)
setClassName(
packageName,
"$packageName.${cls[1].trimStart('.')}",
)
}
addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
val extraKey = prefs.recastExtraKey
if (extraKey.isNotEmpty()) putExtra(extraKey, prefs.recastExtraValue)
})
}
}

View File

@ -0,0 +1,76 @@
package me.lucky.wasted.fragment
import android.content.Context
import android.content.SharedPreferences
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 me.lucky.wasted.Preferences
import me.lucky.wasted.databinding.FragmentRecastBinding
class RecastFragment : Fragment() {
private lateinit var binding: FragmentRecastBinding
private lateinit var ctx: Context
private lateinit var prefs: Preferences
private lateinit var prefsdb: Preferences
private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
prefs.copyTo(prefsdb, key)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentRecastBinding.inflate(inflater, container, false)
init()
setup()
return binding.root
}
override fun onStart() {
super.onStart()
prefs.registerListener(prefsListener)
}
override fun onStop() {
super.onStop()
prefs.unregisterListener(prefsListener)
}
private fun init() {
ctx = requireContext()
prefs = Preferences(ctx)
prefsdb = Preferences(ctx, encrypted = false)
binding.apply {
enabled.isChecked = prefs.isRecastEnabled
action.editText?.setText(prefs.recastAction)
receiver.editText?.setText(prefs.recastReceiver)
extraKey.editText?.setText(prefs.recastExtraKey)
extraValue.editText?.setText(prefs.recastExtraValue)
}
}
private fun setup() = binding.apply {
enabled.setOnCheckedChangeListener { _, isChecked ->
prefs.isRecastEnabled = isChecked
}
action.editText?.doAfterTextChanged {
prefs.recastAction = it?.toString()?.trim() ?: return@doAfterTextChanged
}
receiver.editText?.doAfterTextChanged {
prefs.recastReceiver = it?.toString()?.trim() ?: return@doAfterTextChanged
}
extraKey.editText?.doAfterTextChanged {
prefs.recastExtraKey = it?.toString()?.trim() ?: return@doAfterTextChanged
}
extraValue.editText?.doAfterTextChanged {
prefs.recastExtraValue = it?.toString()?.trim() ?: return@doAfterTextChanged
}
}
}

View File

@ -3,24 +3,13 @@ package me.lucky.wasted.trigger.application
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Utils
class ApplicationActivity : AppCompatActivity() {
private val prefs by lazy { Preferences(this) }
private val admin by lazy { DeviceAdminManager(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!prefs.isEnabled || prefs.triggers.and(Trigger.APPLICATION.value) == 0) {
finishAndRemoveTask()
return
}
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
Utils(this).fire(Trigger.APPLICATION)
finishAndRemoveTask()
}
}

View File

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

View File

@ -3,19 +3,12 @@ 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
import me.lucky.wasted.Utils
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) {}
Utils(this).fire(Trigger.LOCK)
return false
}

View File

@ -5,13 +5,13 @@ 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
import me.lucky.wasted.Utils
class NotificationListenerService : NotificationListenerService() {
private lateinit var prefs: Preferences
private lateinit var admin: DeviceAdminManager
private lateinit var utils: Utils
override fun onCreate() {
super.onCreate()
@ -20,22 +20,17 @@ class NotificationListenerService : NotificationListenerService() {
private fun init() {
prefs = Preferences.new(this)
admin = DeviceAdminManager(this)
utils = Utils(this)
}
override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
if (sbn == null ||
!prefs.isEnabled ||
prefs.triggers.and(Trigger.NOTIFICATION.value) == 0) return
if (sbn == null) return
val secret = prefs.secret
assert(secret.isNotEmpty())
if (sbn.notification.extras[Notification.EXTRA_TEXT]?.toString() != secret) return
if (sbn.notification.extras[Notification.EXTRA_TEXT]?.toString()?.trim() != secret) return
cancelAllNotifications()
try {
admin.lockNow()
if (prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
utils.fire(Trigger.NOTIFICATION)
}
override fun onListenerConnected() {

View File

@ -5,28 +5,20 @@ 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
import me.lucky.wasted.Utils
class PanicResponderActivity : AppCompatActivity() {
private val prefs by lazy { Preferences.new(this) }
private val admin by lazy { DeviceAdminManager(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!Panic.isTriggerIntent(intent) ||
!prefs.isEnabled ||
prefs.triggers.and(Trigger.PANIC_KIT.value) == 0)
{
if (!Panic.isTriggerIntent(intent)) {
finishAndRemoveTask()
return
}
try {
admin.lockNow()
if (PanicResponder.receivedTriggerFromConnectedApp(this) &&
prefs.isWipeData) admin.wipeData()
} catch (exc: SecurityException) {}
Utils(this).fire(
Trigger.PANIC_KIT,
PanicResponder.receivedTriggerFromConnectedApp(this),
)
finishAndRemoveTask()
}
}

View File

@ -13,7 +13,7 @@ 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.Utils
import me.lucky.wasted.trigger.lock.LockJobManager
class ForegroundService : Service() {
@ -113,17 +113,11 @@ class ForegroundService : Service() {
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 utils = Utils(context ?: return)
if (!utils.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) {}
utils.fire(Trigger.USB)
}
}
}

View File

@ -3,15 +3,13 @@ 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)
if (Preferences(this).triggers.and(Trigger.SHORTCUT.value) != 0)
BroadcastReceiver.panic(this, intent)
BroadcastReceiver.panic(this, intent, Trigger.SHORTCUT)
finishAndRemoveTask()
}
}

View File

@ -10,11 +10,13 @@ import kotlin.concurrent.timerTask
import me.lucky.wasted.admin.DeviceAdminManager
import me.lucky.wasted.Preferences
import me.lucky.wasted.Trigger
import me.lucky.wasted.Utils
@RequiresApi(Build.VERSION_CODES.N)
class TileService : TileService() {
private lateinit var prefs: Preferences
private lateinit var admin: DeviceAdminManager
private lateinit var utils: Utils
private var counter = 0
private var timer: Timer? = null
@ -26,6 +28,7 @@ class TileService : TileService() {
private fun init() {
prefs = Preferences.new(this)
admin = DeviceAdminManager(this)
utils = Utils(this)
}
override fun onStartListening() {
@ -38,11 +41,8 @@ class TileService : TileService() {
override fun onClick() {
super.onClick()
if (!prefs.isEnabled || prefs.triggers.and(Trigger.TILE.value) == 0) return
if (!prefs.isWipeData) {
try {
admin.lockNow()
} catch (exc: SecurityException) {}
utils.fire(Trigger.TILE, false)
return
}
val v = counter
@ -53,10 +53,7 @@ class TileService : TileService() {
timer?.cancel()
timer = Timer()
timer?.schedule(timerTask {
try {
admin.lockNow()
admin.wipeData()
} catch (exc: SecurityException) {}
utils.fire(Trigger.TILE)
}, prefs.triggerTileDelay)
}
else -> {

View File

@ -1,27 +1,19 @@
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
import me.lucky.wasted.Utils
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) {}
val utils = Utils(context ?: return)
if (!utils.isDeviceLocked()) return
utils.fire(Trigger.USB)
}
}

View File

@ -0,0 +1,101 @@
<?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.RecastFragment">
<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.switchmaterial.SwitchMaterial
android:id="@+id/enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:text="@string/enable" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/recast_description"
android:textAppearance="?attr/textAppearanceBodySmall" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="10dp"
android:background="?android:attr/listDivider" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/action">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/receiver"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/receiver">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/extraKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/extra_key">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/extraValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/extra_value">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -29,4 +29,9 @@
</group>
<item
android:id="@+id/nav_recast"
android:title="@string/recast"
android:checkable="true" />
</menu>

View File

@ -44,4 +44,11 @@
<string name="telegram">Telegram</string>
<string name="threema">Threema</string>
<string name="session">Session</string>
<string name="recast">Recast</string>
<string name="recast_description">Instead of wipe data you may want to send a broadcast message.</string>
<string name="enable">Enable</string>
<string name="action">action*</string>
<string name="receiver">receiver</string>
<string name="extra_key">extra key</string>
<string name="extra_value">extra value</string>
</resources>

View File

@ -0,0 +1 @@
recast option

View File

@ -1,5 +1,6 @@
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.
Device Administration API, it locks a device and optionally runs wipe (factory reset). Also it can
send a broadcast message instead of the wipe.
Also you can:
* fire when a device was not unlocked for X time