Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />

<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:ignore="RequestInstallPackagesPolicy" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

<application
android:name=".app.GithubStoreApp"
Expand Down Expand Up @@ -64,8 +67,7 @@
<data android:mimeType="text/html" />
</intent-filter>

<!-- GitHub repository links: https://github.com/{owner}/{repo} -->
<intent-filter>
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package zed.rainxch.githubstore.app

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import zed.rainxch.core.data.services.PackageEventReceiver
import zed.rainxch.core.data.services.UpdateScheduler
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.system.PackageMonitor
import zed.rainxch.githubstore.app.di.initKoin
Expand All @@ -20,7 +23,21 @@ class GithubStoreApp : Application() {
androidContext(this@GithubStoreApp)
}

createNotificationChannels()
registerPackageEventReceiver()
scheduleBackgroundUpdateChecks()
}

private fun createNotificationChannels() {
val channel = NotificationChannel(
UPDATES_CHANNEL_ID,
"App Updates",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications when app updates are available"
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}

private fun registerPackageEventReceiver() {
Expand All @@ -38,4 +55,12 @@ class GithubStoreApp : Application() {

packageEventReceiver = receiver
}

private fun scheduleBackgroundUpdateChecks() {
UpdateScheduler.schedule(context = this)
}

companion object {
const val UPDATES_CHANNEL_ID = "app_updates"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand All @@ -26,6 +28,7 @@ import io.github.fletchmckee.liquid.rememberLiquidState
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
import zed.rainxch.apps.presentation.AppsRoot
import zed.rainxch.apps.presentation.AppsViewModel
import zed.rainxch.auth.presentation.AuthenticationRoot
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
Expand All @@ -45,6 +48,9 @@ fun AppNavigation(
var bottomNavigationHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current

val appsViewModel = koinViewModel<AppsViewModel>()
val appsState by appsViewModel.state.collectAsStateWithLifecycle()

CompositionLocalProvider(
LocalBottomNavigationLiquid provides liquidState,
LocalBottomNavigationHeight provides bottomNavigationHeight
Expand Down Expand Up @@ -222,6 +228,9 @@ fun AppNavigation(
},
onNavigateToFavouriteRepos = {
navController.navigate(GithubStoreGraph.FavouritesScreen)
},
onNavigateToDevProfile = { username ->
navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username))
}
)
}
Expand All @@ -237,7 +246,9 @@ fun AppNavigation(
repositoryId = repoId
)
)
}
},
viewModel = appsViewModel,
state = appsState
)
}
}
Expand All @@ -248,8 +259,16 @@ fun AppNavigation(
BottomNavigation(
currentScreen = currentScreen,
onNavigate = {
navController.navigate(it)
navController.navigate(it) {
popUpTo(GithubStoreGraph.HomeScreen) {
saveState = true
}

launchSingleTop = true
restoreState = true
}
},
isUpdateAvailable = appsState.apps.any { it.installedApp.isUpdateAvailable },
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ import zed.rainxch.core.domain.getPlatform
import zed.rainxch.core.domain.model.Platform
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
import zed.rainxch.core.presentation.theme.GithubStoreTheme
import zed.rainxch.details.presentation.utils.isLiquidFrostAvailable
import zed.rainxch.core.presentation.utils.isLiquidFrostAvailable

@Composable
fun BottomNavigation(
currentScreen: GithubStoreGraph,
onNavigate: (GithubStoreGraph) -> Unit,
isUpdateAvailable: Boolean,
modifier: Modifier = Modifier
) {
val liquidState = LocalBottomNavigationLiquid.current
Expand Down Expand Up @@ -138,9 +139,7 @@ fun BottomNavigation(
if (isLiquidFrostAvailable()) {
Modifier.liquid(liquidState) {
this.shape = CircleShape
if (isLiquidFrostAvailable()) {
this.frost = if (isDarkTheme) 12.dp else 10.dp
}
this.frost = if (isDarkTheme) 12.dp else 10.dp
this.curve = if (isDarkTheme) .35f else .45f
this.refraction = if (isDarkTheme) .08f else .12f
this.dispersion = if (isDarkTheme) .18f else .25f
Expand Down Expand Up @@ -237,6 +236,7 @@ fun BottomNavigation(
visibleItems.forEachIndexed { index, item ->
LiquidGlassTabItem(
item = item,
hasBadge = item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable,
isSelected = item.screen == currentScreen,
onSelect = { onNavigate(item.screen) },
onPositioned = { x, width ->
Expand Down Expand Up @@ -264,6 +264,7 @@ private fun LiquidGlassTabItem(
item: BottomNavigationItem,
isSelected: Boolean,
onSelect: () -> Unit,
hasBadge: Boolean = false,
onPositioned: suspend (x: Float, width: Float) -> Unit
) {
val scope = rememberCoroutineScope()
Expand Down Expand Up @@ -331,7 +332,7 @@ private fun LiquidGlassTabItem(
label = "hPadding"
)

Column(
Box(
modifier = Modifier
.clip(CircleShape)
.clickable(
Expand All @@ -347,46 +348,59 @@ private fun LiquidGlassTabItem(
scaleX = pressScale
scaleY = pressScale
}
.padding(horizontal = horizontalPadding, vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(1.dp)
.padding(horizontal = horizontalPadding, vertical = 6.dp)
) {
Icon(
imageVector = if (isSelected) item.iconFilled else item.iconOutlined,
contentDescription = stringResource(item.titleRes),
modifier = Modifier
.size(22.dp)
.graphicsLayer {
scaleX = iconScale
scaleY = iconScale
translationY = with(density) { iconOffsetY.toPx() }
},
tint = iconTint
)

Box(
modifier = Modifier
.height(if (isSelected) 16.dp else 0.dp)
.graphicsLayer {
alpha = labelAlpha
scaleX = labelScale
scaleY = labelScale
},
contentAlignment = Alignment.Center
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
Text(
text = stringResource(item.titleRes),
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
lineHeight = 12.sp
),
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
},
maxLines = 1
Icon(
imageVector = if (isSelected) item.iconFilled else item.iconOutlined,
contentDescription = stringResource(item.titleRes),
modifier = Modifier
.size(22.dp)
.graphicsLayer {
scaleX = iconScale
scaleY = iconScale
translationY = with(density) { iconOffsetY.toPx() }
},
tint = iconTint
)

Box(
modifier = Modifier
.height(if (isSelected) 16.dp else 0.dp)
.graphicsLayer {
alpha = labelAlpha
scaleX = labelScale
scaleY = labelScale
},
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(item.titleRes),
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
lineHeight = 12.sp
),
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
},
maxLines = 1
)
}
}

if (hasBadge) {
Box(
Modifier
.size(12.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.error)
.align(Alignment.TopEnd)
)
}
}
Expand All @@ -403,7 +417,8 @@ fun BottomNavigationPreview() {
currentScreen = GithubStoreGraph.HomeScreen,
onNavigate = {

}
},
isUpdateAvailable = true
)
}
}
Expand Down
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ kotlin {
androidMain {
dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.androidx.work.runtime)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ class AndroidInstaller(
}
}

override fun uninstall(packageName: String) {
Logger.d { "Requesting uninstall for: $packageName" }
val intent = Intent(Intent.ACTION_DELETE).apply {
data = "package:$packageName".toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (e: Exception) {
Logger.w { "Failed to start uninstall for $packageName: ${e.message}" }
}

}

override fun isObtainiumInstalled(): Boolean {
return try {
context.packageManager.getPackageInfo("dev.imranr.obtainium.fdroid", 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ import kotlinx.coroutines.launch
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.system.PackageMonitor

/**
* Listens to system package install/uninstall/replace broadcasts.
* When a tracked package is installed or updated, it resolves the pending
* install flag and updates version info from the system PackageManager.
* When a tracked package is removed, it deletes the record from the database.
*/
class PackageEventReceiver(
private val installedAppsRepository: InstalledAppsRepository,
private val packageMonitor: PackageMonitor
Expand Down Expand Up @@ -49,17 +43,37 @@ class PackageEventReceiver(
if (app.isPendingInstall) {
val systemInfo = packageMonitor.getInstalledPackageInfo(packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(
app.copy(
isPendingInstall = false,
isUpdateAvailable = false,
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
val expectedVersionCode = app.latestVersionCode ?: 0L
val wasActuallyUpdated = expectedVersionCode > 0L &&
systemInfo.versionCode >= expectedVersionCode

if (wasActuallyUpdated) {
installedAppsRepository.updateAppVersion(
packageName = packageName,
newTag = app.latestVersion ?: systemInfo.versionName,
newAssetName = app.latestAssetName ?: "",
newAssetUrl = app.latestAssetUrl ?: "",
newVersionName = systemInfo.versionName,
newVersionCode = systemInfo.versionCode
)
)
Logger.i { "Resolved pending install via broadcast: $packageName (v${systemInfo.versionName})" }
installedAppsRepository.updatePendingStatus(packageName, false)
Logger.i { "Update confirmed via broadcast: $packageName (v${systemInfo.versionName})" }
} else {
installedAppsRepository.updateApp(
app.copy(
isPendingInstall = false,
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
isUpdateAvailable = (app.latestVersionCode
?: 0L) > systemInfo.versionCode
)
)
Logger.i {
"Package replaced but not updated to target: $packageName " +
"(system: v${systemInfo.versionName}/${systemInfo.versionCode}, " +
"target: v${app.latestVersionName}/${app.latestVersionCode})"
}
}
} else {
installedAppsRepository.updatePendingStatus(packageName, false)
Logger.i { "Resolved pending install via broadcast (no system info): $packageName" }
Expand All @@ -83,7 +97,6 @@ class PackageEventReceiver(

private suspend fun onPackageRemoved(packageName: String) {
try {
val app = installedAppsRepository.getAppByPackage(packageName) ?: return
installedAppsRepository.deleteInstalledApp(packageName)
Logger.i { "Removed uninstalled app via broadcast: $packageName" }
} catch (e: Exception) {
Expand Down
Loading