Skip to content

Commit 30e9701

Browse files
committed
improve uninstall screen
1 parent 95dcc6d commit 30e9701

15 files changed

Lines changed: 874 additions & 71 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
6+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
67

78
<uses-permission
89
android:name="android.permission.WRITE_EXTERNAL_STORAGE"

app/src/main/java/app/pwhs/universalinstaller/MainActivity.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package app.pwhs.universalinstaller
22

3+
import android.Manifest
4+
import android.content.pm.PackageManager
5+
import android.os.Build
36
import android.os.Bundle
47
import androidx.activity.ComponentActivity
58
import androidx.activity.compose.setContent
69
import androidx.activity.enableEdgeToEdge
10+
import androidx.activity.result.contract.ActivityResultContracts
11+
import androidx.core.content.ContextCompat
712
import androidx.compose.foundation.isSystemInDarkTheme
813
import androidx.compose.runtime.collectAsState
914
import androidx.compose.runtime.getValue
@@ -23,9 +28,14 @@ private enum class AppRoute { Splash, Onboarding, Main }
2328

2429
class MainActivity : ComponentActivity() {
2530

31+
private val notificationPermissionLauncher = registerForActivityResult(
32+
ActivityResultContracts.RequestPermission()
33+
) { /* granted or not — uninstall flow works either way, notifications just won't show */ }
34+
2635
override fun onCreate(savedInstanceState: Bundle?) {
2736
super.onCreate(savedInstanceState)
2837
enableEdgeToEdge()
38+
maybeRequestNotificationPermission()
2939
setContent {
3040
val themeModeFlow = dataStore.data.map { prefs ->
3141
val name = prefs[stringPreferencesKey("theme_mode")] ?: ThemeMode.System.name
@@ -55,4 +65,12 @@ class MainActivity : ComponentActivity() {
5565
}
5666
}
5767
}
68+
69+
private fun maybeRequestNotificationPermission() {
70+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
71+
val granted = ContextCompat.checkSelfPermission(
72+
this, Manifest.permission.POST_NOTIFICATIONS
73+
) == PackageManager.PERMISSION_GRANTED
74+
if (!granted) notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
75+
}
5876
}

app/src/main/java/app/pwhs/universalinstaller/data/local/AppDatabase.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,34 @@ package app.pwhs.universalinstaller.data.local
22

33
import androidx.room.Database
44
import androidx.room.RoomDatabase
5+
import androidx.room.migration.Migration
6+
import androidx.sqlite.db.SupportSQLiteDatabase
57

6-
@Database(entities = [InstallHistoryEntity::class], version = 1, exportSchema = false)
8+
@Database(
9+
entities = [InstallHistoryEntity::class, UninstallLogEntity::class],
10+
version = 2,
11+
exportSchema = false,
12+
)
713
abstract class AppDatabase : RoomDatabase() {
814
abstract fun installHistoryDao(): InstallHistoryDao
15+
abstract fun uninstallLogDao(): UninstallLogDao
16+
17+
companion object {
18+
val MIGRATION_1_2 = object : Migration(1, 2) {
19+
override fun migrate(db: SupportSQLiteDatabase) {
20+
db.execSQL(
21+
"""
22+
CREATE TABLE IF NOT EXISTS `uninstall_logs` (
23+
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
24+
`packageName` TEXT NOT NULL,
25+
`appName` TEXT NOT NULL,
26+
`success` INTEGER NOT NULL,
27+
`errorMessage` TEXT,
28+
`uninstalledAt` INTEGER NOT NULL
29+
)
30+
""".trimIndent()
31+
)
32+
}
33+
}
34+
}
935
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package app.pwhs.universalinstaller.data.local
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.Query
6+
import kotlinx.coroutines.flow.Flow
7+
8+
@Dao
9+
interface UninstallLogDao {
10+
@Insert
11+
suspend fun insert(entry: UninstallLogEntity)
12+
13+
@Query("SELECT * FROM uninstall_logs ORDER BY uninstalledAt DESC")
14+
fun getAll(): Flow<List<UninstallLogEntity>>
15+
16+
@Query("DELETE FROM uninstall_logs")
17+
suspend fun clearAll()
18+
19+
@Query("DELETE FROM uninstall_logs WHERE id = :id")
20+
suspend fun deleteById(id: Long)
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package app.pwhs.universalinstaller.data.local
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "uninstall_logs")
7+
data class UninstallLogEntity(
8+
@PrimaryKey(autoGenerate = true) val id: Long = 0,
9+
val packageName: String,
10+
val appName: String,
11+
val success: Boolean,
12+
val errorMessage: String? = null,
13+
val uninstalledAt: Long = System.currentTimeMillis(),
14+
)

app/src/main/java/app/pwhs/universalinstaller/di/module.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import app.pwhs.universalinstaller.domain.repository.SessionDataRepository
99
import app.pwhs.universalinstaller.presentation.install.InstallViewModel
1010
import app.pwhs.universalinstaller.presentation.setting.SettingViewModel
1111
import app.pwhs.universalinstaller.presentation.uninstall.UninstallViewModel
12+
import app.pwhs.universalinstaller.presentation.uninstall.logs.UninstallLogsViewModel
1213
import io.ktor.client.HttpClient
1314
import io.ktor.client.engine.cio.CIO
1415
import org.koin.core.module.dsl.bind
@@ -27,9 +28,11 @@ val appModule = module {
2728
// Room
2829
single {
2930
Room.databaseBuilder(get(), AppDatabase::class.java, "universal_installer.db")
31+
.addMigrations(AppDatabase.MIGRATION_1_2)
3032
.build()
3133
}
3234
single { get<AppDatabase>().installHistoryDao() }
35+
single { get<AppDatabase>().uninstallLogDao() }
3336

3437
// Ktor HttpClient
3538
single { HttpClient(CIO) }
@@ -38,4 +41,5 @@ val appModule = module {
3841
viewModelOf(::InstallViewModel)
3942
viewModelOf(::UninstallViewModel)
4043
viewModelOf(::SettingViewModel)
44+
viewModelOf(::UninstallLogsViewModel)
4145
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package app.pwhs.universalinstaller.presentation.composable
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.layout.size
7+
import androidx.compose.foundation.shape.RoundedCornerShape
8+
import androidx.compose.material.icons.Icons
9+
import androidx.compose.material.icons.rounded.AdminPanelSettings
10+
import androidx.compose.material.icons.rounded.Android
11+
import androidx.compose.material3.Icon
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.material3.Text
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.collectAsState
16+
import androidx.compose.runtime.getValue
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.draw.clip
21+
import androidx.compose.ui.platform.LocalContext
22+
import androidx.compose.ui.unit.dp
23+
import app.pwhs.universalinstaller.presentation.setting.PreferencesKeys
24+
import app.pwhs.universalinstaller.presentation.setting.dataStore
25+
import kotlinx.coroutines.flow.map
26+
27+
@Composable
28+
fun InstallerModeBadge(modifier: Modifier = Modifier) {
29+
val context = LocalContext.current
30+
val useShizukuFlow = remember(context) {
31+
context.dataStore.data.map { prefs -> prefs[PreferencesKeys.USE_SHIZUKU] ?: false }
32+
}
33+
val useShizuku by useShizukuFlow.collectAsState(initial = false)
34+
35+
val label = if (useShizuku) "Shizuku" else "Package Installer"
36+
val icon = if (useShizuku) Icons.Rounded.AdminPanelSettings else Icons.Rounded.Android
37+
val container = if (useShizuku)
38+
MaterialTheme.colorScheme.primaryContainer
39+
else
40+
MaterialTheme.colorScheme.surfaceContainerHigh
41+
val content = if (useShizuku)
42+
MaterialTheme.colorScheme.onPrimaryContainer
43+
else
44+
MaterialTheme.colorScheme.onSurfaceVariant
45+
46+
Row(
47+
modifier = modifier
48+
.clip(RoundedCornerShape(50))
49+
.background(container)
50+
.padding(horizontal = 10.dp, vertical = 4.dp),
51+
verticalAlignment = Alignment.CenterVertically,
52+
) {
53+
Icon(
54+
imageVector = icon,
55+
contentDescription = null,
56+
tint = content,
57+
modifier = Modifier.size(14.dp),
58+
)
59+
Text(
60+
text = "Using $label",
61+
style = MaterialTheme.typography.labelSmall,
62+
color = content,
63+
modifier = Modifier.padding(start = 6.dp),
64+
)
65+
}
66+
}

app/src/main/java/app/pwhs/universalinstaller/presentation/setting/SettingScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ private fun SettingUi(
124124
val shizukuStatusText = when (uiState.shizukuState) {
125125
ShizukuState.NOT_INSTALLED -> "Shizuku not installed"
126126
ShizukuState.NOT_RUNNING -> "Shizuku installed but not running"
127+
ShizukuState.UNSUPPORTED -> "Shizuku version too old (pre-v11)"
127128
ShizukuState.NO_PERMISSION -> "Tap to grant Shizuku permission"
128129
ShizukuState.READY -> "Silent install without prompts"
129130
}

0 commit comments

Comments
 (0)