Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1f01c55
E1: external-import domain interfaces and models
rainxchzed Apr 25, 2026
3056e22
data: implement persistent storage paths and migration for desktop
rainxchzed Apr 25, 2026
cacfd2b
E1: room entities, daos, and migration 14_15 for external links
rainxchzed Apr 25, 2026
ad2695b
E1: android external app scanner with manifest and installer classifi…
rainxchzed Apr 25, 2026
13c019d
E1: external-import repository skeleton with initial-scan persistence
rainxchzed Apr 25, 2026
3d30236
E1: external-import contract types, banner state, route, tweaks flags
rainxchzed Apr 25, 2026
6c7cf8d
E1: backend match api, mock, selector, and repository fill
rainxchzed Apr 25, 2026
e59353b
E1: external import view model and apps banner observer
rainxchzed Apr 25, 2026
9d93d1e
E1: external import wizard composables and proposal banner
rainxchzed Apr 25, 2026
15e0d8a
E1: wire external import route, banner, and initial scan trigger
rainxchzed Apr 25, 2026
cd12123
E1: fix snapshot torn writes, scan-job race, off-by-one, manual mapping
rainxchzed Apr 25, 2026
cb38a5f
E1: visual hierarchy, contrast, accessibility, and reduced-motion gate
rainxchzed Apr 25, 2026
f2a776e
E1: materialize installed_apps rows on auto-link and manual link
rainxchzed Apr 26, 2026
c3da019
E1 week 3 data: signing-cert seed, delta scan, search, and telemetry …
rainxchzed Apr 26, 2026
195578b
E1 week 3 wizard: search override, reduced-motion preference
rainxchzed Apr 26, 2026
6b55e56
E1 week 3 details: unlink external app affordance with confirmation
rainxchzed Apr 26, 2026
ff99c1e
E1 week 3: wire telemetry from view models for permission, link, skip…
rainxchzed Apr 26, 2026
c0a1783
E1: cache external-match flag, drop banner threshold to 1, prune stal…
rainxchzed Apr 26, 2026
4650649
E1: sdk-int telemetry, package-visibility request, confetti, skip-rem…
rainxchzed Apr 26, 2026
c7e4a2f
E1: extract wizard, banner, and unlink strings to compose-resources c…
rainxchzed Apr 26, 2026
c44b765
E1: filter all FLAG_SYSTEM apps and OEM package prefixes from scan
rainxchzed Apr 26, 2026
d9140ed
E1: prune stale pending rows on scan, expose decision snapshot for undo
rainxchzed Apr 26, 2026
c79eaee
E1: replace card stack with scrollable list, add undo snackbar, paste…
rainxchzed Apr 26, 2026
da568d7
E1: auto-import summary screen and add-manually entry from wizard
rainxchzed Apr 26, 2026
9829d36
E1: reconcile pending state on cold start, persist banner dismiss unt…
rainxchzed Apr 26, 2026
be9c574
E1: only surface candidates with positive evidence (trusted installer…
rainxchzed Apr 26, 2026
2a5acb9
E1: instrument scan, banner, package events, and worker with E1Debug …
rainxchzed Apr 26, 2026
82315a9
apps: switch + FAB to extended FAB with 'Add by link' label
rainxchzed Apr 27, 2026
0980417
Merge branch 'main' into feature/e1-external-imports
rainxchzed Apr 27, 2026
c858b7c
E1: address coderabbit review on PR #461
rainxchzed Apr 27, 2026
ad313fd
E1: runDeltaScan preserves committed decisions on transient evidence …
rainxchzed Apr 27, 2026
3b2bb37
E1: stop optimistic permission grant; drop misleading settings deep-link
rainxchzed Apr 27, 2026
f04c0da
E1: synchronous local watermark to fully close banner re-flash race
rainxchzed Apr 27, 2026
ac5a1d3
E1: drop E1Debug instrumentation
rainxchzed Apr 27, 2026
413136e
apps: 'Scan for GitHub apps' overflow entry triggers manual rescan
rainxchzed Apr 27, 2026
b5d5731
apps: union the apps-tab badge with pending external-import count
rainxchzed Apr 27, 2026
f02ec95
E1: backend handoff doc
rainxchzed Apr 27, 2026
dca895b
E1: permission rationale lists optional backend payload fields
rainxchzed Apr 27, 2026
8def6cc
E1: track installed-app pre-state for bulk undo, short-circuit failed…
rainxchzed Apr 27, 2026
a9e7000
core/data: backstop delta scan fires for first-time installs of untra…
rainxchzed Apr 27, 2026
843ebf2
E1 handoff: clarify cache key includes fingerprint, document actual 4…
rainxchzed Apr 27, 2026
f0004b7
core/data: retry unlink on transient failure, use typed ExternalLinkS…
rainxchzed Apr 28, 2026
1d7358e
E1: capture pre-link snapshots for true bulk-undo, track per-package …
rainxchzed Apr 28, 2026
c910464
E1 handoff: 503 fallback covers manifest hints AND signing-cert seed
rainxchzed Apr 28, 2026
8dbe575
core/data: post-retry delta scan recovers reinstalls during the unlin…
rainxchzed Apr 28, 2026
d247b53
E1: fail-fast on undo and snapshot-capture failures (preserves undo m…
rainxchzed Apr 28, 2026
e6dceca
E1 handoff: manifestHint nullability semantics, confidence clamping c…
rainxchzed Apr 28, 2026
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
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"WebFetch(domain:youtrack.jetbrains.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:blog.jetbrains.com)",
"Bash(rtk grep *)"
"Bash(rtk grep *)",
"Bash(git checkout *)",
"Bash(rtk proxy *)"
Comment on lines +14 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict broad shell permissions for checkout/proxy commands.

Line 14 and Line 15 add wildcard permissions that materially expand destructive and network-capable operations (git checkout *, rtk proxy *). This weakens the default safety boundary for automated command execution.

Consider removing these from persistent allowlist, or narrowing to explicit safe patterns (specific args/targets) and requiring manual approval for all other invocations.

Suggested hardening diff
-      "Bash(git checkout *)",
-      "Bash(rtk proxy *)"
+      // Keep destructive/network-capable commands gated by manual approval.
+      // Add narrowly-scoped patterns only when absolutely required.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/settings.local.json around lines 14 - 15, Remove or restrict the
broad shell permissions currently added as "Bash(git checkout *)" and "Bash(rtk
proxy *)" in the settings file: either delete these entries from the persistent
allowlist or replace them with explicit, narrow patterns (e.g., exact branch
names or specific safe args) and require manual approval for any other variants;
update any validation logic that reads these entries to reject wildcard-enabled
checkout/proxy patterns and document the approved explicit patterns so callers
of the settings know which exact "Bash(...)" values (e.g., the "git checkout"
and "rtk proxy" entries) are permitted.

]
}
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ google-services.json

composeApp/release/baselineProfiles/

composeApp/kotzilla.json
composeApp/kotzilla.json

roadmap/
15 changes: 15 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<!--
Fallback visibility for installer-source classification when
QUERY_ALL_PACKAGES is denied on Android 11+. We need to recognise
these specific packages as install sources even without blanket
package visibility.
-->
<queries>
<package android:name="dev.imranr.obtainium.app" />
<package android:name="dev.imranr.obtainium" />
<package android:name="dev.imranr.obtainium.fdroid" />
<package android:name="org.fdroid.fdroid" />
<package android:name="com.android.vending" />
<package android:name="com.aurora.store" />
</queries>
Comment on lines +27 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what identifier the codebase already uses for Obtainium elsewhere
# (e.g. installer-source allow-lists, telemetry constants, classifier code),
# to confirm the manifest <queries> stays in sync.
rg -nP --type=kt -C2 'dev\.imranr\.obtainium(\.[\w]+)?'
rg -nP -C2 'obtainium' --iglob '!**/*.lock' --iglob '!**/build/**'

Repository: OpenHub-Store/GitHub-Store

Length of output: 25817


Add the F‑Droid package identifier and sync across installer detection.

The manifest is missing dev.imranr.obtainium.fdroid, which AndroidInstaller.isObtainiumInstalled() already checks for as the primary variant. Additionally, InstallerSourceClassifier.OBTAINIUM_PACKAGES lacks the same .fdroid identifier. Both files must be synchronized to ensure consistent installer detection across the codebase.

Suggested update for manifest
     <queries>
         <package android:name="dev.imranr.obtainium.app" />
-        <package android:name="dev.imranr.obtainium" />
+        <package android:name="dev.imranr.obtainium.fdroid" />
         <package android:name="org.fdroid.fdroid" />
         <package android:name="com.android.vending" />
         <package android:name="com.aurora.store" />
     </queries>

Also update core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/InstallerSourceClassifier.kt to include "dev.imranr.obtainium.fdroid" in the OBTAINIUM_PACKAGES set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composeApp/src/androidMain/AndroidManifest.xml` around lines 27 - 33, Add the
missing F‑Droid package identifier "dev.imranr.obtainium.fdroid" to the
AndroidManifest <queries> block so the system can resolve that installer, and
also add the same string to the OBTAINIUM_PACKAGES set in
InstallerSourceClassifier (in core/data's AndroidMain Kotlin file) so
InstallerSourceClassifier.OBTAINIUM_PACKAGES and
AndroidInstaller.isObtainiumInstalled() remain consistent; update the manifest
queries list and the OBTAINIUM_PACKAGES set to include
"dev.imranr.obtainium.fdroid".


<application
android:name=".app.GithubStoreApp"
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import zed.rainxch.core.data.local.db.dao.ExternalLinkDao
import zed.rainxch.core.data.services.DownloadNotificationObserver
import zed.rainxch.core.data.services.PackageEventReceiver
import zed.rainxch.core.data.services.UpdateScheduler
import zed.rainxch.core.domain.model.InstallSource
import zed.rainxch.core.domain.model.InstalledApp
import zed.rainxch.core.domain.repository.ExternalImportRepository
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.repository.TweaksRepository
import zed.rainxch.core.domain.system.PackageMonitor
Expand All @@ -38,6 +40,28 @@ class GithubStoreApp : Application() {
startDownloadNotificationObserver()
scheduleBackgroundUpdateChecks()
registerSelfAsInstalledApp()
scheduleInitialExternalScan()
scheduleSigningSeedSync()
}

private fun scheduleInitialExternalScan() {
appScope.launch {
runCatching {
get<ExternalImportRepository>().scheduleInitialScanIfNeeded()
}.onFailure {
Logger.w(it) { "Initial external scan scheduling failed" }
}
}
}

private fun scheduleSigningSeedSync() {
appScope.launch {
runCatching {
get<ExternalImportRepository>().syncSigningFingerprintSeed()
}.onFailure {
Logger.w(it) { "Signing seed sync failed" }
}
}
}

private fun startDownloadNotificationObserver() {
Expand Down Expand Up @@ -85,6 +109,9 @@ class GithubStoreApp : Application() {
PackageEventReceiver(
installedAppsRepository = get<InstalledAppsRepository>(),
packageMonitor = get<PackageMonitor>(),
externalImportRepository = get<ExternalImportRepository>(),
externalLinkDao = get<ExternalLinkDao>(),
appScope = get<CoroutineScope>(),
)
val filter = PackageEventReceiver.createIntentFilter()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import zed.rainxch.apps.presentation.AppsViewModel
import zed.rainxch.apps.presentation.import.ExternalImportViewModel
import zed.rainxch.auth.presentation.AuthenticationViewModel
import zed.rainxch.details.presentation.DetailsViewModel
import zed.rainxch.devprofile.presentation.DeveloperProfileViewModel
Expand All @@ -18,6 +19,7 @@ import zed.rainxch.tweaks.presentation.TweaksViewModel
val viewModelsModule =
module {
viewModelOf(::AppsViewModel)
viewModelOf(::ExternalImportViewModel)
viewModelOf(::AuthenticationViewModel)
viewModel { params ->
// Indexed access because `ownerParam` and `repoParam` are both
Expand Down Expand Up @@ -48,6 +50,7 @@ val viewModelsModule =
attestationVerifier = get(),
downloadOrchestrator = get(),
telemetryRepository = get(),
externalImportRepository = get(),
)
}
viewModelOf(::DeveloperProfileViewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -28,6 +29,7 @@ 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.apps.presentation.import.ExternalImportRoot
import zed.rainxch.auth.presentation.AuthenticationRoot
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
Expand All @@ -43,6 +45,10 @@ import zed.rainxch.search.presentation.SearchRoot
import zed.rainxch.starred.presentation.StarredReposRoot
import zed.rainxch.tweaks.presentation.TweaksRoot

// Cross-screen "return result" key: set by the external-import wizard's
// "Add manually" path before navigateUp(), read once by the Apps screen.
private const val EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY = "external_import_open_link_sheet"

@Composable
fun AppNavigation(
navController: NavHostController,
Expand Down Expand Up @@ -296,7 +302,18 @@ fun AppNavigation(
TweaksRoot()
}

composable<GithubStoreGraph.AppsScreen> {
composable<GithubStoreGraph.AppsScreen> { backStackEntry ->
// Pick up the "open link sheet" flag set by ExternalImportRoot's
// "Add manually" path. We consume the flag once on entry so a
// later config change or back-stack rewind doesn't reopen the sheet.
LaunchedEffect(backStackEntry) {
val handle = backStackEntry.savedStateHandle
val openLinkSheet = handle.get<Boolean>(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY)
if (openLinkSheet == true) {
handle.remove<Boolean>(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY)
appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick)
}
}
AppsRoot(
onNavigateBack = {
navController.navigateUp()
Expand All @@ -309,10 +326,35 @@ fun AppNavigation(
),
)
},
onNavigateToExternalImport = {
navController.navigate(GithubStoreGraph.ExternalImportScreen)
},
viewModel = appsViewModel,
state = appsState,
)
}

composable<GithubStoreGraph.ExternalImportScreen> {
ExternalImportRoot(
onNavigateBack = {
navController.navigateUp()
},
onNavigateToDetails = { repoId ->
navController.navigate(
GithubStoreGraph.DetailsScreen(
repositoryId = repoId,
isComingFromUpdate = true,
),
)
},
onAddManually = {
navController.previousBackStackEntry
?.savedStateHandle
?.set(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY, true)
navController.navigateUp()
},
)
}
}

val currentScreen =
Expand All @@ -331,7 +373,13 @@ fun AppNavigation(
restoreState = true
}
},
isUpdateAvailable = appsState.apps.any { it.installedApp.isUpdateAvailable },
// Badge fires when either an update is waiting OR pending
// import candidates need review. The badge is a single dot
// — a union of the two conditions is honest "you have
// something to look at on this tab".
isUpdateAvailable =
appsState.apps.any { it.installedApp.isUpdateAvailable } ||
appsState.showImportProposalBanner,
isLiquidGlassEnabled = isLiquidGlassEnabled,
modifier =
Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ sealed interface GithubStoreGraph {

@Serializable
data object SponsorScreen : GithubStoreGraph

@Serializable
data object ExternalImportScreen : GithubStoreGraph
}
Loading