diff --git a/README.md b/README.md index d3c55f7..9305f01 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index d7e69f7..5e82cf1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e241719..00bcc17 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + - - - - - - - - - - - - - @@ -76,18 +54,20 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -127,16 +156,5 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/Application.kt b/app/src/main/java/me/lucky/wasted/Application.kt index 67ec508..eb01f5e 100644 --- a/app/src/main/java/me/lucky/wasted/Application.kt +++ b/app/src/main/java/me/lucky/wasted/Application.kt @@ -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) } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt b/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt deleted file mode 100644 index e6c9705..0000000 --- a/app/src/main/java/me/lucky/wasted/DeviceAdminReceiver.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/me/lucky/wasted/ForegroundService.kt b/app/src/main/java/me/lucky/wasted/ForegroundService.kt deleted file mode 100644 index ef0c85b..0000000 --- a/app/src/main/java/me/lucky/wasted/ForegroundService.kt +++ /dev/null @@ -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() - } - } - } -} diff --git a/app/src/main/java/me/lucky/wasted/MainActivity.kt b/app/src/main/java/me/lucky/wasted/MainActivity.kt index 77b478c..6aed5c5 100644 --- a/app/src/main/java/me/lucky/wasted/MainActivity.kt +++ b/app/src/main/java/me/lucky/wasted/MainActivity.kt @@ -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) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/Preferences.kt b/app/src/main/java/me/lucky/wasted/Preferences.kt index 3f70fab..cf9d7a9 100644 --- a/app/src/main/java/me/lucky/wasted/Preferences.kt +++ b/app/src/main/java/me/lucky/wasted/Preferences.kt @@ -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), +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/TriggerReceiver.kt b/app/src/main/java/me/lucky/wasted/TriggerReceiver.kt index 67e11a1..c1c16c8 100644 --- a/app/src/main/java/me/lucky/wasted/TriggerReceiver.kt +++ b/app/src/main/java/me/lucky/wasted/TriggerReceiver.kt @@ -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) } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/Utils.kt b/app/src/main/java/me/lucky/wasted/Utils.kt new file mode 100644 index 0000000..5a865b7 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/Utils.kt @@ -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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/WipeJobService.kt b/app/src/main/java/me/lucky/wasted/WipeJobService.kt deleted file mode 100644 index ae50800..0000000 --- a/app/src/main/java/me/lucky/wasted/WipeJobService.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt b/app/src/main/java/me/lucky/wasted/admin/DeviceAdminManager.kt similarity index 96% rename from app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt rename to app/src/main/java/me/lucky/wasted/admin/DeviceAdminManager.kt index cb195d5..35a8403 100644 --- a/app/src/main/java/me/lucky/wasted/DeviceAdminManager.kt +++ b/app/src/main/java/me/lucky/wasted/admin/DeviceAdminManager.kt @@ -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) -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/admin/DeviceAdminReceiver.kt b/app/src/main/java/me/lucky/wasted/admin/DeviceAdminReceiver.kt new file mode 100644 index 0000000..2e11a9a --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/admin/DeviceAdminReceiver.kt @@ -0,0 +1,5 @@ +package me.lucky.wasted.admin + +import android.app.admin.DeviceAdminReceiver + +class DeviceAdminReceiver : DeviceAdminReceiver() \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/fragment/LockFragment.kt b/app/src/main/java/me/lucky/wasted/fragment/LockFragment.kt new file mode 100644 index 0000000..3a00582 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/fragment/LockFragment.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/fragment/MainFragment.kt b/app/src/main/java/me/lucky/wasted/fragment/MainFragment.kt new file mode 100644 index 0000000..60a2250 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/fragment/MainFragment.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/fragment/SettingsFragment.kt b/app/src/main/java/me/lucky/wasted/fragment/SettingsFragment.kt new file mode 100644 index 0000000..3b3d226 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/fragment/SettingsFragment.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/trigger/broadcast/BroadcastReceiver.kt b/app/src/main/java/me/lucky/wasted/trigger/broadcast/BroadcastReceiver.kt new file mode 100644 index 0000000..db79190 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/trigger/broadcast/BroadcastReceiver.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/WipeJobManager.kt b/app/src/main/java/me/lucky/wasted/trigger/lock/LockJobManager.kt similarity index 77% rename from app/src/main/java/me/lucky/wasted/WipeJobManager.kt rename to app/src/main/java/me/lucky/wasted/trigger/lock/LockJobManager.kt index 1f8d360..767a730 100644 --- a/app/src/main/java/me/lucky/wasted/WipeJobManager.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/lock/LockJobManager.kt @@ -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) -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/trigger/lock/LockJobService.kt b/app/src/main/java/me/lucky/wasted/trigger/lock/LockJobService.kt new file mode 100644 index 0000000..66be7a3 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/trigger/lock/LockJobService.kt @@ -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 } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/NotificationListenerService.kt b/app/src/main/java/me/lucky/wasted/trigger/notification/NotificationListenerService.kt similarity index 82% rename from app/src/main/java/me/lucky/wasted/NotificationListenerService.kt rename to app/src/main/java/me/lucky/wasted/trigger/notification/NotificationListenerService.kt index c91f95d..8b988dd 100644 --- a/app/src/main/java/me/lucky/wasted/NotificationListenerService.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/notification/NotificationListenerService.kt @@ -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) } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt b/app/src/main/java/me/lucky/wasted/trigger/panic/PanicConnectionActivity.kt similarity index 94% rename from app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt rename to app/src/main/java/me/lucky/wasted/trigger/panic/PanicConnectionActivity.kt index 964df8c..f1b9e6c 100644 --- a/app/src/main/java/me/lucky/wasted/PanicConnectionActivity.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/panic/PanicConnectionActivity.kt @@ -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() } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt b/app/src/main/java/me/lucky/wasted/trigger/panic/PanicResponderActivity.kt similarity index 85% rename from app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt rename to app/src/main/java/me/lucky/wasted/trigger/panic/PanicResponderActivity.kt index 460a49b..0b52129 100644 --- a/app/src/main/java/me/lucky/wasted/PanicResponderActivity.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/panic/PanicResponderActivity.kt @@ -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() } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/trigger/shared/ForegroundService.kt b/app/src/main/java/me/lucky/wasted/trigger/shared/ForegroundService.kt new file mode 100644 index 0000000..c0583b6 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/trigger/shared/ForegroundService.kt @@ -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) {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/NotificationManager.kt b/app/src/main/java/me/lucky/wasted/trigger/shared/NotificationManager.kt similarity index 90% rename from app/src/main/java/me/lucky/wasted/NotificationManager.kt rename to app/src/main/java/me/lucky/wasted/trigger/shared/NotificationManager.kt index 23e2c2d..f84e396 100644 --- a/app/src/main/java/me/lucky/wasted/NotificationManager.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/shared/NotificationManager.kt @@ -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()) } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/RestartReceiver.kt b/app/src/main/java/me/lucky/wasted/trigger/shared/RestartReceiver.kt similarity index 70% rename from app/src/main/java/me/lucky/wasted/RestartReceiver.kt rename to app/src/main/java/me/lucky/wasted/trigger/shared/RestartReceiver.kt index 10c1da1..64bde36 100644 --- a/app/src/main/java/me/lucky/wasted/RestartReceiver.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/shared/RestartReceiver.kt @@ -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), ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/ShortcutActivity.kt b/app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutActivity.kt similarity index 64% rename from app/src/main/java/me/lucky/wasted/ShortcutActivity.kt rename to app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutActivity.kt index 1d86a9c..2208e25 100644 --- a/app/src/main/java/me/lucky/wasted/ShortcutActivity.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutActivity.kt @@ -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() } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/ShortcutManager.kt b/app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutManager.kt similarity index 67% rename from app/src/main/java/me/lucky/wasted/ShortcutManager.kt rename to app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutManager.kt index b34058c..fb4689e 100644 --- a/app/src/main/java/me/lucky/wasted/ShortcutManager.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/shortcut/ShortcutManager.kt @@ -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)) +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/TileService.kt b/app/src/main/java/me/lucky/wasted/trigger/tile/TileService.kt similarity index 92% rename from app/src/main/java/me/lucky/wasted/TileService.kt rename to app/src/main/java/me/lucky/wasted/trigger/tile/TileService.kt index cba326f..75bcea5 100644 --- a/app/src/main/java/me/lucky/wasted/TileService.kt +++ b/app/src/main/java/me/lucky/wasted/trigger/tile/TileService.kt @@ -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() } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/wasted/trigger/usb/UsbReceiver.kt b/app/src/main/java/me/lucky/wasted/trigger/usb/UsbReceiver.kt new file mode 100644 index 0000000..7673f41 --- /dev/null +++ b/app/src/main/java/me/lucky/wasted/trigger/usb/UsbReceiver.kt @@ -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) {} + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_check_circle_24.xml b/app/src/main/res/drawable/ic_baseline_menu_24.xml similarity index 54% rename from app/src/main/res/drawable/ic_baseline_check_circle_24.xml rename to app/src/main/res/drawable/ic_baseline_menu_24.xml index b83d1bc..0c86f36 100644 --- a/app/src/main/res/drawable/ic_baseline_check_circle_24.xml +++ b/app/src/main/res/drawable/ic_baseline_menu_24.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6101854..27419ee 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,119 +1,45 @@ - - + android:layout_height="match_parent" + android:fitsSystemWindows="true"> - + android:layout_height="wrap_content"> - + android:layout_height="?attr/actionBarSize" + app:menu="@menu/top" + app:navigationIcon="@drawable/ic_baseline_menu_24" /> - + - + - + - + - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lock.xml b/app/src/main/res/layout/fragment_lock.xml new file mode 100644 index 0000000..c18c8db --- /dev/null +++ b/app/src/main/res/layout/fragment_lock.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..ff3db33 --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..1c11b72 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav.xml b/app/src/main/res/menu/nav.xml new file mode 100644 index 0000000..7c0aa2f --- /dev/null +++ b/app/src/main/res/menu/nav.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/top.xml similarity index 75% rename from app/src/main/res/menu/main.xml rename to app/src/main/res/menu/top.xml index d883b2b..a818682 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/top.xml @@ -3,9 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f07546b..08aa4cb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,24 +1,21 @@ Wasted - Geräteadministrator nicht verfügbar - Daten löschen - eSim löschen + Daten löschen + eSim löschen Bestätige Panik App Bist du sicher, dass du %1$s erlauben willst, destruktive Aktionen auslösen zu lassen\? eine unbekannte App Zulassen Flugmodus Panik - Bei Inaktivität löschen - Den Speicher des Geräts löschen, wenn es für N Tage nicht entsperrt wurde. + Den Speicher des Geräts löschen, wenn es für N Tage nicht entsperrt wurde. Standard Guard - PanicKit - Tile - Shortcut - Broadcast - Benachrichtigung - Auslöser + PanicKit + Tile + Shortcut + Broadcast + Benachrichtigung Kopiert diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index d7daf4d..5b18cc5 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -1,24 +1,21 @@ Wasted - Administrador de dispositivos no disponible - Borrar datos - Borrar eSIM + Borrar datos + Borrar eSIM Confirmar aplicación de pánico ¿Está seguro de que desea permitir que %1$s active acciones de pánico destructivas\? una aplicación desconocida Permitir Modo avión Pánico - Borrar por inactividad - Limpia un dispositivo cuando no fue desbloqueado por N días. + Limpia un dispositivo cuando no fue desbloqueado por N días. Predeterminado Guardia - PanicKit - Título - Acceso directo - Transmisión - Notificación - Activadores + PanicKit + Título + Acceso directo + Transmisión + Notificación Copiado diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 939f7ae..01f81b7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,27 +1,24 @@ Wasted - Amministrazione dispositivo non disponibile - Cancella i dati - Cancella eSIM + Cancella i dati + Cancella eSIM Conferma app di panico Sei sicuro di voler consentire a %1$s di attivare azioni di panico distruttive\? un\'app sconosciuta Consenti Modalità aereo Panico - Cancella in caso di inattività - Cancella i dati quando il dispositivo non viene sbloccato per N tempo. - tempo - 7d / 48h / 120m - [d]giorni [h]ore [m]minuti + Cancella i dati quando il dispositivo non viene sbloccato per N tempo. + tempo + 7d / 48h / 120m + [d]giorni [h]ore [m]minuti Predefinito Guardia - PanicKit - Toggle - Scorciatoia - Broadcast - Notifica - Attivatori + PanicKit + Toggle + Scorciatoia + Broadcast + Notifica Copiato diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index bef17fd..960ab3c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -