diff --git a/README.md b/README.md
index 17c29a9..a9e9998 100644
--- a/README.md
+++ b/README.md
@@ -24,3 +24,10 @@ your connection through volunteer proxies located in uncensored countries.
Similar to VPNs, which help users bypass Internet censorship, Snowflake disguises your Internet
activity as though you’re making a video or voice call, making you less detectable to Internet
censors.
+
+## Panic Kit Support
+
+This app responds to "Panic Button" apps such as [Ripple](https://github.com/guardianproject/ripple).
+If the panic button is triggered, the app will delete all data, reset user preferences, and hide
+itself has an app called "Plants" 🪴. If you open that Plants app, the app's name and icon will be
+restored.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3ebe536..fc008c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,15 +20,45 @@
+ android:launchMode="singleInstance">
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/io/bloco/snowflake/Dependencies.kt b/app/src/main/java/io/bloco/snowflake/Dependencies.kt
index 155dc60..eb01566 100644
--- a/app/src/main/java/io/bloco/snowflake/Dependencies.kt
+++ b/app/src/main/java/io/bloco/snowflake/Dependencies.kt
@@ -53,7 +53,7 @@ class Dependencies(
}
}
}
- private val appDatabase by lazy {
+ val appDatabase by lazy {
Room.databaseBuilder(app, AppDatabase::class.java, "snowflake").build()
}
private val statsDao by lazy { appDatabase.statsDao() }
diff --git a/app/src/main/java/io/bloco/snowflake/data/AppDataStore.kt b/app/src/main/java/io/bloco/snowflake/data/AppDataStore.kt
index 04f1782..5f55ce7 100644
--- a/app/src/main/java/io/bloco/snowflake/data/AppDataStore.kt
+++ b/app/src/main/java/io/bloco/snowflake/data/AppDataStore.kt
@@ -74,6 +74,12 @@ class AppDataStore(
suspend fun setStunServersDate(date: Instant) = dataStore().edit { it[STUN_SERVERS_DATE] = date.epochSeconds }
+ // Clear
+
+ suspend fun clear() {
+ dataStoreFlow().first().edit { it.clear() }
+ }
+
// Internal
@Suppress("ktlint:standard:backing-property-naming")
diff --git a/app/src/main/java/io/bloco/snowflake/data/database/AppDatabase.kt b/app/src/main/java/io/bloco/snowflake/data/database/AppDatabase.kt
index 1bf113f..534a844 100644
--- a/app/src/main/java/io/bloco/snowflake/data/database/AppDatabase.kt
+++ b/app/src/main/java/io/bloco/snowflake/data/database/AppDatabase.kt
@@ -4,6 +4,8 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import io.bloco.snowflake.models.DayStats
+import timber.log.Timber
+import java.io.File
@Database(
entities = [DayStats::class],
@@ -12,4 +14,13 @@ import io.bloco.snowflake.models.DayStats
@TypeConverters(LocalDateConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun statsDao(): StatsDao
+
+ fun clear() {
+ try {
+ if (isOpen) close()
+ openHelper.readableDatabase.path?.let { File(it).delete() }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to delete database")
+ }
+ }
}
diff --git a/app/src/main/java/io/bloco/snowflake/ui/Launchers.kt b/app/src/main/java/io/bloco/snowflake/ui/Launchers.kt
new file mode 100644
index 0000000..5dc02a6
--- /dev/null
+++ b/app/src/main/java/io/bloco/snowflake/ui/Launchers.kt
@@ -0,0 +1,37 @@
+package io.bloco.snowflake.ui
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.pm.PackageManager
+
+sealed interface Launcher
+
+object DefaultLauncher : Launcher
+
+object HiddenLauncher : Launcher
+
+val LAUNCHERS = listOf(DefaultLauncher, HiddenLauncher)
+
+fun Activity.setLauncher(launcher: Launcher) {
+ LAUNCHERS.forEach {
+ setLauncherEnabled(launcher = it, isEnabled = launcher == it)
+ }
+}
+
+private fun Activity.setLauncherEnabled(
+ launcher: Launcher,
+ isEnabled: Boolean,
+) {
+ application.packageManager.setComponentEnabledSetting(
+ ComponentName(
+ application,
+ launcher::class.java.name,
+ ),
+ if (isEnabled) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ },
+ PackageManager.DONT_KILL_APP,
+ )
+}
diff --git a/app/src/main/java/io/bloco/snowflake/ui/MainActivity.kt b/app/src/main/java/io/bloco/snowflake/ui/MainActivity.kt
index 8e3d79c..eb8fdc5 100644
--- a/app/src/main/java/io/bloco/snowflake/ui/MainActivity.kt
+++ b/app/src/main/java/io/bloco/snowflake/ui/MainActivity.kt
@@ -40,6 +40,7 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ setLauncher(DefaultLauncher)
setContent {
val viewModel = viewModel { dependencies.mainViewModel }
diff --git a/app/src/main/java/io/bloco/snowflake/ui/PanicResponseActivity.kt b/app/src/main/java/io/bloco/snowflake/ui/PanicResponseActivity.kt
new file mode 100644
index 0000000..71af79a
--- /dev/null
+++ b/app/src/main/java/io/bloco/snowflake/ui/PanicResponseActivity.kt
@@ -0,0 +1,34 @@
+package io.bloco.snowflake.ui
+
+import android.Manifest
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import info.guardianproject.panic.PanicResponder
+import io.bloco.snowflake.App
+import kotlinx.coroutines.runBlocking
+import kotlin.system.exitProcess
+
+/*
+ * If a Panic button is trigger, reset the app state as much as possible
+ */
+class PanicResponseActivity : ComponentActivity() {
+ private val dependencies by lazy { (applicationContext as App).dependencies }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (PanicResponder.shouldUseDefaultResponseToTrigger(this)) {
+ runBlocking {
+ dependencies.appDataStore.clear()
+ dependencies.appDatabase.clear()
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ setLauncher(HiddenLauncher)
+ exitProcess(0)
+ } else {
+ finish()
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_hidden_launcher_foreground.xml b/app/src/main/res/drawable/ic_hidden_launcher_foreground.xml
new file mode 100644
index 0000000..4f91ef1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_hidden_launcher_foreground.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi/ic_hidden_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_hidden_launcher.xml
new file mode 100644
index 0000000..cf03aef
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_hidden_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3353fb8..74db9a6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,10 @@
Snowflake
+ Plants
Close
+ Enabled
+ Disabled
Enable Snowflake. Help others bypass censorship.
Snowflake is Active. You’re helping bypass censorship.
@@ -68,7 +71,4 @@
Looking…
Helping %d
-
- Enabled
- Disabled
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0bd3bb0..cc4fb3c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -53,6 +53,7 @@ ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref =
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+panic = { module = "info.guardianproject.panic:panic", version = "1.0" }
[bundles]
kotlin = [
@@ -89,6 +90,7 @@ app = [
"ktor-json",
"room-runtime",
"room-ktx",
+ "panic",
]
ksp = [
"room-compiler",