mirror of https://github.com/x13a/Wasted.git
add wipe on inactive
This commit is contained in:
parent
3d472f1550
commit
22c060e054
15
README.md
15
README.md
|
@ -17,8 +17,9 @@ with authentication code. On trigger, using
|
|||
[Device Administration API](https://developer.android.com/guide/topics/admin/device-admin), it
|
||||
locks device and optionally runs wipe.
|
||||
|
||||
Also you can limit the maximum number of failed password attempts. After the limit exceeded device
|
||||
will be wiped.
|
||||
Also you can:
|
||||
- limit the maximum number of failed password attempts
|
||||
- wipe device when it was not unlocked for N days
|
||||
|
||||
The app works in `Work Profile` too. You can 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
|
||||
|
@ -26,6 +27,16 @@ wiping the whole device.
|
|||
|
||||
Only encrypted device may guarantee that the data will not be recoverable.
|
||||
|
||||
## Permissions
|
||||
|
||||
`FOREGROUND_SERVICE`
|
||||
|
||||
[Wipe on inactive] receive unlock events
|
||||
|
||||
`RECEIVE_BOOT_COMPLETED`
|
||||
|
||||
[Wipe on inactive] persist wipe job across reboots
|
||||
|
||||
## Example
|
||||
|
||||
Broadcast message:
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="me.lucky.wasted">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
@ -90,5 +93,28 @@
|
|||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".WipeJobService"
|
||||
android:exported="true"
|
||||
android:description="@string/wipe_job_service_description"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".UnlockService"
|
||||
android:exported="false"
|
||||
android:description="@string/unlock_service_description">
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".RestartReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,25 @@
|
|||
package me.lucky.wasted
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
class AppNotificationManager(private val ctx: Context) {
|
||||
companion object {
|
||||
const val CHANNEL_DEFAULT_ID = "default"
|
||||
}
|
||||
|
||||
fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val notificationManager =
|
||||
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_DEFAULT_ID,
|
||||
ctx.getString(R.string.notification_channel_default_name),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
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 android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.util.*
|
||||
|
||||
|
@ -19,6 +21,7 @@ open class MainActivity : AppCompatActivity() {
|
|||
private val prefs by lazy { Preferences(this) }
|
||||
private val admin by lazy { DeviceAdminManager(this) }
|
||||
private val shortcut by lazy { ShortcutManager(this) }
|
||||
private val job by lazy { WipeJobManager(this) }
|
||||
|
||||
private val requestAdminPolicy =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
|
@ -51,6 +54,7 @@ open class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun init() {
|
||||
AppNotificationManager(this).createNotificationChannels()
|
||||
if (prefs.code == "") prefs.code = makeCode()
|
||||
updateCodeColorState()
|
||||
binding.apply {
|
||||
|
@ -60,6 +64,7 @@ open class MainActivity : AppCompatActivity() {
|
|||
wipeESIM.isChecked = prefs.isWipeESIM
|
||||
wipeESIM.isEnabled = wipeData.isChecked
|
||||
maxFailedPasswordAttempts.value = prefs.maxFailedPasswordAttempts.toFloat()
|
||||
wipeOnInactiveSwitch.isChecked = prefs.isWipeOnInactive
|
||||
toggle.isChecked = prefs.isServiceEnabled
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +91,19 @@ open class MainActivity : AppCompatActivity() {
|
|||
maxFailedPasswordAttempts.addOnChangeListener { _, value, _ ->
|
||||
prefs.maxFailedPasswordAttempts = value.toInt()
|
||||
}
|
||||
wipeOnInactiveSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (!setWipeOnInactiveComponentsState(prefs.isServiceEnabled && isChecked)) {
|
||||
wipeOnInactiveSwitch.isChecked = false
|
||||
showWipeJobServiceStartFailedPopup()
|
||||
return@setOnCheckedChangeListener
|
||||
}
|
||||
prefs.isWipeOnInactive = isChecked
|
||||
|
||||
}
|
||||
wipeOnInactiveSwitch.setOnLongClickListener {
|
||||
showWipeOnInactiveSettings()
|
||||
true
|
||||
}
|
||||
toggle.setOnCheckedChangeListener { _, isChecked ->
|
||||
when (isChecked) {
|
||||
true -> if (!admin.isActive()) requestAdmin() else setOn()
|
||||
|
@ -95,6 +113,23 @@ open class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showWipeOnInactiveSettings() {
|
||||
val items = arrayOf("1", "2", "3", "5", "7", "10", "15", "30")
|
||||
var days = prefs.wipeOnInactiveDays
|
||||
var checked = items.indexOf(days.toString())
|
||||
if (checked == -1) checked = items
|
||||
.indexOf(Preferences.DEFAULT_WIPE_ON_INACTIVE_DAYS.toString())
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.wipe_on_inactive_days)
|
||||
.setSingleChoiceItems(items, checked) { _, which ->
|
||||
days = items[which].toInt()
|
||||
}
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
prefs.wipeOnInactiveDays = days
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun updateCodeColorState() {
|
||||
binding.code.setBackgroundColor(getColor(
|
||||
if (prefs.isCodeEnabled) R.color.code_receiver_on else R.color.code_receiver_off
|
||||
|
@ -102,27 +137,60 @@ open class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun setOn() {
|
||||
if (!setWipeOnInactiveComponentsState(prefs.isWipeOnInactive)) {
|
||||
binding.toggle.isChecked = false
|
||||
showWipeJobServiceStartFailedPopup()
|
||||
return
|
||||
}
|
||||
prefs.isServiceEnabled = true
|
||||
setCodeReceiverState(prefs.isCodeEnabled)
|
||||
shortcut.push()
|
||||
}
|
||||
|
||||
private fun showWipeJobServiceStartFailedPopup() {
|
||||
Snackbar.make(
|
||||
findViewById(R.id.toggle),
|
||||
R.string.wipe_job_service_start_failed_popup,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun setOff() {
|
||||
prefs.isServiceEnabled = false
|
||||
setCodeReceiverState(false)
|
||||
setWipeOnInactiveComponentsState(false)
|
||||
shortcut.remove()
|
||||
admin.remove()
|
||||
}
|
||||
|
||||
private fun requestAdmin() = requestAdminPolicy.launch(admin.makeRequestIntent())
|
||||
private fun makeCode(): String = UUID.randomUUID().toString()
|
||||
private fun setCodeReceiverState(value: Boolean) =
|
||||
setReceiverState(CodeReceiver::class.java, value)
|
||||
private fun setRestartReceiverState(value: Boolean) =
|
||||
setReceiverState(RestartReceiver::class.java, value)
|
||||
|
||||
private fun setCodeReceiverState(value: Boolean) {
|
||||
private fun setReceiverState(cls: Class<*>, value: Boolean) {
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(this, CodeReceiver::class.java),
|
||||
ComponentName(this, cls),
|
||||
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUnlockServiceState(value: Boolean) {
|
||||
Intent(this, UnlockService::class.java).also {
|
||||
if (value) ContextCompat.startForegroundService(this, it) else stopService(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setWipeOnInactiveComponentsState(value: Boolean): Boolean {
|
||||
val result = job.setState(value)
|
||||
if (result) {
|
||||
setUnlockServiceState(value)
|
||||
setRestartReceiverState(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,16 @@ import androidx.security.crypto.MasterKeys
|
|||
|
||||
class Preferences(ctx: Context) {
|
||||
companion object {
|
||||
const val DEFAULT_WIPE_ON_INACTIVE_DAYS = 7
|
||||
|
||||
private const val SERVICE_ENABLED = "service_enabled"
|
||||
private const val CODE = "code"
|
||||
private const val CODE_ENABLED = "code_enabled"
|
||||
private const val WIPE_DATA = "wipe_data"
|
||||
private const val WIPE_ESIM = "wipe_esim"
|
||||
private const val MAX_FAILED_PASSWORD_ATTEMPTS = "max_failed_password_attempts"
|
||||
private const val WIPE_ON_INACTIVE = "wipe_on_inactive"
|
||||
private const val WIPE_ON_INACTIVE_DAYS = "wipe_on_inactive_days"
|
||||
|
||||
private const val FILE_NAME = "sec_shared_prefs"
|
||||
// migration
|
||||
|
@ -51,4 +55,12 @@ class Preferences(ctx: Context) {
|
|||
var maxFailedPasswordAttempts: Int
|
||||
get() = prefs.getInt(MAX_FAILED_PASSWORD_ATTEMPTS, 0)
|
||||
set(value) = prefs.edit { putInt(MAX_FAILED_PASSWORD_ATTEMPTS, value) }
|
||||
|
||||
var isWipeOnInactive: Boolean
|
||||
get() = prefs.getBoolean(WIPE_ON_INACTIVE, false)
|
||||
set(value) = prefs.edit { putBoolean(WIPE_ON_INACTIVE, value) }
|
||||
|
||||
var wipeOnInactiveDays: Int
|
||||
get() = prefs.getInt(WIPE_ON_INACTIVE_DAYS, DEFAULT_WIPE_ON_INACTIVE_DAYS)
|
||||
set(value) = prefs.edit { putInt(WIPE_ON_INACTIVE_DAYS, value) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package me.lucky.wasted
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class RestartReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED &&
|
||||
intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||
val prefs = Preferences(context)
|
||||
if (!prefs.isServiceEnabled || !prefs.isWipeOnInactive) return
|
||||
ContextCompat.startForegroundService(context, Intent(context, UnlockService::class.java))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
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 android.os.SystemClock
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
class UnlockService : Service() {
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1000
|
||||
}
|
||||
|
||||
private lateinit var receiver: BroadcastReceiver
|
||||
|
||||
private class UnlockReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val keyguardManager = context
|
||||
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
if (!keyguardManager.isDeviceSecure) return
|
||||
while (WipeJobManager(context).schedule() == JobScheduler.RESULT_FAILURE)
|
||||
SystemClock.sleep(1000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
receiver = UnlockReceiver()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
NotificationCompat.Builder(this, AppNotificationManager.CHANNEL_DEFAULT_ID)
|
||||
.setContentTitle(getString(R.string.unlock_service_notification_title))
|
||||
.setSmallIcon(android.R.drawable.ic_delete)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
)
|
||||
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package me.lucky.wasted
|
||||
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WipeJobManager(private val ctx: Context) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1000
|
||||
}
|
||||
private val prefs by lazy { Preferences(ctx) }
|
||||
private val jobScheduler by lazy {
|
||||
ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
}
|
||||
|
||||
fun schedule(): Int {
|
||||
return jobScheduler.schedule(
|
||||
JobInfo.Builder(JOB_ID, ComponentName(ctx, WipeJobService::class.java))
|
||||
.setMinimumLatency(TimeUnit.DAYS.toMillis(prefs.wipeOnInactiveDays.toLong()))
|
||||
.setBackoffCriteria(0, JobInfo.BACKOFF_POLICY_LINEAR)
|
||||
.setPersisted(true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun setState(value: Boolean): Boolean {
|
||||
if (value) {
|
||||
if (schedule() == JobScheduler.RESULT_FAILURE) return false
|
||||
} else { jobScheduler.cancel(JOB_ID) }
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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(this)
|
||||
if (!prefs.isServiceEnabled || !prefs.isWipeOnInactive) return false
|
||||
try {
|
||||
DeviceAdminManager(this).wipeData()
|
||||
} catch (exc: SecurityException) {}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -86,18 +86,30 @@
|
|||
android:valueTo="10"
|
||||
android:stepSize="1.0" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/max_failed_password_attempts_description" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/wipeOnInactiveSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/wipe_on_inactive_switch" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/wipe_on_inactive_description" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
|
|
@ -15,4 +15,13 @@
|
|||
<string name="shortcut_label">Паниковать</string>
|
||||
<string name="max_failed_password_attempts_description">Максимальное количество неудачных попыток ввода пароля.</string>
|
||||
<string name="max_failed_password_attempts_content_description">Количество</string>
|
||||
<string name="wipe_on_inactive_switch">Стереть при неактивности</string>
|
||||
<string name="wipe_on_inactive_description">Стереть данные когда устройство не разблокируется N дней.</string>
|
||||
<string name="wipe_on_inactive_days">Дней</string>
|
||||
<string name="notification_channel_default_name">Дефолт</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="wipe_job_service_description">Стереть данные при условии</string>
|
||||
<string name="wipe_job_service_start_failed_popup">Не удалось запустить службу стирания данных</string>
|
||||
<string name="unlock_service_description">Получать события разблокировки</string>
|
||||
<string name="unlock_service_notification_title">Служба Разблокировки</string>
|
||||
</resources>
|
||||
|
|
|
@ -15,4 +15,13 @@
|
|||
<string name="shortcut_label">Panic</string>
|
||||
<string name="max_failed_password_attempts_description">Maximum number of failed password attempts.</string>
|
||||
<string name="max_failed_password_attempts_content_description">Count</string>
|
||||
<string name="wipe_on_inactive_switch">Wipe on inactive</string>
|
||||
<string name="wipe_on_inactive_description">Wipe device when it was not unlocked for N days.</string>
|
||||
<string name="wipe_on_inactive_days">Days</string>
|
||||
<string name="notification_channel_default_name">Default</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="wipe_job_service_description">Wipe device on condition</string>
|
||||
<string name="wipe_job_service_start_failed_popup">Failed to start wipe service</string>
|
||||
<string name="unlock_service_description">Receive unlock events</string>
|
||||
<string name="unlock_service_notification_title">Unlock Service</string>
|
||||
</resources>
|
||||
|
|
|
@ -3,8 +3,9 @@ Lock device and wipe data on panic trigger.
|
|||
You can use PanicKit, tile, shortcut or send broadcast message with authentication code. On
|
||||
trigger, using Device Administration API, it locks device and optionally runs wipe.
|
||||
|
||||
Also you can limit the maximum number of failed password attempts. After the limit exceeded device
|
||||
will be wiped.
|
||||
Also you can:
|
||||
* limit the maximum number of failed password attempts
|
||||
* wipe device when it was not unlocked for N days
|
||||
|
||||
The app works in Work Profile too. You can 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.
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
сообщение с кодом аутентификации. При получении тревожного сигнала, используя API Администратора
|
||||
Устройства, программа заблокирует устройство и опционально запустит стирание данных.
|
||||
|
||||
Также Вы можете установить лимит на максимальное количество неудачных попыток ввода пароля. После
|
||||
исчерпания лимита данные на устройстве будут стёрты.
|
||||
Также Вы можете:
|
||||
* установить лимит на максимальное количество неудачных попыток ввода пароля
|
||||
* стереть данные когда устройство не разблокировалось N дней
|
||||
|
||||
Приложение также работает с Рабочим Профилем. Вы можете использовать Shelter для установки
|
||||
рискованных приложений и Потрачено в него. Затем Вы можете стереть данные этого профиля одним
|
||||
|
|
Loading…
Reference in New Issue