feat: app selector screen

This commit is contained in:
CnC-Robert 2023-05-06 12:42:30 +02:00
parent d3dbe33262
commit 01847c901c
No known key found for this signature in database
GPG Key ID: C58ED617AEA8CB68
17 changed files with 575 additions and 113 deletions

View File

@ -46,29 +46,29 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-compose:1.7.1")
implementation("androidx.paging:paging-common-ktx:3.1.1")
// Compose
implementation(platform("androidx.compose:compose-bom:2023.04.01"))
implementation(platform("androidx.compose:compose-bom:2023.05.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.paging:paging-common-ktx:3.1.1")
implementation("androidx.core:core-ktx:1.10.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3:1.1.0-rc01")
// Accompanist
val accompanistVersion = "0.30.1"
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
//val accompanistVersion = "0.30.1"
//implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
// Coil (async image loading, network image)
implementation("io.coil-kt:coil-compose:2.2.2")
// KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
// Material 3
implementation("androidx.compose.material3:material3")
// ReVanced
implementation("app.revanced:revanced-patcher:7.0.0")
@ -86,4 +86,5 @@ dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}
}

View File

@ -38,7 +38,7 @@
</intent-filter>
</activity>
<service android:name=".installer.service.InstallService" />
<service android:name=".installer.service.UninstallService" />
<service android:name=".service.InstallService" />
<service android:name=".service.UninstallService" />
</application>
</manifest>

View File

@ -6,24 +6,38 @@ import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.compose.destination.Destination
import app.revanced.manager.compose.domain.manager.PreferencesManager
import app.revanced.manager.compose.ui.destination.Destination
import app.revanced.manager.compose.ui.screen.AppSelectorScreen
import app.revanced.manager.compose.ui.screen.DashboardScreen
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
import app.revanced.manager.compose.ui.theme.Theme
import app.revanced.manager.compose.util.PM
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.rememberNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
private val prefs: PreferencesManager by inject()
private val mainScope = MainScope()
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
val context = this
mainScope.launch(Dispatchers.IO) {
PM.loadApps(context)
}
setContent {
ReVancedManagerTheme(
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
@ -34,12 +48,18 @@ class MainActivity : ComponentActivity() {
NavBackHandler(navController)
AnimatedNavHost(
controller = navController,
controller = navController
) { destination ->
when (destination) {
is Destination.Dashboard -> {
DashboardScreen()
}
is Destination.Dashboard -> { DashboardScreen(
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }
) }
is Destination.AppSelector -> AppSelectorScreen(
onBackClick = { navController.pop() }
)
}
}
}

View File

@ -1,68 +0,0 @@
package app.revanced.manager.compose.installer.utils
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import app.revanced.manager.compose.installer.service.InstallService
import app.revanced.manager.compose.installer.service.UninstallService
import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
object PM {
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
session.writeApk(apk)
session.commit(context.installIntentSender)
session.close()
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
}
}
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, byteArraySize)
fsync(outputStream)
}
}
}
private val intentFlags
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_MUTABLE
else
0
private val sessionParams
get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
private val Context.installIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, InstallService::class.java),
intentFlags
).intentSender
private val Context.uninstallIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, UninstallService::class.java),
intentFlags
).intentSender

View File

@ -1,4 +1,4 @@
package app.revanced.manager.compose.installer.service
package app.revanced.manager.compose.service
import android.app.Service
import android.content.Intent

View File

@ -1,4 +1,4 @@
package app.revanced.manager.compose.installer.service
package app.revanced.manager.compose.service
import android.app.Service
import android.content.Intent

View File

@ -0,0 +1,41 @@
package app.revanced.manager.compose.ui.component
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Android
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
@Composable
fun AppIcon(
drawable: Drawable?,
contentDescription: String?,
size: Int = 48
) {
if (drawable == null) {
val image = rememberVectorPainter(Icons.Default.Android)
val colorFilter = ColorFilter.tint(LocalContentColor.current)
Image(
image,
contentDescription,
Modifier.size(size.dp),
colorFilter = colorFilter
)
} else {
val image = rememberAsyncImagePainter(drawable)
Image(
image,
contentDescription,
Modifier.size(size.dp)
)
}
}

View File

@ -0,0 +1,60 @@
package app.revanced.manager.compose.ui.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppScaffold(
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar(scrollBehavior) },
bottomBar = bottomBar,
floatingActionButton = floatingActionButton,
content = content
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
title: String,
onBackClick: (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
TopAppBar(
title = { Text(title) },
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null
)
}
}
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor
)
)
}

View File

@ -0,0 +1,31 @@
package app.revanced.manager.compose.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
@Composable
fun LoadingIndicator(progress: Float? = null, text: Int? = R.string.loading_body) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (text != null)
Text(stringResource(text))
if (progress == null) {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
} else {
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp))
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.manager.compose.destination
package app.revanced.manager.compose.ui.destination
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@ -8,4 +8,7 @@ sealed interface Destination: Parcelable {
@Parcelize
object Dashboard: Destination
@Parcelize
object AppSelector: Destination
}

View File

@ -0,0 +1,178 @@
package app.revanced.manager.compose.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppIcon
import app.revanced.manager.compose.ui.component.AppScaffold
import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.component.LoadingIndicator
import app.revanced.manager.compose.util.PM
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppSelectorScreen(
onBackClick: () -> Unit
) {
var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) }
// TODO: find something better for this
if (search) {
SearchBar(
query = filterText,
onQueryChange = { filterText = it },
onSearch = { },
active = true,
onActiveChange = { search = it },
modifier = Modifier.fillMaxSize(),
placeholder = { Text(stringResource(R.string.search_apps)) },
leadingIcon = { IconButton({ search = false }) { Icon(Icons.Default.ArrowBack, null) } },
shape = SearchBarDefaults.inputFieldShape,
content = {
if (PM.appList.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(
PM.appList
.filter { app ->
(app.label.contains(
filterText,
true
) or app.packageName.contains(filterText, true))
}
) { app ->
ListItem(
modifier = Modifier.clickable { },
leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) },
supportingContent = { Text(app.packageName) },
trailingContent = { Text((PM.testList[app.packageName]?: 0).let { if (it == 1) "$it Patch" else "$it Patches" }) }
)
}
}
} else {
LoadingIndicator()
}
}
)
}
AppScaffold(
topBar = {
AppTopBar(
title = "Select an app",
onBackClick = onBackClick,
actions = {
IconButton({}) {
Icon(Icons.Outlined.HelpOutline, "Help")
}
IconButton(onClick = { search = true }) {
Icon(Icons.Outlined.Search, "Search")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
if (PM.supportedAppList.isNotEmpty()) {
/*Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
FilterChip(
selected = false,
onClick = {},
label = { Text("Patched apps") },
leadingIcon = { Icon(Icons.Default.Check, null) },
enabled = false
)
FilterChip(
selected = false,
onClick = {},
label = { Text("User apps") },
leadingIcon = { Icon(Icons.Default.Android, null) }
)
FilterChip(
selected = filterSystemApps,
onClick = { filterSystemApps = !filterSystemApps },
label = { Text("System apps") },
leadingIcon = { Icon(Icons.Default.Apps, null) }
)
}*/
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
ListItem(
modifier = Modifier.clickable { },
leadingContent = { Box(Modifier.size(36.dp), Alignment.Center) { Icon(Icons.Default.Storage, null, modifier = Modifier.size(24.dp)) } },
headlineContent = { Text("Select from storage") }
)
Divider()
}
(PM.appList.ifEmpty { PM.supportedAppList }).also { list ->
items(
count = list.size,
key = { list[it].packageName }
) { index ->
val app = list[index]
ListItem(
modifier = Modifier.clickable { },
leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) },
supportingContent = { Text(app.packageName) },
trailingContent = {
Text(
(PM.testList[app.packageName]?: 0).let { if (it == 1) "$it Patch" else "$it Patches" }
)
}
)
}
if (PM.appList.isEmpty()) {
item {
Box(Modifier.fillMaxWidth(), Alignment.Center) {
CircularProgressIndicator(Modifier.padding(vertical = 15.dp).size(24.dp), strokeWidth = 3.dp)
}
}
}
}
}
} else {
LoadingIndicator()
}
}
}
}

View File

@ -15,16 +15,18 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppScaffold
import app.revanced.manager.compose.ui.component.AppTopBar
import kotlinx.coroutines.launch
enum class DashboardPage(
@ -37,16 +39,18 @@ enum class DashboardPage(
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen() {
fun DashboardScreen(
onAppSelectorClick: () -> Unit
) {
val pages: Array<DashboardPage> = DashboardPage.values()
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
Scaffold(
AppScaffold(
topBar = {
TopAppBar(
title = { Text("ReVanced Manager") },
AppTopBar(
title = "ReVanced Manager",
actions = {
IconButton(onClick = {}) {
Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
@ -61,13 +65,20 @@ fun DashboardScreen() {
)
},
floatingActionButton = {
FloatingActionButton(onClick = {}) {
FloatingActionButton(onClick = {
if (pagerState.currentPage == DashboardPage.DASHBOARD.ordinal)
onAppSelectorClick()
}
) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
}
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
TabRow(selectedTabIndex = pagerState.currentPage) {
TabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {
pages.forEachIndexed { index, page ->
val title = stringResource(id = page.titleResId)
Tab(

View File

@ -2,22 +2,20 @@ package app.revanced.manager.compose.ui.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import app.revanced.manager.compose.R
@Composable
fun InstalledAppsScreen() {
Box(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_patched_apps_found),
fontSize = 24.sp,
modifier = Modifier
.align(alignment = Alignment.Center)
style = MaterialTheme.typography.titleLarge
)
}
}

View File

@ -2,22 +2,20 @@ package app.revanced.manager.compose.ui.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import app.revanced.manager.compose.R
@Composable
fun SourcesScreen() {
Box(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_sources_set),
fontSize = 24.sp,
modifier = Modifier
.align(alignment = Alignment.Center)
style = MaterialTheme.typography.titleLarge
)
}
}

View File

@ -2,14 +2,14 @@ package app.revanced.manager.compose.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
@ -25,8 +25,8 @@ private val LightColorScheme = lightColorScheme(
@Composable
fun ReVancedManagerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
darkTheme: Boolean,
dynamicColor: Boolean,
content: @Composable () -> Unit
) {
val colorScheme = when {
@ -37,11 +37,19 @@ fun ReVancedManagerTheme(
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
val activity = view.context as Activity
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
activity.window.navigationBarColor = colorScheme.background.toArgb()
activity.window.statusBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(activity.window, view).isAppearanceLightStatusBars = !darkTheme
WindowCompat.getInsetsController(activity.window, view).isAppearanceLightNavigationBars = !darkTheme
}
}

View File

@ -0,0 +1,161 @@
package app.revanced.manager.compose.util
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Parcelable
import androidx.compose.runtime.mutableStateListOf
import app.revanced.manager.compose.service.InstallService
import app.revanced.manager.compose.service.UninstallService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
@SuppressLint("QueryPermissionsNeeded")
@Suppress("DEPRECATION")
object PM {
val testList = mapOf(
"com.google.android.youtube" to 59,
"com.android.vending" to 34,
"com.backdrops.wallpapers" to 2,
"com.termux" to 2,
"com.notinstalled.app" to 1,
"com.2notinstalled.app" to 1,
"org.adaway" to 5,
"com.activitymanager" to 1,
"com.guoshi.httpcanary" to 1,
"org.lsposed.lspatch" to 1,
"app.revanced.manager.flutter" to 100,
"com.reddit.frontpage" to 20
)
val appList = mutableStateListOf<AppInfo>()
val supportedAppList = mutableStateListOf<AppInfo>()
suspend fun loadApps(context: Context) {
val packageManager = context.packageManager
testList.keys.map {
try {
val applicationInfo = packageManager.getApplicationInfo(it, 0)
AppInfo(
it,
applicationInfo.loadLabel(packageManager).toString(),
applicationInfo.loadIcon(packageManager),
)
} catch (e: PackageManager.NameNotFoundException) {
AppInfo(
it,
"Not installed"
)
}
}.let { list ->
list.sortedWith(
compareByDescending<AppInfo> {
testList[it.packageName]
}.thenBy { it.label }.thenBy { it.packageName }
)
}.also {
withContext(Dispatchers.Main) { supportedAppList.addAll(it) }
}
val localAppList = mutableListOf<AppInfo>()
packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map {
AppInfo(
it.packageName,
it.loadLabel(packageManager).toString(),
it.loadIcon(packageManager)
)
}.also { localAppList.addAll(it) }
testList.keys.mapNotNull { packageName ->
if (!localAppList.any { packageName == it.packageName }) {
AppInfo(
packageName,
"Not installed"
)
} else {
null
}
}.also { localAppList.addAll(it) }
localAppList.sortWith(
compareByDescending<AppInfo> {
testList[it.packageName]
}.thenBy { it.label }.thenBy { it.packageName }
).also {
withContext(Dispatchers.Main) { appList.addAll(localAppList) }
}
}
@Parcelize
data class AppInfo(
val packageName: String,
val label: String,
val icon: @RawValue Drawable? = null
) : Parcelable
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
session.writeApk(apk)
session.commit(context.installIntentSender)
session.close()
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
}
}
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, byteArraySize)
fsync(outputStream)
}
}
}
private val intentFlags
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_MUTABLE
else
0
private val sessionParams
get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
private val Context.installIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, InstallService::class.java),
intentFlags
).intentSender
private val Context.uninstallIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, UninstallService::class.java),
intentFlags
).intentSender

View File

@ -1,6 +1,26 @@
<resources>
<string name="app_name">ReVanced Manager</string>
<string name="patcher">Patcher</string>
<string name="patches">Patches</string>
<string name="integrations">Integrations</string>
<string name="cli">CLI</string>
<string name="manager">Manager</string>
<string name="dashboard">Dashboard</string>
<string name="settings">Settings</string>
<string name="about">About</string>
<string name="search_apps">Search apps…</string>
<string name="loading_body">Loading…</string>
<string name="downloading_patches">Downloading patch bundle…</string>
<string name="downloading_integrations">Downloading Integrations…</string>
<string name="contributors">Contributors</string>
<string name="appearance">Appearance</string>
<string name="dynamic_color">Dynamic color</string>
<string name="theme">Theme</string>
<string name="storage">Storage</string>
<string name="tab_apps">Apps</string>
<string name="tab_sources">Sources</string>
<string name="no_sources_set">No sources set</string>