11package dev.pranav.applock.features.antiuninstall.ui
22
33import android.content.Context
4+ import android.content.Intent
45import android.content.pm.ApplicationInfo
56import android.content.pm.PackageManager
67import androidx.compose.foundation.Image
78import androidx.compose.foundation.clickable
8- import androidx.compose.foundation.layout.*
9+ import androidx.compose.foundation.layout.Arrangement
10+ import androidx.compose.foundation.layout.Box
11+ import androidx.compose.foundation.layout.Column
12+ import androidx.compose.foundation.layout.PaddingValues
13+ import androidx.compose.foundation.layout.Row
14+ import androidx.compose.foundation.layout.Spacer
15+ import androidx.compose.foundation.layout.fillMaxSize
16+ import androidx.compose.foundation.layout.fillMaxWidth
17+ import androidx.compose.foundation.layout.height
18+ import androidx.compose.foundation.layout.padding
19+ import androidx.compose.foundation.layout.size
20+ import androidx.compose.foundation.layout.width
21+ import androidx.compose.foundation.layout.windowInsetsPadding
922import androidx.compose.foundation.lazy.LazyColumn
1023import androidx.compose.foundation.lazy.items
1124import androidx.compose.foundation.shape.RoundedCornerShape
@@ -14,8 +27,27 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
1427import androidx.compose.material.icons.filled.Add
1528import androidx.compose.material.icons.filled.Search
1629import androidx.compose.material.icons.outlined.Shield
17- import androidx.compose.material3.*
18- import androidx.compose.runtime.*
30+ import androidx.compose.material3.AlertDialog
31+ import androidx.compose.material3.CircularProgressIndicator
32+ import androidx.compose.material3.ExperimentalMaterial3Api
33+ import androidx.compose.material3.Icon
34+ import androidx.compose.material3.IconButton
35+ import androidx.compose.material3.MaterialTheme
36+ import androidx.compose.material3.OutlinedTextField
37+ import androidx.compose.material3.Scaffold
38+ import androidx.compose.material3.Surface
39+ import androidx.compose.material3.Switch
40+ import androidx.compose.material3.Text
41+ import androidx.compose.material3.TextButton
42+ import androidx.compose.material3.TopAppBarDefaults
43+ import androidx.compose.runtime.Composable
44+ import androidx.compose.runtime.DisposableEffect
45+ import androidx.compose.runtime.LaunchedEffect
46+ import androidx.compose.runtime.collectAsState
47+ import androidx.compose.runtime.getValue
48+ import androidx.compose.runtime.mutableStateOf
49+ import androidx.compose.runtime.remember
50+ import androidx.compose.runtime.setValue
1951import androidx.compose.ui.Alignment
2052import androidx.compose.ui.Modifier
2153import androidx.compose.ui.draw.clip
@@ -26,6 +58,7 @@ import androidx.compose.ui.text.style.TextOverflow
2658import androidx.compose.ui.unit.dp
2759import androidx.compose.ui.window.DialogProperties
2860import androidx.core.graphics.drawable.toBitmap
61+ import androidx.core.net.toUri
2962import androidx.lifecycle.ViewModel
3063import androidx.lifecycle.viewModelScope
3164import androidx.lifecycle.viewmodel.compose.viewModel
@@ -41,6 +74,14 @@ import kotlinx.coroutines.flow.asStateFlow
4174import kotlinx.coroutines.launch
4275import kotlinx.coroutines.withContext
4376import rikka.shizuku.Shizuku
77+ import rikka.shizuku.ShizukuProvider
78+
79+ enum class ShizukuState {
80+ NOT_INSTALLED ,
81+ NOT_RUNNING , // Binder hasn't been received
82+ PERMISSION_DENIED ,
83+ READY
84+ }
4485
4586class AntiUninstallViewModel : ViewModel () {
4687 private val _allApps = MutableStateFlow <List <AppInfo >>(emptyList())
@@ -157,6 +198,22 @@ class AntiUninstallViewModel: ViewModel() {
157198 }
158199}
159200
201+ fun isShizukuInstalled (context : Context ): Boolean {
202+ return try {
203+ context.packageManager.getPackageInfo(ShizukuProvider .MANAGER_APPLICATION_ID , 0 )
204+ true
205+ } catch (e: PackageManager .NameNotFoundException ) {
206+ false
207+ }
208+ }
209+
210+ fun checkShizukuState (context : Context ): ShizukuState {
211+ if (! isShizukuInstalled(context)) return ShizukuState .NOT_INSTALLED
212+ if (! Shizuku .pingBinder()) return ShizukuState .NOT_RUNNING
213+ if (Shizuku .checkSelfPermission() == PackageManager .PERMISSION_DENIED ) return ShizukuState .PERMISSION_DENIED
214+ return ShizukuState .READY
215+ }
216+
160217@OptIn(ExperimentalMaterial3Api ::class )
161218@Composable
162219fun AntiUninstallScreen (
@@ -174,12 +231,33 @@ fun AntiUninstallScreen(
174231
175232 val showManualAddDialog = remember { mutableStateOf(false ) }
176233
234+ var shizukuState by remember { mutableStateOf(checkShizukuState(context)) }
235+
177236 LaunchedEffect (Unit ) {
178237 viewModel.loadApps(context)
179238 }
180239
181- val showMessage =
182- remember { mutableStateOf(Shizuku .checkSelfPermission() != PackageManager .PERMISSION_GRANTED ) }
240+ DisposableEffect (Unit ) {
241+ val binderReceivedListener = Shizuku .OnBinderReceivedListener {
242+ shizukuState = checkShizukuState(context)
243+ }
244+ val binderDeadListener = Shizuku .OnBinderDeadListener {
245+ shizukuState = checkShizukuState(context)
246+ }
247+ val permissionResultListener = Shizuku .OnRequestPermissionResultListener { _, _ ->
248+ shizukuState = checkShizukuState(context)
249+ }
250+
251+ Shizuku .addBinderReceivedListener(binderReceivedListener)
252+ Shizuku .addBinderDeadListener(binderDeadListener)
253+ Shizuku .addRequestPermissionResultListener(permissionResultListener)
254+
255+ onDispose {
256+ Shizuku .removeBinderReceivedListener(binderReceivedListener)
257+ Shizuku .removeBinderDeadListener(binderDeadListener)
258+ Shizuku .removeRequestPermissionResultListener(permissionResultListener)
259+ }
260+ }
183261
184262 Scaffold (
185263 modifier = Modifier .fillMaxSize(),
@@ -302,33 +380,79 @@ fun AntiUninstallScreen(
302380 )
303381 }
304382
305- if (showMessage.value ) {
383+ if (shizukuState != ShizukuState . READY ) {
306384 AlertDialog (
307- onDismissRequest = { showMessage.value = false },
308- properties = DialogProperties (usePlatformDefaultWidth = false ),
309- title = { Text (" Shizuku" ) },
385+ onDismissRequest = { /* Force response to use feature */ },
386+ properties = DialogProperties (
387+ usePlatformDefaultWidth = false ,
388+ dismissOnBackPress = false ,
389+ dismissOnClickOutside = false
390+ ),
391+ title = {
392+ Text (
393+ text = when (shizukuState) {
394+ ShizukuState .NOT_INSTALLED -> " Shizuku Not Installed"
395+ ShizukuState .NOT_RUNNING -> " Shizuku Not Running"
396+ ShizukuState .PERMISSION_DENIED -> " Permission Required"
397+ else -> " Shizuku Setup"
398+ }
399+ )
400+ },
310401 text = {
311402 Column (Modifier .fillMaxWidth(0.8f )) {
312403 Text (
313- text = " Please note that Shizuku must be installed and granted for this feature to work. You may revoke the permission later on, if you wish.\n\n Thankfully, Shizuku does not require root, and its only required while you Block/Unblock uninstalls, so you don't need it always running ;)" ,
314- style = MaterialTheme .typography.bodyMedium,
315- modifier = Modifier .padding(bottom = 16 .dp)
404+ text = when (shizukuState) {
405+ ShizukuState .NOT_INSTALLED -> " Shizuku is required to configure Anti-Uninstall protection without root.\n\n Please install Shizuku from the Play Store or GitHub to continue."
406+ ShizukuState .NOT_RUNNING -> " Shizuku is installed, but the background service has not been started yet (Binder hasn't been received).\n\n Please open the Shizuku application and start the service via Wireless Debugging or Root."
407+ ShizukuState .PERMISSION_DENIED -> " Shizuku is active, but this application needs your permission authorization to configure package restrictions.\n\n Thankfully, Shizuku is only used while blocking/unblocking uninstalls, so it doesn't need to stay running forever!"
408+ else -> " "
409+ },
410+ style = MaterialTheme .typography.bodyMedium
316411 )
317412 }
318413 },
319414 confirmButton = {
320415 TextButton (
321416 onClick = {
322- Shizuku .requestPermission(0 )
323- showMessage.value = false
417+ when (shizukuState) {
418+ ShizukuState .NOT_INSTALLED -> {
419+ val intent = Intent (
420+ Intent .ACTION_VIEW ,
421+ " https://play.google.com/store/apps/details?id=${ShizukuProvider .MANAGER_APPLICATION_ID } " .toUri()
422+ )
423+ context.startActivity(intent)
424+ }
425+
426+ ShizukuState .NOT_RUNNING -> {
427+ val launchIntent =
428+ context.packageManager.getLaunchIntentForPackage(ShizukuProvider .MANAGER_APPLICATION_ID )
429+ if (launchIntent != null ) {
430+ context.startActivity(launchIntent)
431+ }
432+ }
433+
434+ ShizukuState .PERMISSION_DENIED -> {
435+ Shizuku .requestPermission(0 )
436+ }
437+
438+ else -> {}
439+ }
324440 }
325- ) { Text (" Confirm" ) }
441+ ) {
442+ Text (
443+ text = when (shizukuState) {
444+ ShizukuState .NOT_INSTALLED -> " Install Shizuku"
445+ ShizukuState .NOT_RUNNING -> " Open Shizuku"
446+ ShizukuState .PERMISSION_DENIED -> " Grant Permission"
447+ else -> " Confirm"
448+ }
449+ )
450+ }
326451 },
327452 dismissButton = {
328453 TextButton (onClick = {
329454 navController.popBackStack()
330- showMessage.value = false
331- }) { Text (" Cancel" ) }
455+ }) { Text (" Go Back" ) }
332456 }
333457 )
334458 }
@@ -477,4 +601,4 @@ private fun ManualPackageItem(packageName: String, onToggle: () -> Unit) {
477601 Switch (checked = true , onCheckedChange = { onToggle() })
478602 }
479603 }
480- }
604+ }
0 commit comments