Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

<!-- Shizuku provider for optional silent install support -->
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
</application>

</manifest>
9 changes: 9 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ plugins {
alias(libs.plugins.convention.buildkonfig)
}

android {
buildFeatures {
aidl = true
}
}

kotlin {
sourceSets {
commonMain {
Expand All @@ -28,6 +34,9 @@ kotlin {
dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.androidx.work.runtime)
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
compileOnly(libs.hidden.api.stub)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package zed.rainxch.core.data.services.shizuku;

interface IShizukuInstallerService {
int installPackage(in ParcelFileDescriptor pfd, long fileSize);
int uninstallPackage(String packageName);
void destroy();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,124 @@ package zed.rainxch.core.data.di

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import zed.rainxch.core.data.local.data_store.createDataStore
import zed.rainxch.core.data.local.db.AppDatabase
import zed.rainxch.core.data.local.db.initDatabase
import zed.rainxch.core.data.services.AndroidInstallerInfoExtractor
import zed.rainxch.core.data.services.AndroidDownloader
import zed.rainxch.core.data.services.AndroidFileLocationsProvider
import zed.rainxch.core.data.services.AndroidInstaller
import zed.rainxch.core.data.services.AndroidInstallerInfoExtractor
import zed.rainxch.core.data.services.AndroidLocalizationManager
import zed.rainxch.core.data.services.AndroidPackageMonitor
import zed.rainxch.core.data.services.FileLocationsProvider
import zed.rainxch.core.data.services.LocalizationManager
import zed.rainxch.core.data.services.shizuku.AndroidInstallerStatusProvider
import zed.rainxch.core.data.services.shizuku.ShizukuInstallerWrapper
import zed.rainxch.core.data.services.shizuku.ShizukuServiceManager
import zed.rainxch.core.data.utils.AndroidAppLauncher
import zed.rainxch.core.data.utils.AndroidBrowserHelper
import zed.rainxch.core.data.utils.AndroidClipboardHelper
import zed.rainxch.core.data.utils.AndroidShareManager
import zed.rainxch.core.domain.network.Downloader
import zed.rainxch.core.domain.system.Installer
import zed.rainxch.core.domain.system.InstallerStatusProvider
import zed.rainxch.core.domain.system.PackageMonitor
import zed.rainxch.core.domain.utils.AppLauncher
import zed.rainxch.core.domain.utils.BrowserHelper
import zed.rainxch.core.domain.utils.ClipboardHelper
import zed.rainxch.core.domain.utils.ShareManager

actual val corePlatformModule =
module {
// Core
actual val corePlatformModule = module {
// Core

single<Downloader> {
AndroidDownloader(
files = get(),
)
}
single<Downloader> {
AndroidDownloader(
files = get(),
)
}

single<Installer> {
AndroidInstaller(
context = get(),
installerInfoExtractor = AndroidInstallerInfoExtractor(androidContext()),
)
}
// AndroidInstaller — registered by class so the wrapper can inject it
single {
AndroidInstaller(
context = get(),
installerInfoExtractor = AndroidInstallerInfoExtractor(androidContext())
)
}

single<FileLocationsProvider> {
AndroidFileLocationsProvider(context = get())
}
// ShizukuServiceManager — manages Shizuku lifecycle, permissions, service binding
single {
ShizukuServiceManager(
context = androidContext()
).also { it.initialize() }
}

single<PackageMonitor> {
AndroidPackageMonitor(androidContext())
// Installer — the ShizukuInstallerWrapper is the public Installer singleton.
// It delegates to AndroidInstaller by default, intercepting with Shizuku when enabled.
single<Installer> {
ShizukuInstallerWrapper(
androidInstaller = get<AndroidInstaller>(),
shizukuServiceManager = get(),
themesRepository = get()
).also { wrapper ->
wrapper.observeInstallerPreference(get<CoroutineScope>())
}
}

single<LocalizationManager> {
AndroidLocalizationManager()
}
// InstallerStatusProvider — exposes Shizuku availability to the UI layer
single<InstallerStatusProvider> {
AndroidInstallerStatusProvider(
shizukuServiceManager = get(),
scope = get()
)
}

// Locals
single<FileLocationsProvider> {
AndroidFileLocationsProvider(context = get())
}

single<AppDatabase> {
initDatabase(androidContext())
}
single<PackageMonitor> {
AndroidPackageMonitor(androidContext())
}

single<DataStore<Preferences>> {
createDataStore(androidContext())
}
single<LocalizationManager> {
AndroidLocalizationManager()
}

// Utils
// Locals

single<BrowserHelper> {
AndroidBrowserHelper(androidContext())
}
single<AppDatabase> {
initDatabase(androidContext())
}

single<ClipboardHelper> {
AndroidClipboardHelper(androidContext())
}
single<DataStore<Preferences>> {
createDataStore(androidContext())
}

single<AppLauncher> {
AndroidAppLauncher(
context = androidContext(),
logger = get(),
)
}

single<ShareManager> {
AndroidShareManager(
context = androidContext(),
)
}
// Utils

single<BrowserHelper> {
AndroidBrowserHelper(androidContext())
}

single<ClipboardHelper> {
AndroidClipboardHelper(androidContext())
}

single<AppLauncher> {
AndroidAppLauncher(
context = androidContext(),
logger = get()
)
}

single<ShareManager> {
AndroidShareManager(
context = androidContext()
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package zed.rainxch.core.data.services.shizuku

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import zed.rainxch.core.domain.model.ShizukuAvailability
import zed.rainxch.core.domain.system.InstallerStatusProvider

/**
* Android implementation of [InstallerStatusProvider].
* Maps [ShizukuServiceManager.status] to the platform-agnostic [ShizukuAvailability] enum.
*/
class AndroidInstallerStatusProvider(
private val shizukuServiceManager: ShizukuServiceManager,
scope: CoroutineScope
) : InstallerStatusProvider {

override val shizukuAvailability: StateFlow<ShizukuAvailability> =
shizukuServiceManager.status.map { status ->
when (status) {
ShizukuStatus.NOT_INSTALLED -> ShizukuAvailability.UNAVAILABLE
ShizukuStatus.NOT_RUNNING -> ShizukuAvailability.NOT_RUNNING
ShizukuStatus.PERMISSION_NEEDED -> ShizukuAvailability.PERMISSION_NEEDED
ShizukuStatus.READY -> ShizukuAvailability.READY
}
}.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = ShizukuAvailability.UNAVAILABLE
Comment on lines +20 to +31
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

Preserve the NOT_INSTALLED state instead of collapsing it to UNAVAILABLE.

Line 23 drops a user-actionable distinction the new UI needs. With this mapping, the profile flow cannot tell “Shizuku is not installed” apart from generic unavailability, so the install-specific status/hint strings added in this PR become unreachable. Please expose a dedicated availability for that case, or pass the raw ShizukuStatus through to the UI layer.

)

override fun requestShizukuPermission() {
shizukuServiceManager.requestPermission()
}
}
Loading