Skip to content

Commit dee34a5

Browse files
authored
Merge pull request #317 from OpenHub-Store/shizuku-integration
2 parents ba8e27f + abaa77d commit dee34a5

44 files changed

Lines changed: 2240 additions & 220 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
GitHub Store is a cross-platform app store for GitHub releases, built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. Targets **Android** (min API 26) and **Desktop** (Windows, macOS, Linux via JVM).
66

7-
Package: `zed.rainxch.githubstore`
7+
Package: `zed.rainxch.githubstore` | Version: 1.6.2 (code 13) | Target SDK: 36
88

99
## Build & Run Commands
1010

@@ -45,8 +45,8 @@ feature/
4545
dev-profile/ # Developer/user profile display
4646
favourites/ # Saved favorite repositories (presentation-only)
4747
home/ # Main discovery screen (trending, hot, popular)
48+
profile/ # User profile, settings, appearance, proxy, Shizuku installer
4849
search/ # Repository search with filters
49-
settings/ # App settings (theme, account, appearance)
5050
starred/ # Starred repositories (presentation-only)
5151
build-logic/convention/ # Custom Gradle convention plugins
5252
```
@@ -86,9 +86,9 @@ class XViewModel : ViewModel() {
8686
Type-safe navigation using `@Serializable` sealed interface `GithubStoreGraph`:
8787

8888
```
89-
HomeScreen, SearchScreen, AuthenticationScreen, SettingsScreen,
90-
FavouritesScreen, StarredReposScreen, AppsScreen
91-
DetailsScreen(repositoryId, owner, repo)
89+
HomeScreen, SearchScreen, AuthenticationScreen, ProfileScreen,
90+
FavouritesScreen, StarredReposScreen, AppsScreen, SponsorScreen
91+
DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)
9292
DeveloperProfileScreen(username)
9393
```
9494

@@ -102,26 +102,29 @@ Routes defined in `composeApp/.../app/navigation/GithubStoreGraph.kt`, wired in
102102

103103
| Module | Purpose | Key Contents |
104104
|--------|---------|--------------|
105-
| `core/domain` | Shared contracts | Repository interfaces (`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `ThemesRepository`), models (`GithubRepoSummary`, `GithubRelease`, `InstalledApp`, `ProxyConfig`), system interfaces (`Downloader`, `Installer`, `PackageMonitor`) |
106-
| `core/data` | Shared implementations | `HttpClientFactory` (Ktor + interceptors), `AppDatabase` (Room), `ProxyManager`, `TokenStore`, `LocalizationManager`, platform-specific clients (OkHttp for Android, CIO for Desktop) |
107-
| `core/presentation` | Shared UI | `GithubStoreTheme` (Material 3), reusable components (`RepositoryCard`, `GithubStoreButton`), formatting utils |
105+
| `core/domain` | Shared contracts | Repository interfaces (`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`), models (`GithubRepoSummary`, `GithubRelease`, `InstalledApp`, `ProxyConfig`, `InstallerType`, `ShizukuAvailability`), system interfaces (`Installer`, `InstallerInfoExtractor`, `InstallerStatusProvider`, `PackageMonitor`) |
106+
| `core/data` | Shared implementations | `HttpClientFactory` (Ktor + interceptors), `AppDatabase` (Room), `ProxyManager`, `TokenStore`, `LocalizationManager`, platform-specific clients (OkHttp for Android, CIO for Desktop), Shizuku integration (Android: `ShizukuServiceManager`, `ShizukuInstallerWrapper`, `ShizukuInstallerServiceImpl`, `AndroidInstallerStatusProvider`; Desktop: `DesktopInstallerStatusProvider`) |
107+
| `core/presentation` | Shared UI | `GithubStoreTheme` (Material 3), reusable components (`RepositoryCard`, `GithubStoreButton`), formatting utils, localized strings (11 languages) |
108108

109109
## Tech Stack
110110

111111
| Area | Library | Version |
112112
|------|---------|---------|
113-
| Language | Kotlin | 2.3.0 |
114-
| UI | Compose Multiplatform | 1.9.0-beta01 |
115-
| HTTP | Ktor | 3.2.3 |
116-
| Database | Room | 2.7.2 |
117-
| DI | Koin | 4.1.0 |
118-
| Serialization | Kotlinx Serialization | 1.9.0 |
119-
| Preferences | DataStore | 1.1.7 |
120-
| Image Loading | Landscapist (Coil3) | 2.9.1 |
113+
| Language | Kotlin | 2.3.10 |
114+
| UI | Compose Multiplatform | 1.10.1 |
115+
| HTTP | Ktor | 3.4.0 |
116+
| Database | Room | 2.8.4 |
117+
| DI | Koin | 4.1.1 |
118+
| Serialization | Kotlinx Serialization | 1.10.0 |
119+
| Preferences | DataStore | 1.2.0 |
120+
| Image Loading | Landscapist (Coil3) | 2.9.5 |
121121
| Logging | Kermit | 2.0.8 |
122-
| Permissions | MOKO Permissions | 0.19.1 |
123-
| Navigation | Navigation Compose | 2.9.1 |
124-
| Markdown | Multiplatform Markdown Renderer | 0.39.1 |
122+
| Permissions | MOKO Permissions | 0.20.1 |
123+
| Navigation | Navigation Compose | 2.9.2 |
124+
| Markdown | Multiplatform Markdown Renderer | 0.39.2 |
125+
| Shizuku | Shizuku API | 13.1.5 |
126+
| Background Work | WorkManager | 2.11.1 |
127+
| Date/Time | Kotlinx Datetime | 0.7.1 |
125128

126129
All versions managed in `gradle/libs.versions.toml` (Version Catalog).
127130

@@ -151,7 +154,8 @@ Custom Gradle plugins in `build-logic/convention/` standardize module setup:
151154

152155
## Key Configuration
153156

154-
- **GitHub OAuth:** Set `GITHUB_CLIENT_ID` in `local.properties`. Callback URL: `githubstore://callback`
157+
- **GitHub OAuth:** Set `GITHUB_CLIENT_ID` in `local.properties`. Callback URL: `githubstore://callback`. Deep link: `githubstore://repo`
158+
- **Shizuku (Android):** Optional silent install via `ShizukuProvider` (registered in AndroidManifest). Requires Shizuku app running with ADB or root. AIDL service passes APK via `ParcelFileDescriptor` to `pm install -S`. Falls back to standard installer on failure.
155159
- **Gradle properties:** Config cache enabled, build cache enabled, 4GB Gradle heap, 3GB Kotlin daemon heap
156160
- **Code style:** Official Kotlin style (`kotlin.code.style=official`)
157161

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
1515
tools:ignore="RequestInstallPackagesPolicy" />
1616
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
17+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
19+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1720

1821
<application
1922
android:name=".app.GithubStoreApp"
@@ -100,6 +103,36 @@
100103
android:name="android.support.FILE_PROVIDER_PATHS"
101104
android:resource="@xml/filepaths" />
102105
</provider>
106+
107+
<!-- Reschedule update checks after device reboot -->
108+
<receiver
109+
android:name="zed.rainxch.core.data.services.BootReceiver"
110+
android:exported="false">
111+
<intent-filter>
112+
<action android:name="android.intent.action.BOOT_COMPLETED" />
113+
</intent-filter>
114+
</receiver>
115+
116+
<!-- Static receiver for package install/remove events (works even when process is dead) -->
117+
<receiver
118+
android:name="zed.rainxch.core.data.services.PackageEventReceiver"
119+
android:exported="false">
120+
<intent-filter>
121+
<action android:name="android.intent.action.PACKAGE_ADDED" />
122+
<action android:name="android.intent.action.PACKAGE_REPLACED" />
123+
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
124+
<data android:scheme="package" />
125+
</intent-filter>
126+
</receiver>
127+
128+
<!-- Shizuku provider for optional silent install support -->
129+
<provider
130+
android:name="rikka.shizuku.ShizukuProvider"
131+
android:authorities="${applicationId}.shizuku"
132+
android:multiprocess="false"
133+
android:enabled="true"
134+
android:exported="true"
135+
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
103136
</application>
104137

105138
</manifest>

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,28 @@ class GithubStoreApp : Application() {
2828
}
2929

3030
private fun createNotificationChannels() {
31-
val channel =
31+
val notificationManager = getSystemService(NotificationManager::class.java)
32+
33+
val updatesChannel =
3234
NotificationChannel(
3335
UPDATES_CHANNEL_ID,
3436
"App Updates",
3537
NotificationManager.IMPORTANCE_HIGH,
3638
).apply {
3739
description = "Notifications when app updates are available"
3840
}
39-
val notificationManager = getSystemService(NotificationManager::class.java)
40-
notificationManager.createNotificationChannel(channel)
41+
notificationManager.createNotificationChannel(updatesChannel)
42+
43+
val serviceChannel =
44+
NotificationChannel(
45+
UPDATE_SERVICE_CHANNEL_ID,
46+
"Update Service",
47+
NotificationManager.IMPORTANCE_LOW,
48+
).apply {
49+
description = "Background update check and auto-update progress"
50+
setShowBadge(false)
51+
}
52+
notificationManager.createNotificationChannel(serviceChannel)
4153
}
4254

4355
private fun registerPackageEventReceiver() {
@@ -63,5 +75,6 @@ class GithubStoreApp : Application() {
6375

6476
companion object {
6577
const val UPDATES_CHANNEL_ID = "app_updates"
78+
const val UPDATE_SERVICE_CHANNEL_ID = "update_service"
6679
}
6780
}

core/data/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ plugins {
44
alias(libs.plugins.convention.buildkonfig)
55
}
66

7+
android {
8+
buildFeatures {
9+
aidl = true
10+
}
11+
}
12+
713
kotlin {
814
sourceSets {
915
commonMain {
@@ -28,6 +34,9 @@ kotlin {
2834
dependencies {
2935
implementation(libs.ktor.client.okhttp)
3036
implementation(libs.androidx.work.runtime)
37+
implementation(libs.shizuku.api)
38+
implementation(libs.shizuku.provider)
39+
compileOnly(libs.hidden.api.stub)
3140
}
3241
}
3342

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package zed.rainxch.core.data.services.shizuku;
2+
3+
interface IShizukuInstallerService {
4+
int installPackage(in ParcelFileDescriptor pfd, long fileSize);
5+
int uninstallPackage(String packageName);
6+
void destroy();
7+
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,90 +2,124 @@ package zed.rainxch.core.data.di
22

33
import androidx.datastore.core.DataStore
44
import androidx.datastore.preferences.core.Preferences
5+
import kotlinx.coroutines.CoroutineScope
56
import org.koin.android.ext.koin.androidContext
67
import org.koin.dsl.module
78
import zed.rainxch.core.data.local.data_store.createDataStore
89
import zed.rainxch.core.data.local.db.AppDatabase
910
import zed.rainxch.core.data.local.db.initDatabase
11+
import zed.rainxch.core.data.services.AndroidInstallerInfoExtractor
1012
import zed.rainxch.core.data.services.AndroidDownloader
1113
import zed.rainxch.core.data.services.AndroidFileLocationsProvider
1214
import zed.rainxch.core.data.services.AndroidInstaller
13-
import zed.rainxch.core.data.services.AndroidInstallerInfoExtractor
1415
import zed.rainxch.core.data.services.AndroidLocalizationManager
1516
import zed.rainxch.core.data.services.AndroidPackageMonitor
1617
import zed.rainxch.core.data.services.FileLocationsProvider
1718
import zed.rainxch.core.data.services.LocalizationManager
19+
import zed.rainxch.core.data.services.shizuku.AndroidInstallerStatusProvider
20+
import zed.rainxch.core.data.services.shizuku.ShizukuInstallerWrapper
21+
import zed.rainxch.core.data.services.shizuku.ShizukuServiceManager
1822
import zed.rainxch.core.data.utils.AndroidAppLauncher
1923
import zed.rainxch.core.data.utils.AndroidBrowserHelper
2024
import zed.rainxch.core.data.utils.AndroidClipboardHelper
2125
import zed.rainxch.core.data.utils.AndroidShareManager
2226
import zed.rainxch.core.domain.network.Downloader
2327
import zed.rainxch.core.domain.system.Installer
28+
import zed.rainxch.core.domain.system.InstallerStatusProvider
2429
import zed.rainxch.core.domain.system.PackageMonitor
2530
import zed.rainxch.core.domain.utils.AppLauncher
2631
import zed.rainxch.core.domain.utils.BrowserHelper
2732
import zed.rainxch.core.domain.utils.ClipboardHelper
2833
import zed.rainxch.core.domain.utils.ShareManager
2934

30-
actual val corePlatformModule =
31-
module {
32-
// Core
35+
actual val corePlatformModule = module {
36+
// Core
3337

34-
single<Downloader> {
35-
AndroidDownloader(
36-
files = get(),
37-
)
38-
}
38+
single<Downloader> {
39+
AndroidDownloader(
40+
files = get(),
41+
)
42+
}
3943

40-
single<Installer> {
41-
AndroidInstaller(
42-
context = get(),
43-
installerInfoExtractor = AndroidInstallerInfoExtractor(androidContext()),
44-
)
45-
}
44+
// AndroidInstaller — registered by class so the wrapper can inject it
45+
single {
46+
AndroidInstaller(
47+
context = get(),
48+
installerInfoExtractor = AndroidInstallerInfoExtractor(androidContext())
49+
)
50+
}
4651

47-
single<FileLocationsProvider> {
48-
AndroidFileLocationsProvider(context = get())
49-
}
52+
// ShizukuServiceManager — manages Shizuku lifecycle, permissions, service binding
53+
single {
54+
ShizukuServiceManager(
55+
context = androidContext()
56+
).also { it.initialize() }
57+
}
5058

51-
single<PackageMonitor> {
52-
AndroidPackageMonitor(androidContext())
59+
// Installer — the ShizukuInstallerWrapper is the public Installer singleton.
60+
// It delegates to AndroidInstaller by default, intercepting with Shizuku when enabled.
61+
single<Installer> {
62+
ShizukuInstallerWrapper(
63+
androidInstaller = get<AndroidInstaller>(),
64+
shizukuServiceManager = get(),
65+
themesRepository = get()
66+
).also { wrapper ->
67+
wrapper.observeInstallerPreference(get<CoroutineScope>())
5368
}
69+
}
5470

55-
single<LocalizationManager> {
56-
AndroidLocalizationManager()
57-
}
71+
// InstallerStatusProvider — exposes Shizuku availability to the UI layer
72+
single<InstallerStatusProvider> {
73+
AndroidInstallerStatusProvider(
74+
shizukuServiceManager = get(),
75+
scope = get()
76+
)
77+
}
5878

59-
// Locals
79+
single<FileLocationsProvider> {
80+
AndroidFileLocationsProvider(context = get())
81+
}
6082

61-
single<AppDatabase> {
62-
initDatabase(androidContext())
63-
}
83+
single<PackageMonitor> {
84+
AndroidPackageMonitor(androidContext())
85+
}
6486

65-
single<DataStore<Preferences>> {
66-
createDataStore(androidContext())
67-
}
87+
single<LocalizationManager> {
88+
AndroidLocalizationManager()
89+
}
6890

69-
// Utils
91+
// Locals
7092

71-
single<BrowserHelper> {
72-
AndroidBrowserHelper(androidContext())
73-
}
93+
single<AppDatabase> {
94+
initDatabase(androidContext())
95+
}
7496

75-
single<ClipboardHelper> {
76-
AndroidClipboardHelper(androidContext())
77-
}
97+
single<DataStore<Preferences>> {
98+
createDataStore(androidContext())
99+
}
78100

79-
single<AppLauncher> {
80-
AndroidAppLauncher(
81-
context = androidContext(),
82-
logger = get(),
83-
)
84-
}
85101

86-
single<ShareManager> {
87-
AndroidShareManager(
88-
context = androidContext(),
89-
)
90-
}
102+
// Utils
103+
104+
single<BrowserHelper> {
105+
AndroidBrowserHelper(androidContext())
91106
}
107+
108+
single<ClipboardHelper> {
109+
AndroidClipboardHelper(androidContext())
110+
}
111+
112+
single<AppLauncher> {
113+
AndroidAppLauncher(
114+
context = androidContext(),
115+
logger = get()
116+
)
117+
}
118+
119+
single<ShareManager> {
120+
AndroidShareManager(
121+
context = androidContext()
122+
)
123+
}
124+
125+
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import java.net.PasswordAuthentication
2121
import java.net.Proxy
2222
import java.util.UUID
2323
import java.util.concurrent.ConcurrentHashMap
24+
import java.util.concurrent.TimeUnit
2425

2526
class AndroidDownloader(
2627
private val files: FileLocationsProvider,
@@ -34,6 +35,9 @@ class AndroidDownloader(
3435

3536
return OkHttpClient
3637
.Builder()
38+
.connectTimeout(30, TimeUnit.SECONDS)
39+
.readTimeout(60, TimeUnit.SECONDS)
40+
.writeTimeout(60, TimeUnit.SECONDS)
3741
.apply {
3842
when (val config = proxyManager.currentProxyConfig.value) {
3943
is ProxyConfig.None -> {

0 commit comments

Comments
 (0)