├── .github ├── dependabot.yaml └── workflows │ ├── android.yaml │ └── maven.yaml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── full │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── dev │ │ │ └── sanmer │ │ │ └── pi │ │ │ ├── datastore │ │ │ └── PreferenceSerializer.kt │ │ │ ├── model │ │ │ └── ServiceState.kt │ │ │ ├── repository │ │ │ ├── PreferenceRepository.kt │ │ │ └── ServiceRepository.kt │ │ │ ├── ui │ │ │ ├── MainActivity.kt │ │ │ ├── main │ │ │ │ ├── MainScreen.kt │ │ │ │ └── SetupScreen.kt │ │ │ └── screens │ │ │ │ ├── apps │ │ │ │ ├── AppsScreen.kt │ │ │ │ └── component │ │ │ │ │ ├── AppItem.kt │ │ │ │ │ └── AppList.kt │ │ │ │ └── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ └── component │ │ │ │ ├── LanguageItem.kt │ │ │ │ ├── ServiceItem.kt │ │ │ │ └── WorkingModeItem.kt │ │ │ └── viewmodel │ │ │ ├── AppsViewModel.kt │ │ │ ├── MainViewModel.kt │ │ │ └── SettingsViewModel.kt │ └── res │ │ └── drawable │ │ ├── launcher_foreground.xml │ │ └── launcher_outline.xml │ ├── lite │ ├── AndroidManifest.xml │ └── kotlin │ │ ├── dev │ │ └── sanmer │ │ │ └── pi │ │ │ ├── model │ │ │ └── ShizukuState.kt │ │ │ └── repository │ │ │ ├── PreferenceRepository.kt │ │ │ └── ServiceRepository.kt │ │ └── kotlinx │ │ └── serialization │ │ └── protobuf │ │ └── ProtoNumber.kt │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── dev │ │ └── sanmer │ │ └── pi │ │ ├── App.kt │ │ ├── Const.kt │ │ ├── compat │ │ ├── BuildCompat.kt │ │ ├── MediaStoreCompat.kt │ │ ├── PermissionCompat.kt │ │ └── VersionCompat.kt │ │ ├── datastore │ │ └── model │ │ │ ├── Preference.kt │ │ │ └── Provider.kt │ │ ├── ktx │ │ ├── ContextExt.kt │ │ ├── DpExt.kt │ │ ├── FlowExt.kt │ │ ├── LocaleExt.kt │ │ ├── LocaleListCompatExt.kt │ │ └── ParcelableExt.kt │ │ ├── model │ │ └── IPackageInfo.kt │ │ ├── receiver │ │ ├── BroadcastReceiverEntryPoint.kt │ │ └── Updated.kt │ │ ├── service │ │ ├── InstallService.kt │ │ └── ParseService.kt │ │ ├── ui │ │ ├── InstallActivity.kt │ │ ├── component │ │ │ ├── Logo.kt │ │ │ ├── MenuChip.kt │ │ │ ├── PageIndicator.kt │ │ │ ├── SearchTopBar.kt │ │ │ └── SettingItem.kt │ │ ├── ktx │ │ │ ├── LazyListStateExt.kt │ │ │ ├── ModifierExt.kt │ │ │ ├── NavControllerExt.kt │ │ │ ├── PaddingValuesExt.kt │ │ │ └── ShapExt.kt │ │ ├── provider │ │ │ └── LocalPreference.kt │ │ ├── screens │ │ │ └── install │ │ │ │ ├── InstallScreen.kt │ │ │ │ └── component │ │ │ │ ├── PackageInfoItem.kt │ │ │ │ ├── SelectUserItem.kt │ │ │ │ ├── SplitConfigItem.kt │ │ │ │ └── TittleItem.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── viewmodel │ │ └── InstallViewModel.kt │ └── res │ ├── drawable │ ├── alert_triangle.xml │ ├── arrow_left.xml │ ├── box.xml │ ├── brand_github.xml │ ├── check.xml │ ├── chevron_right.xml │ ├── circle_check_filled.xml │ ├── code.xml │ ├── command.xml │ ├── cpu.xml │ ├── language.xml │ ├── launcher_foreground.xml │ ├── launcher_outline.xml │ ├── launcher_splash.xml │ ├── list_search.xml │ ├── mood_neutral.xml │ ├── mood_wink.xml │ ├── mood_xd.xml │ ├── photo.xml │ ├── player_play.xml │ ├── search.xml │ ├── settings_2.xml │ ├── user.xml │ ├── user_circle.xml │ ├── world.xml │ └── x.xml │ ├── mipmap-anydpi-v26 │ └── launcher.xml │ ├── values-ar │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-iw │ └── strings.xml │ ├── values-night-v31 │ └── colors.xml │ ├── values-night │ ├── colors.xml │ └── themes.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-su │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-v31 │ └── colors.xml │ ├── values-vi │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ ├── strings_untranslatable.xml │ └── themes.xml │ └── xml │ └── locales_config.xml ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── ApplicationConventionPlugin.kt │ ├── ComposeConventionPlugin.kt │ ├── HiltConventionPlugin.kt │ ├── LibraryConventionPlugin.kt │ ├── ProjectExt.kt │ └── RoomConventionPlugin.kt ├── build.gradle.kts ├── core ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ └── kotlin │ └── dev │ └── sanmer │ └── pi │ ├── BuildCompat.kt │ ├── ContextCompat.kt │ ├── IntentReceiverCompat.kt │ ├── PackageInfoCompat.kt │ ├── PackageParserCompat.kt │ ├── SessionInfoCompat.kt │ ├── UserHandleCompat.kt │ ├── bundle │ ├── ABI.kt │ ├── BundleInfo.kt │ ├── DPI.kt │ └── SplitConfig.kt │ └── delegate │ ├── AppOpsManagerDelegate.kt │ ├── PackageInfoDelegate.kt │ ├── PackageInstallerDelegate.kt │ ├── PackageManagerDelegate.kt │ ├── PermissionManagerDelegate.kt │ └── UserManagerDelegate.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── stub ├── build.gradle.kts └── src └── main └── java ├── android ├── app │ ├── ActivityThread.java │ └── AppOpsManagerHidden.java ├── companion │ └── virtual │ │ └── VirtualDeviceManagerHidden.java ├── content │ ├── ContextHidden.java │ ├── IIntentReceiver.java │ ├── IIntentSender.java │ ├── IntentSenderHidden.java │ └── pm │ │ ├── IPackageInstaller.java │ │ ├── IPackageInstallerCallback.java │ │ ├── IPackageInstallerSession.java │ │ ├── IPackageManager.java │ │ ├── PackageInfoHidden.java │ │ ├── PackageInstallerHidden.java │ │ ├── PackageManagerHidden.java │ │ ├── PackageParser.java │ │ ├── PackageUserState.java │ │ ├── ParceledListSlice.java │ │ ├── UserInfo.java │ │ └── pkg │ │ ├── FrameworkPackageUserState.java │ │ └── FrameworkPackageUserStateDefault.java ├── os │ ├── IUserManager.java │ ├── ServiceManager.java │ ├── SystemProperties.java │ └── UserHandleHidden.java └── permission │ └── IPermissionManager.java └── com └── android └── internal └── app ├── IAppOpsCallback.java └── IAppOpsService.java /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "21:00" 8 | labels: [ "github_actions" ] 9 | 10 | - package-ecosystem: gradle 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | time: "21:00" 15 | labels: [ "dependencies" ] 16 | registries: "*" 17 | ignore: 18 | - dependency-name: "self.*" 19 | groups: 20 | kotlin-ksp: 21 | patterns: 22 | - "org.jetbrains.kotlin:*" 23 | - "org.jetbrains.kotlin.jvm" 24 | - "com.google.devtools.ksp" 25 | - "com.google.devtools.ksp.gradle.plugin" 26 | 27 | registries: 28 | maven-google: 29 | type: "maven-repository" 30 | url: "https://maven.google.com" 31 | replaces-base: true 32 | -------------------------------------------------------------------------------- /.github/workflows/android.yaml: -------------------------------------------------------------------------------- 1 | name: Android 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Checkout compat 20 | uses: actions/checkout@v4 21 | with: 22 | repository: SanmerApps/ServiceManagerCompat 23 | path: compat 24 | 25 | - name: Set up signing key 26 | if: github.ref == 'refs/heads/main' 27 | run: | 28 | if [ ! -z "${{ secrets.KEY_STORE }}" ]; then 29 | echo keyStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> signing.properties 30 | echo keyAlias='${{ secrets.KEY_ALIAS }}' >> signing.properties 31 | echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> signing.properties 32 | echo keyStore='${{ github.workspace }}/key.jks' >> signing.properties 33 | echo ${{ secrets.KEY_STORE }} | base64 --decode > ${{ github.workspace }}/key.jks 34 | fi 35 | 36 | - name: Set up JDK 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: 'jetbrains' 40 | java-version: 21 41 | 42 | - name: Set up Gradle 43 | uses: gradle/actions/setup-gradle@v4 44 | 45 | - name: Build dependencies 46 | working-directory: compat 47 | run: ./gradlew publishToMavenLocal 48 | 49 | - name: Build with Gradle 50 | run: ./gradlew assembleRelease 51 | 52 | - name: Get release name 53 | if: success() && github.ref == 'refs/heads/main' 54 | id: release-name 55 | run: | 56 | name=`ls app/build/outputs/apk/full/release/*.apk | awk -F '(/|-full-release.apk)' '{print $7}'` && echo "name=${name}" >> $GITHUB_OUTPUT 57 | 58 | - name: Upload full apk 59 | if: success() && github.ref == 'refs/heads/main' 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: ${{ steps.release-name.outputs.name }}-full 63 | path: app/build/outputs/apk/full/release/*.apk* 64 | 65 | - name: Upload full mapping 66 | if: success() && github.ref == 'refs/heads/main' 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: ${{ steps.release-name.outputs.name }}-full-mapping 70 | path: app/build/outputs/mapping/fullRelease 71 | 72 | - name: Upload lite apk 73 | if: success() && github.ref == 'refs/heads/main' 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: ${{ steps.release-name.outputs.name }}-lite 77 | path: app/build/outputs/apk/lite/release/*.apk* 78 | 79 | - name: Upload lite mapping 80 | if: success() && github.ref == 'refs/heads/main' 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: ${{ steps.release-name.outputs.name }}-lite-mapping 84 | path: app/build/outputs/mapping/liteRelease -------------------------------------------------------------------------------- /.github/workflows/maven.yaml: -------------------------------------------------------------------------------- 1 | name: Build Maven Packages 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | ref: ${{ github.ref_name }} 20 | 21 | - name: Checkout compat 22 | uses: actions/checkout@v4 23 | with: 24 | repository: SanmerApps/ServiceManagerCompat 25 | path: compat 26 | 27 | - name: Set up JDK 28 | uses: actions/setup-java@v4 29 | with: 30 | distribution: 'jetbrains' 31 | java-version: 21 32 | 33 | - name: Set up Gradle 34 | uses: gradle/actions/setup-gradle@v4 35 | 36 | - name: Build dependencies 37 | working-directory: compat 38 | run: ./gradlew publishToMavenLocal 39 | 40 | - name: Publish 41 | env: 42 | GITHUB_ACTOR: ${{ github.repository_owner }} 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: ./gradlew publish 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle/ 3 | build/ 4 | 5 | # Kotlin 6 | .kotlin 7 | 8 | # Local configuration 9 | local.properties 10 | signing.properties 11 | 12 | # Android Studio 13 | captures/ 14 | release/ 15 | .externalNativeBuild/ 16 | .cxx/ 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | 22 | # Keystore 23 | *.jks 24 | *.keystore 25 | 26 | # MacOS 27 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sanmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PI 2 | [![release](https://img.shields.io/github/v/release/SanmerApps/PI?label=release&color=red)](https://github.com/SanmerApps/PI/releases) [![download](https://shields.io/github/downloads/SanmerApps/PI/total?label=download)](https://github.com/SanmerApps/PI/releases/latest) 3 | 4 | ## Supported Versions 5 | Android 11 ~ 15 6 | 7 | ## Credits 8 | - [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git) 9 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -repackageclasses dev.sanmer.pi -------------------------------------------------------------------------------- /app/src/full/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/datastore/PreferenceSerializer.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.datastore 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.CorruptionException 5 | import androidx.datastore.core.DataStore 6 | import androidx.datastore.core.DataStoreFactory 7 | import androidx.datastore.core.Serializer 8 | import androidx.datastore.dataStoreFile 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import dev.sanmer.pi.datastore.model.Preference 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.withContext 17 | import kotlinx.serialization.SerializationException 18 | import kotlinx.serialization.decodeFromByteArray 19 | import kotlinx.serialization.encodeToByteArray 20 | import kotlinx.serialization.protobuf.ProtoBuf 21 | import java.io.InputStream 22 | import java.io.OutputStream 23 | import javax.inject.Inject 24 | import javax.inject.Singleton 25 | 26 | class PreferenceSerializer @Inject constructor() : Serializer { 27 | override val defaultValue = Preference() 28 | 29 | override suspend fun readFrom(input: InputStream) = 30 | try { 31 | ProtoBuf.decodeFromByteArray(input.readBytes()) 32 | } catch (e: SerializationException) { 33 | throw CorruptionException("Failed to read proto", e) 34 | } 35 | 36 | override suspend fun writeTo(t: Preference, output: OutputStream) = 37 | withContext(Dispatchers.IO) { 38 | output.write(ProtoBuf.encodeToByteArray(t)) 39 | } 40 | 41 | @Module 42 | @InstallIn(SingletonComponent::class) 43 | object Impl { 44 | @Provides 45 | @Singleton 46 | fun dataStore( 47 | @ApplicationContext context: Context, 48 | serializer: PreferenceSerializer 49 | ): DataStore = 50 | DataStoreFactory.create( 51 | serializer = serializer 52 | ) { 53 | context.dataStoreFile("user_preferences.pb") 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/model/ServiceState.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.model 2 | 3 | import dev.sanmer.su.IServiceManager 4 | 5 | sealed class ServiceState { 6 | data object Pending : ServiceState() 7 | data class Success(val service: IServiceManager) : ServiceState() 8 | data class Failure(val error: Throwable) : ServiceState() 9 | 10 | val isPending inline get() = this == Pending 11 | val isSucceed inline get() = this is Success 12 | val isFailed inline get() = this is Failure 13 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.repository 2 | 3 | import androidx.datastore.core.DataStore 4 | import dev.sanmer.pi.datastore.model.Preference 5 | import dev.sanmer.pi.datastore.model.Provider 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | class PreferenceRepository @Inject constructor( 13 | private val dataStore: DataStore 14 | ) { 15 | val data get() = dataStore.data 16 | 17 | suspend fun setProvider(value: Provider) { 18 | withContext(Dispatchers.IO) { 19 | dataStore.updateData { 20 | it.copy( 21 | provider = value 22 | ) 23 | } 24 | } 25 | } 26 | 27 | suspend fun setRequester(value: String) { 28 | withContext(Dispatchers.IO) { 29 | dataStore.updateData { 30 | it.copy( 31 | requester = value 32 | ) 33 | } 34 | } 35 | } 36 | 37 | suspend fun setExecutor(value: String) { 38 | withContext(Dispatchers.IO) { 39 | dataStore.updateData { 40 | it.copy( 41 | executor = value 42 | ) 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/repository/ServiceRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.repository 2 | 3 | import dev.sanmer.pi.datastore.model.Provider 4 | import dev.sanmer.pi.delegate.AppOpsManagerDelegate 5 | import dev.sanmer.pi.delegate.PackageInstallerDelegate 6 | import dev.sanmer.pi.delegate.PackageManagerDelegate 7 | import dev.sanmer.pi.delegate.PermissionManagerDelegate 8 | import dev.sanmer.pi.delegate.UserManagerDelegate 9 | import dev.sanmer.pi.model.ServiceState 10 | import dev.sanmer.su.IServiceManager 11 | import dev.sanmer.su.ServiceManagerCompat 12 | import dev.sanmer.su.ServiceManagerCompat.proxyBy 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.flow.update 18 | import kotlinx.coroutines.launch 19 | import timber.log.Timber 20 | import javax.inject.Inject 21 | import javax.inject.Singleton 22 | 23 | @Singleton 24 | class ServiceRepository @Inject constructor( 25 | private val preferenceRepository: PreferenceRepository 26 | ) { 27 | private val coroutineScope = CoroutineScope(Dispatchers.IO) 28 | 29 | private var _state = MutableStateFlow(ServiceState.Pending) 30 | val state get() = _state.asStateFlow() 31 | 32 | val isSucceed get() = state.value.isSucceed 33 | 34 | init { 35 | preferenceObserver() 36 | } 37 | 38 | private fun preferenceObserver() { 39 | coroutineScope.launch { 40 | preferenceRepository.data.collect { preference -> 41 | _state.update { if (!it.isSucceed) create(preference.provider) else it } 42 | } 43 | } 44 | } 45 | 46 | private suspend fun create(provider: Provider) = try { 47 | when (provider) { 48 | Provider.None -> ServiceState.Pending 49 | Provider.Shizuku -> ServiceState.Success(ServiceManagerCompat.fromShizuku()) 50 | Provider.Superuser -> ServiceState.Success(ServiceManagerCompat.fromLibSu()) 51 | } 52 | } catch (e: Throwable) { 53 | Timber.e(e) 54 | ServiceState.Failure(e) 55 | } 56 | 57 | suspend fun recreate(provider: Provider) { 58 | _state.update { create(provider) } 59 | } 60 | 61 | private fun unsafe(block: (IServiceManager) -> T): T { 62 | return when (val value = state.value) { 63 | is ServiceState.Success -> block(value.service) 64 | is ServiceState.Failure -> throw value.error 65 | ServiceState.Pending -> throw IllegalStateException("Pending") 66 | } 67 | } 68 | 69 | fun getAppOpsManager() = unsafe { ism -> AppOpsManagerDelegate { proxyBy(ism) } } 70 | fun getPackageManager() = unsafe { ism -> PackageManagerDelegate { proxyBy(ism) } } 71 | fun getPackageInstaller() = unsafe { ism -> PackageInstallerDelegate { proxyBy(ism) } } 72 | fun getPermissionManager() = unsafe { ism -> PermissionManagerDelegate { proxyBy(ism) } } 73 | fun getUserManager() = unsafe { ism -> UserManagerDelegate { proxyBy(ism) } } 74 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.activity.viewModels 8 | import androidx.compose.animation.Crossfade 9 | import androidx.compose.foundation.background 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.ui.Modifier 13 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import dev.sanmer.pi.ui.main.MainScreen 16 | import dev.sanmer.pi.ui.main.SetupScreen 17 | import dev.sanmer.pi.ui.provider.LocalPreference 18 | import dev.sanmer.pi.ui.theme.AppTheme 19 | import dev.sanmer.pi.viewmodel.MainViewModel 20 | import dev.sanmer.pi.viewmodel.MainViewModel.LoadState 21 | 22 | @AndroidEntryPoint 23 | class MainActivity : ComponentActivity() { 24 | val viewModel: MainViewModel by viewModels() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | val splashScreen = installSplashScreen() 28 | super.onCreate(savedInstanceState) 29 | enableEdgeToEdge() 30 | 31 | splashScreen.setKeepOnScreenCondition { viewModel.isPending } 32 | 33 | setContent { 34 | when (viewModel.loadState) { 35 | LoadState.Pending -> {} 36 | is LoadState.Ready -> CompositionLocalProvider( 37 | LocalPreference provides viewModel.preference 38 | ) { 39 | AppTheme { 40 | Crossfade( 41 | modifier = Modifier.background( 42 | color = MaterialTheme.colorScheme.background 43 | ), 44 | targetState = viewModel.isNone, 45 | ) { isNone -> 46 | if (isNone) { 47 | SetupScreen( 48 | setProvider = viewModel::setProvider 49 | ) 50 | } else { 51 | MainScreen() 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.main 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.animation.AnimatedContentTransitionScope 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.fadeIn 8 | import androidx.compose.animation.fadeOut 9 | import androidx.compose.runtime.Composable 10 | import androidx.navigation.NamedNavArgument 11 | import androidx.navigation.NavBackStackEntry 12 | import androidx.navigation.NavController 13 | import androidx.navigation.NavGraphBuilder 14 | import androidx.navigation.compose.NavHost 15 | import androidx.navigation.compose.composable 16 | import androidx.navigation.compose.rememberNavController 17 | import dev.sanmer.pi.ui.screens.apps.AppsScreen 18 | import dev.sanmer.pi.ui.screens.settings.SettingsScreen 19 | 20 | @Composable 21 | fun MainScreen() { 22 | val navController = rememberNavController() 23 | 24 | NavHost( 25 | navController = navController, 26 | startDestination = Screen.Apps() 27 | ) { 28 | Screen.Apps(navController).addTo(this) 29 | Screen.Settings(navController).addTo(this) 30 | } 31 | } 32 | 33 | sealed class Screen( 34 | private val route: String, 35 | private val content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 36 | private val arguments: List = emptyList(), 37 | private val enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition) = { fadeIn() }, 38 | private val exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition) = { fadeOut() }, 39 | ) { 40 | fun addTo(builder: NavGraphBuilder) = builder.composable( 41 | route = this@Screen.route, 42 | arguments = this@Screen.arguments, 43 | enterTransition = this@Screen.enterTransition, 44 | exitTransition = this@Screen.exitTransition, 45 | content = this@Screen.content 46 | ) 47 | 48 | @Suppress("FunctionName") 49 | companion object Routes { 50 | fun Apps() = "Apps" 51 | fun Settings() = "Settings" 52 | } 53 | 54 | class Apps(navController: NavController) : Screen( 55 | route = Apps(), 56 | content = { AppsScreen(navController = navController) } 57 | ) 58 | 59 | class Settings(navController: NavController) : Screen( 60 | route = Settings(), 61 | content = { SettingsScreen(navController = navController) } 62 | ) 63 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/main/SetupScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.main 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.requiredHeightIn 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import dev.sanmer.pi.R 18 | import dev.sanmer.pi.datastore.model.Provider 19 | import dev.sanmer.pi.ui.screens.settings.component.WorkingModeItem 20 | 21 | @Composable 22 | fun SetupScreen( 23 | setProvider: (Provider) -> Unit 24 | ) = Column( 25 | modifier = Modifier 26 | .background(color = MaterialTheme.colorScheme.background) 27 | .fillMaxSize(), 28 | verticalArrangement = Arrangement.Center, 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | Text( 32 | text = stringResource(id = R.string.setup_title), 33 | style = MaterialTheme.typography.titleLarge, 34 | color = MaterialTheme.colorScheme.onBackground 35 | ) 36 | 37 | Spacer(modifier = Modifier.height(20.dp)) 38 | WorkingModeItem( 39 | title = stringResource(id = R.string.setup_root_title), 40 | desc = stringResource(id = R.string.setup_root_desc), 41 | modifier = Modifier.requiredHeightIn(min = 150.dp), 42 | onClick = { setProvider(Provider.Superuser) } 43 | ) 44 | 45 | Spacer(modifier = Modifier.height(30.dp)) 46 | WorkingModeItem( 47 | title = stringResource(id = R.string.setup_shizuku_title), 48 | desc = stringResource(id = R.string.setup_shizuku_desc), 49 | modifier = Modifier.requiredHeightIn(min = 150.dp), 50 | onClick = { setProvider(Provider.Shizuku) } 51 | ) 52 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/component/LanguageItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.settings.component 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.provider.Settings 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.stringResource 9 | import dev.sanmer.pi.R 10 | import dev.sanmer.pi.compat.BuildCompat 11 | import dev.sanmer.pi.ktx.applicationLocale 12 | import dev.sanmer.pi.ktx.localizedDisplayName 13 | import dev.sanmer.pi.ui.component.SettingNormalItem 14 | 15 | @Composable 16 | fun LanguageItem( 17 | context: Context 18 | ) = SettingNormalItem( 19 | icon = R.drawable.world, 20 | title = stringResource(id = R.string.settings_language), 21 | desc = context.applicationLocale?.localizedDisplayName ?: stringResource(id = R.string.settings_language_system), 22 | onClick = { 23 | // noinspection InlinedApi 24 | context.startActivity( 25 | Intent( 26 | Settings.ACTION_APP_LOCALE_SETTINGS, 27 | Uri.fromParts("package", context.packageName, null) 28 | ) 29 | ) 30 | }, 31 | enabled = BuildCompat.atLeastT 32 | ) -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/component/ServiceItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.settings.component 2 | 3 | import android.os.Process 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import dev.sanmer.pi.BuildConfig 7 | import dev.sanmer.pi.R 8 | import dev.sanmer.pi.model.ServiceState 9 | import dev.sanmer.pi.ui.component.SettingNormalItem 10 | import dev.sanmer.su.IServiceManager 11 | 12 | @Composable 13 | fun ServiceItem( 14 | state: ServiceState, 15 | restart: () -> Unit 16 | ) = SettingNormalItem( 17 | icon = when (state) { 18 | ServiceState.Pending -> R.drawable.mood_neutral 19 | is ServiceState.Success -> R.drawable.mood_wink 20 | is ServiceState.Failure -> R.drawable.mood_xd 21 | }, 22 | title = stringResource(id = when (state) { 23 | ServiceState.Pending -> R.string.settings_service_starting 24 | is ServiceState.Success -> R.string.settings_service_running 25 | is ServiceState.Failure -> R.string.settings_service_not_running 26 | }), 27 | desc = when (state) { 28 | ServiceState.Pending -> stringResource(id = R.string.settings_service_wait) 29 | is ServiceState.Success -> stringResource( 30 | id = R.string.settings_service_version, 31 | BuildConfig.VERSION_CODE, 32 | state.service.platform 33 | ) 34 | is ServiceState.Failure -> stringResource(id = R.string.settings_service_restart) 35 | }, 36 | onClick = { if (state.isFailed) restart() } 37 | ) 38 | 39 | private val IServiceManager.platform 40 | inline get() = when (uid) { 41 | Process.ROOT_UID -> "root" 42 | Process.SHELL_UID -> "adb" 43 | else -> "unknown (${uid})" 44 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/component/WorkingModeItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.settings.component 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.requiredHeightIn 11 | import androidx.compose.foundation.layout.requiredWidth 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.unit.dp 22 | import dev.sanmer.pi.R 23 | 24 | @Composable 25 | fun WorkingModeItem( 26 | title: String, 27 | desc: String, 28 | modifier: Modifier = Modifier, 29 | selected: Boolean = false, 30 | onClick: () -> Unit 31 | ) = Box( 32 | modifier = Modifier 33 | .requiredWidth(240.dp) 34 | .then(modifier) 35 | ) { 36 | Surface( 37 | onClick = onClick, 38 | tonalElevation = if (selected) 4.dp else 0.dp, 39 | border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outline), 40 | shape = MaterialTheme.shapes.large 41 | ) { 42 | Column( 43 | modifier = Modifier 44 | .padding(all = 16.dp) 45 | .requiredHeightIn(min = 120.dp) 46 | .fillMaxWidth() 47 | ) { 48 | Text( 49 | text = title, 50 | style = MaterialTheme.typography.titleMedium, 51 | color = MaterialTheme.colorScheme.primary 52 | ) 53 | 54 | Spacer(modifier = Modifier.height(10.dp)) 55 | 56 | Text( 57 | text = desc, 58 | style = MaterialTheme.typography.bodyMedium, 59 | color = MaterialTheme.colorScheme.onSurfaceVariant 60 | ) 61 | } 62 | } 63 | 64 | if (selected) { 65 | Box( 66 | modifier = Modifier 67 | .padding(top = 8.dp, end = 8.dp) 68 | .align(Alignment.TopEnd) 69 | ) { 70 | Icon( 71 | painter = painterResource(id = R.drawable.circle_check_filled), 72 | contentDescription = null, 73 | tint = MaterialTheme.colorScheme.primary, 74 | modifier = Modifier.size(30.dp) 75 | ) 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import dev.sanmer.pi.datastore.model.Preference 10 | import dev.sanmer.pi.datastore.model.Provider 11 | import dev.sanmer.pi.repository.PreferenceRepository 12 | import kotlinx.coroutines.launch 13 | import timber.log.Timber 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class MainViewModel @Inject constructor( 18 | private val preferenceRepository: PreferenceRepository 19 | ) : ViewModel() { 20 | var loadState by mutableStateOf(LoadState.Pending) 21 | private set 22 | 23 | val isPending inline get() = loadState.isPending 24 | val preference inline get() = loadState.preference 25 | val isNone inline get() = preference.provider == Provider.None 26 | 27 | init { 28 | Timber.d("MainViewModel init") 29 | preferenceObserver() 30 | } 31 | 32 | private fun preferenceObserver() { 33 | viewModelScope.launch { 34 | preferenceRepository.data.collect { 35 | loadState = LoadState.Ready(it) 36 | } 37 | } 38 | } 39 | 40 | fun setProvider(value: Provider) { 41 | viewModelScope.launch { 42 | preferenceRepository.setProvider(value) 43 | } 44 | } 45 | 46 | sealed class LoadState { 47 | abstract val preference: Preference 48 | 49 | data object Pending : LoadState() { 50 | override val preference = Preference() 51 | } 52 | 53 | data class Ready( 54 | override val preference: Preference 55 | ) : LoadState() 56 | 57 | val isPending inline get() = this is Pending 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/full/kotlin/dev/sanmer/pi/viewmodel/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import dev.sanmer.pi.datastore.model.Provider 7 | import dev.sanmer.pi.repository.PreferenceRepository 8 | import dev.sanmer.pi.repository.ServiceRepository 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.coroutines.launch 11 | import timber.log.Timber 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class SettingsViewModel @Inject constructor( 16 | private val preferenceRepository: PreferenceRepository, 17 | private val serviceRepository: ServiceRepository 18 | ) : ViewModel() { 19 | val state = serviceRepository.state 20 | 21 | init { 22 | Timber.d("SettingsViewModel init") 23 | } 24 | 25 | fun setProvider(value: Provider) { 26 | viewModelScope.launch { 27 | preferenceRepository.setProvider(value) 28 | } 29 | } 30 | 31 | fun restart() { 32 | viewModelScope.launch { 33 | val preference = preferenceRepository.data.first() 34 | serviceRepository.recreate(preference.provider) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/full/res/drawable/launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/full/res/drawable/launcher_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/lite/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/lite/kotlin/dev/sanmer/pi/model/ShizukuState.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.model 2 | 3 | sealed class ShizukuState { 4 | data object Pending : ShizukuState() 5 | data class Success(val uid: Int) : ShizukuState() 6 | data class Failure(val error: Throwable) : ShizukuState() 7 | 8 | val isPending inline get() = this == Pending 9 | val isSucceed inline get() = this is Success 10 | val isFailed inline get() = this is Failure 11 | } -------------------------------------------------------------------------------- /app/src/lite/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.repository 2 | 3 | import dev.sanmer.pi.datastore.model.Preference 4 | import kotlinx.coroutines.flow.flowOf 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class PreferenceRepository @Inject constructor() { 10 | val data = flowOf(Preference()) 11 | } -------------------------------------------------------------------------------- /app/src/lite/kotlin/dev/sanmer/pi/repository/ServiceRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.repository 2 | 3 | import android.content.pm.PackageManager 4 | import android.os.IBinder 5 | import dev.sanmer.pi.delegate.AppOpsManagerDelegate 6 | import dev.sanmer.pi.delegate.PackageInstallerDelegate 7 | import dev.sanmer.pi.delegate.PackageManagerDelegate 8 | import dev.sanmer.pi.delegate.PermissionManagerDelegate 9 | import dev.sanmer.pi.delegate.UserManagerDelegate 10 | import dev.sanmer.pi.model.ShizukuState 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.launch 16 | import kotlinx.coroutines.suspendCancellableCoroutine 17 | import rikka.shizuku.Shizuku 18 | import rikka.shizuku.ShizukuBinderWrapper 19 | import javax.inject.Inject 20 | import javax.inject.Singleton 21 | import kotlin.coroutines.resume 22 | 23 | @Singleton 24 | class ServiceRepository @Inject constructor() { 25 | private val coroutineScope = CoroutineScope(Dispatchers.Main) 26 | 27 | private var _state = MutableStateFlow(ShizukuState.Pending) 28 | val state get() = _state.asStateFlow() 29 | 30 | private val isGranted get() = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 31 | 32 | init { 33 | coroutineScope.launch { 34 | _state.value = when { 35 | !isAvailable() -> ShizukuState.Failure(IllegalStateException("Shizuku not available")) 36 | !isAuthorized() -> ShizukuState.Failure(IllegalStateException("Shizuku not authorized")) 37 | else -> ShizukuState.Success(Shizuku.getUid()) 38 | } 39 | } 40 | } 41 | 42 | private fun isAvailable(): Boolean { 43 | return Shizuku.pingBinder() 44 | } 45 | 46 | private suspend fun isAuthorized() = when { 47 | isGranted -> true 48 | else -> suspendCancellableCoroutine { continuation -> 49 | val listener = object : Shizuku.OnRequestPermissionResultListener { 50 | override fun onRequestPermissionResult( 51 | requestCode: Int, 52 | grantResult: Int 53 | ) { 54 | Shizuku.removeRequestPermissionResultListener(this) 55 | continuation.resume(isGranted) 56 | } 57 | } 58 | 59 | Shizuku.addRequestPermissionResultListener(listener) 60 | continuation.invokeOnCancellation { 61 | Shizuku.removeRequestPermissionResultListener(listener) 62 | } 63 | Shizuku.requestPermission(listener.hashCode()) 64 | } 65 | } 66 | 67 | private fun IBinder.proxy() = ShizukuBinderWrapper(this) 68 | 69 | fun getAppOpsManager() = AppOpsManagerDelegate { proxy() } 70 | fun getPackageManager() = PackageManagerDelegate { proxy() } 71 | fun getPackageInstaller() = PackageInstallerDelegate { proxy() } 72 | fun getPermissionManager() = PermissionManagerDelegate { proxy() } 73 | fun getUserManager() = UserManagerDelegate { proxy() } 74 | } -------------------------------------------------------------------------------- /app/src/lite/kotlin/kotlinx/serialization/protobuf/ProtoNumber.kt: -------------------------------------------------------------------------------- 1 | package kotlinx.serialization.protobuf 2 | 3 | annotation class ProtoNumber(val value: Int) 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/App.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.app.Application 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import androidx.core.app.NotificationManagerCompat 8 | import coil.ImageLoader 9 | import coil.ImageLoaderFactory 10 | import dagger.hilt.android.HiltAndroidApp 11 | import dev.sanmer.pi.ktx.dp 12 | import me.zhanghai.android.appiconloader.coil.AppIconFetcher 13 | import me.zhanghai.android.appiconloader.coil.AppIconKeyer 14 | import org.lsposed.hiddenapibypass.HiddenApiBypass 15 | import timber.log.Timber 16 | 17 | @HiltAndroidApp 18 | class App : Application(), ImageLoaderFactory { 19 | init { 20 | Timber.plant(Timber.DebugTree()) 21 | } 22 | 23 | override fun onCreate() { 24 | super.onCreate() 25 | 26 | createNotificationChannels(this) 27 | HiddenApiBypass.setHiddenApiExemptions("") 28 | } 29 | 30 | override fun newImageLoader() = 31 | ImageLoader.Builder(this) 32 | .components { 33 | add(AppIconKeyer()) 34 | add(AppIconFetcher.Factory(40.dp, true, this@App)) 35 | } 36 | .build() 37 | 38 | private fun createNotificationChannels(context: Context) { 39 | val channels = listOf( 40 | NotificationChannel( 41 | Const.CHANNEL_ID_PARSE, 42 | context.getString(R.string.parsing_service), 43 | NotificationManager.IMPORTANCE_HIGH 44 | ), 45 | NotificationChannel( 46 | Const.CHANNEL_ID_INSTALL, 47 | context.getString(R.string.installation_service), 48 | NotificationManager.IMPORTANCE_HIGH 49 | ) 50 | ) 51 | 52 | NotificationManagerCompat.from(context).apply { 53 | createNotificationChannels(channels) 54 | deleteUnlistedNotificationChannels(channels.map { it.id }) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/Const.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | object Const { 4 | const val GITHUB_URL = "https://github.com/SanmerApps/PI" 5 | 6 | const val CHANNEL_ID_INSTALL = "INSTALL" 7 | const val CHANNEL_ID_PARSE = "PARSE" 8 | 9 | const val NOTIFICATION_ID_INSTALL = 1024 10 | const val NOTIFICATION_ID_PARSE = 1025 11 | const val NOTIFICATION_ID_UPDATED = 1026 12 | 13 | const val SHELL = "com.android.shell" 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/compat/BuildCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.compat 2 | 3 | import android.os.Build 4 | import androidx.annotation.ChecksSdkIntAtLeast 5 | 6 | object BuildCompat { 7 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 8 | val atLeastU inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 9 | 10 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) 11 | val atLeastT inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 12 | 13 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) 14 | val atLeastS inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/compat/MediaStoreCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.compat 2 | 3 | import android.content.ContentResolver 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.net.Uri 7 | import android.provider.DocumentsContract 8 | import android.provider.MediaStore 9 | import android.system.Os 10 | import androidx.core.net.toFile 11 | import androidx.documentfile.provider.DocumentFile 12 | import java.io.File 13 | 14 | object MediaStoreCompat { 15 | fun Context.createMediaStoreUri( 16 | file: File, 17 | collection: Uri = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL), 18 | mimeType: String 19 | ): Uri { 20 | val entry = ContentValues().apply { 21 | put(MediaStore.MediaColumns.DISPLAY_NAME, file.name) 22 | put(MediaStore.MediaColumns.RELATIVE_PATH, file.parent) 23 | put(MediaStore.MediaColumns.MIME_TYPE, mimeType) 24 | } 25 | 26 | return requireNotNull(contentResolver.insert(collection, entry)) 27 | } 28 | 29 | private fun ContentResolver.queryString(uri: Uri, columnName: String): String? { 30 | query( 31 | uri, 32 | arrayOf(columnName), 33 | null, 34 | null, 35 | null 36 | )?.use { cursor -> 37 | if (cursor.moveToFirst()) { 38 | return cursor.getString( 39 | cursor.getColumnIndexOrThrow(columnName) 40 | ) 41 | } 42 | } 43 | 44 | return null 45 | } 46 | 47 | fun Context.getOwnerPackageNameForUri(uri: Uri): String? { 48 | require(uri.scheme == "content") { "Expected scheme = content" } 49 | return when { 50 | uri.authority == MediaStore.AUTHORITY -> { 51 | contentResolver.queryString( 52 | uri = uri, 53 | columnName = MediaStore.MediaColumns.OWNER_PACKAGE_NAME 54 | ) 55 | } 56 | 57 | else -> { 58 | uri.authority?.let { 59 | packageManager.resolveContentProvider( 60 | it, 0 61 | )?.packageName 62 | } 63 | } 64 | } 65 | } 66 | 67 | fun Context.getDisplayNameForUri(uri: Uri): String { 68 | if (uri.scheme == "file") { 69 | return uri.toFile().name 70 | } 71 | 72 | require(uri.scheme == "content") { "Expected scheme = content" } 73 | return contentResolver.queryString( 74 | uri = uri, 75 | columnName = MediaStore.MediaColumns.DISPLAY_NAME 76 | ) ?: uri.toString() 77 | } 78 | 79 | private fun getDocumentUri(context: Context, uri: Uri): Uri { 80 | return when { 81 | DocumentsContract.isTreeUri(uri) -> DocumentFile.fromTreeUri(context, uri)?.uri ?: uri 82 | else -> uri 83 | } 84 | } 85 | 86 | fun Context.getPathForUri(uri: Uri): String { 87 | if (uri.scheme == "file") { 88 | return uri.toFile().path 89 | } 90 | 91 | require(uri.scheme == "content") { "Expected scheme = content" } 92 | contentResolver.openFileDescriptor( 93 | getDocumentUri(this, uri), "r" 94 | )?.use { 95 | return Os.readlink("/proc/self/fd/${it.fd}") 96 | } 97 | 98 | return uri.toString() 99 | } 100 | 101 | fun Context.copyToFile(uri: Uri, file: File): Long? { 102 | return contentResolver.openInputStream(uri)?.buffered()?.use { input -> 103 | file.outputStream().use(input::copyTo) 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/compat/PermissionCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.compat 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import androidx.activity.result.ActivityResultRegistryOwner 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.core.content.ContextCompat 8 | import dev.sanmer.pi.ktx.findActivity 9 | import java.util.UUID 10 | 11 | object PermissionCompat { 12 | @JvmInline 13 | value class PermissionState( 14 | private val state: Map 15 | ) { 16 | val allGranted get() = state.all { it.value } 17 | } 18 | 19 | fun checkPermissions( 20 | context: Context, 21 | permissions: List 22 | ) = PermissionState( 23 | permissions.associateWith { 24 | ContextCompat.checkSelfPermission( 25 | context, it 26 | ) == PackageManager.PERMISSION_GRANTED 27 | } 28 | ) 29 | 30 | fun checkPermission( 31 | context: Context, 32 | permission: String 33 | ) = checkPermissions( 34 | context = context, 35 | permissions = listOf(permission) 36 | ).allGranted 37 | 38 | fun requestPermissions( 39 | context: Context, 40 | permissions: List, 41 | callback: (PermissionState) -> Unit = {} 42 | ) { 43 | val activity = context.findActivity() 44 | if (activity !is ActivityResultRegistryOwner) return 45 | 46 | val activityResultRegistry = activity.activityResultRegistry 47 | val launcher = activityResultRegistry.register( 48 | key = UUID.randomUUID().toString(), 49 | contract = ActivityResultContracts.RequestMultiplePermissions(), 50 | callback = { callback(PermissionState(it)) } 51 | ) 52 | 53 | launcher.launch(permissions.toTypedArray()) 54 | } 55 | 56 | fun requestPermission( 57 | context: Context, 58 | permission: String, 59 | callback: (Boolean) -> Unit = {} 60 | ) = requestPermissions( 61 | context = context, 62 | permissions = listOf(permission), 63 | callback = { callback(it.allGranted) } 64 | ) 65 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/compat/VersionCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.compat 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.text.format.Formatter 6 | import dev.sanmer.pi.PackageInfoCompat.compileSdkVersion 7 | import dev.sanmer.pi.PackageInfoCompat.isEmpty 8 | import dev.sanmer.pi.R 9 | 10 | object VersionCompat { 11 | private fun > Pair.comparator( 12 | ctx: Context, 13 | other: Pair 14 | ): String { 15 | val (value0, text0) = this 16 | val (value1, text1) = other 17 | return when { 18 | value0 == value1 -> text0.ifEmpty { value0.toString() } 19 | else -> ctx.getString( 20 | R.string.comparator, 21 | text0.ifEmpty { value0.toString() }, 22 | text1.ifEmpty { value1.toString() } 23 | ) 24 | } 25 | } 26 | 27 | private fun > T.comparator( 28 | ctx: Context, 29 | other: T, 30 | ) = (this to "").comparator( 31 | ctx = ctx, 32 | other = other to "" 33 | ) 34 | 35 | val PackageInfo.versionStr 36 | inline get() = "$versionName (${longVersionCode})" 37 | 38 | private val PackageInfo.compileSdkDisplay 39 | inline get() = if (compileSdkVersion != 0) compileSdkVersion.toString() else "?" 40 | 41 | fun PackageInfo.getSdkVersion(context: Context): String { 42 | if (isEmpty) return "" 43 | val appInfo = requireNotNull(applicationInfo) 44 | return context.getString( 45 | R.string.sdk_versions, 46 | appInfo.targetSdkVersion.toString(), 47 | appInfo.minSdkVersion.toString(), 48 | compileSdkDisplay 49 | ) 50 | } 51 | 52 | fun PackageInfo.getVersionDiff(context: Context, other: PackageInfo): String { 53 | if (isEmpty) return other.versionStr 54 | if (other.isEmpty) return versionStr 55 | return (longVersionCode to versionStr).comparator( 56 | ctx = context, 57 | other = other.longVersionCode to other.versionStr, 58 | ) 59 | } 60 | 61 | fun PackageInfo.getSdkVersionDiff(context: Context, other: PackageInfo): String { 62 | if (isEmpty) return other.getSdkVersion(context) 63 | if (other.isEmpty) return getSdkVersion(context) 64 | val appInfo0 = requireNotNull(applicationInfo) 65 | val appInfo1 = requireNotNull(other.applicationInfo) 66 | return context.getString( 67 | R.string.sdk_versions, 68 | appInfo0.targetSdkVersion.comparator( 69 | ctx = context, 70 | other = appInfo1.targetSdkVersion 71 | ), 72 | appInfo0.minSdkVersion.comparator( 73 | ctx = context, 74 | other = appInfo1.minSdkVersion 75 | ), 76 | (compileSdkVersion to compileSdkDisplay).comparator( 77 | ctx = context, 78 | other = other.compileSdkVersion to other.compileSdkDisplay 79 | ) 80 | ) 81 | } 82 | 83 | fun Long.fileSize(context: Context): String { 84 | val value = Formatter.formatFileSize(context, this) 85 | return context.getString(R.string.file_size, value) 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/datastore/model/Preference.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.datastore.model 2 | 3 | import dev.sanmer.pi.Const 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.protobuf.ProtoNumber 6 | 7 | @Serializable 8 | data class Preference( 9 | @ProtoNumber(1) 10 | val provider: Provider = Provider.None, 11 | @ProtoNumber(3) 12 | val requester: String = "", 13 | @ProtoNumber(4) 14 | val executor: String = Const.SHELL 15 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/datastore/model/Provider.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.datastore.model 2 | 3 | enum class Provider { 4 | None, 5 | Shizuku, 6 | Superuser 7 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.content.Intent 7 | import androidx.core.app.LocaleManagerCompat 8 | import java.util.Locale 9 | 10 | val Context.applicationLocale: Locale? 11 | get() = LocaleManagerCompat.getApplicationLocales(applicationContext) 12 | .toList().firstOrNull() 13 | 14 | fun Context.viewUrl(url: String) { 15 | startActivity( 16 | Intent.parseUri(url, Intent.URI_INTENT_SCHEME) 17 | ) 18 | } 19 | 20 | fun Context.findActivity(): Activity? { 21 | var context = this 22 | while (context is ContextWrapper) { 23 | if (context is Activity) return context 24 | context = context.baseContext 25 | } 26 | 27 | return null 28 | } 29 | 30 | fun Context.finishActivity() { 31 | if (this is Activity) finish() 32 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/DpExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import android.content.res.Resources 4 | 5 | val Int.dp get() = times(Resources.getSystem().displayMetrics.density).toInt() -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.collectLatest 5 | import kotlinx.coroutines.flow.combine 6 | 7 | suspend fun Flow.combineToLatest( 8 | other: Flow, 9 | transform: suspend (T1, T2) -> R 10 | ) = combine(other) { t1, t2 -> t1 to t2 } 11 | .collectLatest { (t1, t2) -> transform(t1, t2) } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/LocaleExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import java.util.Locale 4 | 5 | val Locale.localizedDisplayName: String 6 | inline get() = getDisplayName(this) 7 | .replaceFirstChar { 8 | if (it.isLowerCase()) { 9 | it.titlecase(this) 10 | } else { 11 | it.toString() 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/LocaleListCompatExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import androidx.core.os.LocaleListCompat 4 | import java.util.Locale 5 | 6 | fun LocaleListCompat.toList(): List = List(size()) { this[it]!! } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ktx/ParcelableExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ktx 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | import androidx.core.content.IntentCompat 7 | import androidx.core.os.BundleCompat 8 | 9 | inline fun Intent.parcelable(key: String): T? = 10 | IntentCompat.getParcelableExtra(this, key, T::class.java) 11 | 12 | inline fun Bundle.parcelable(key: String): T? = 13 | BundleCompat.getParcelable(this, key, T::class.java) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/model/IPackageInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.model 2 | 3 | import android.content.pm.PackageInfo 4 | import androidx.compose.runtime.Immutable 5 | import dev.sanmer.pi.delegate.PackageInfoDelegate 6 | 7 | @Immutable 8 | data class IPackageInfo( 9 | private val inner: PackageInfo, 10 | val isAuthorized: Boolean, 11 | val isRequester: Boolean, 12 | val isExecutor: Boolean 13 | ) : PackageInfoDelegate(inner) { 14 | companion object Default { 15 | fun PackageInfo.toIPackageInfo( 16 | isAuthorized: Boolean = false, 17 | isRequester: Boolean = false, 18 | isExecutor: Boolean = false 19 | ) = IPackageInfo( 20 | inner = this, 21 | isAuthorized = isAuthorized, 22 | isRequester = isRequester, 23 | isExecutor = isExecutor 24 | ) 25 | 26 | fun empty() = IPackageInfo( 27 | inner = PackageInfo(), 28 | isAuthorized = false, 29 | isRequester = false, 30 | isExecutor = false 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/receiver/BroadcastReceiverEntryPoint.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.receiver 2 | 3 | import dagger.hilt.EntryPoint 4 | import dagger.hilt.InstallIn 5 | import dagger.hilt.components.SingletonComponent 6 | import dev.sanmer.pi.repository.PreferenceRepository 7 | import dev.sanmer.pi.repository.ServiceRepository 8 | 9 | @EntryPoint 10 | @InstallIn(SingletonComponent::class) 11 | interface BroadcastReceiverEntryPoint { 12 | fun preferenceRepository(): PreferenceRepository 13 | fun serviceRepository(): ServiceRepository 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.receiver 2 | 3 | import android.Manifest 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import androidx.core.app.NotificationCompat 9 | import androidx.core.app.NotificationManagerCompat 10 | import dagger.hilt.android.EntryPointAccessors 11 | import dev.sanmer.pi.Const 12 | import dev.sanmer.pi.R 13 | import dev.sanmer.pi.compat.BuildCompat 14 | import dev.sanmer.pi.compat.PermissionCompat 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.flow.first 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.withContext 20 | import timber.log.Timber 21 | 22 | class Updated : BroadcastReceiver() { 23 | private val coroutineScope = CoroutineScope(Dispatchers.IO) 24 | 25 | override fun onReceive(context: Context, intent: Intent?) { 26 | when (intent?.action) { 27 | Intent.ACTION_MY_PACKAGE_REPLACED -> { 28 | val pending = goAsync() 29 | coroutineScope.launch { 30 | context.deleteExternalCacheDir() 31 | context.performDexOpt() 32 | pending.finish() 33 | } 34 | } 35 | } 36 | } 37 | 38 | private suspend fun Context.deleteExternalCacheDir() = withContext(Dispatchers.IO) { 39 | externalCacheDir?.deleteRecursively() 40 | } 41 | 42 | private suspend fun Context.performDexOpt() = withContext(Dispatchers.IO) { 43 | val entryPoint = EntryPointAccessors.fromApplication( 44 | applicationContext, 45 | BroadcastReceiverEntryPoint::class.java 46 | ) 47 | 48 | val serviceRepository = entryPoint.serviceRepository() 49 | val state = serviceRepository.state.first { !it.isPending } 50 | 51 | notifyUpdated() 52 | if (state.isSucceed) { 53 | runCatching { 54 | Timber.d("optimize $packageName") 55 | val pm = serviceRepository.getPackageManager() 56 | pm.clearApplicationProfileData(packageName) 57 | pm.performDexOpt(packageName).also { 58 | if (!it) Timber.e("Failed to optimize $packageName") 59 | } 60 | }.onFailure { error -> 61 | Timber.e(error, "Failed to optimize $packageName") 62 | } 63 | } 64 | } 65 | 66 | private fun Context.notifyUpdated() { 67 | if ( 68 | BuildCompat.atLeastT 69 | && !PermissionCompat.checkPermission(this, Manifest.permission.POST_NOTIFICATIONS) 70 | ) return 71 | 72 | val intent = packageManager.getLaunchIntentForPackage(packageName) ?: return 73 | val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 74 | val pending = PendingIntent.getActivity(this, 0, intent, flag) 75 | val builder = NotificationCompat.Builder(this, Const.CHANNEL_ID_INSTALL) 76 | .setSmallIcon(R.drawable.launcher_outline) 77 | .setContentIntent(pending) 78 | .setContentTitle(getText(R.string.updated_title)) 79 | .setContentText(getText(R.string.updated_text)) 80 | .setAutoCancel(true) 81 | 82 | NotificationManagerCompat.from(this).apply { 83 | notify(Const.NOTIFICATION_ID_UPDATED, builder.build()) 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/component/Logo.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.component 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.LocalContentColor 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.Shape 16 | import androidx.compose.ui.res.painterResource 17 | 18 | @Composable 19 | fun Logo( 20 | @DrawableRes icon: Int, 21 | modifier: Modifier = Modifier, 22 | shape: Shape = CircleShape, 23 | contentColor: Color = MaterialTheme.colorScheme.onPrimary, 24 | containerColor: Color = MaterialTheme.colorScheme.primary, 25 | fraction: Float = 0.6f 26 | ) = Surface( 27 | modifier = modifier, 28 | shape = shape, 29 | color = containerColor, 30 | contentColor = contentColor 31 | ) { 32 | Box( 33 | contentAlignment = Alignment.Center 34 | ) { 35 | Icon( 36 | modifier = Modifier.fillMaxSize(fraction), 37 | painter = painterResource(id = icon), 38 | contentDescription = null, 39 | tint = LocalContentColor.current 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/component/MenuChip.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.component 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material3.FilterChip 8 | import androidx.compose.material3.FilterChipDefaults 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.LocalContentColor 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.dp 18 | import dev.sanmer.pi.R 19 | 20 | @Composable 21 | fun MenuChip( 22 | selected: Boolean, 23 | onClick: () -> Unit, 24 | label: @Composable () -> Unit, 25 | modifier: Modifier = Modifier, 26 | enabled: Boolean = true, 27 | ) = FilterChip( 28 | selected = selected, 29 | onClick = onClick, 30 | label = label, 31 | modifier = modifier.height(FilterChipDefaults.Height), 32 | enabled = enabled, 33 | leadingIcon = { 34 | if (!selected) { 35 | Point(size = 8.dp) 36 | } 37 | }, 38 | trailingIcon = { 39 | if (selected) { 40 | Icon( 41 | painter = painterResource(id = R.drawable.check), 42 | contentDescription = null, 43 | modifier = Modifier.size(FilterChipDefaults.IconSize) 44 | ) 45 | } 46 | }, 47 | shape = CircleShape, 48 | colors = FilterChipDefaults.filterChipColors( 49 | iconColor = MaterialTheme.colorScheme.secondary, 50 | selectedContainerColor = MaterialTheme.colorScheme.secondary, 51 | selectedLabelColor = MaterialTheme.colorScheme.onSecondary, 52 | selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondary, 53 | selectedTrailingIconColor = MaterialTheme.colorScheme.onSecondary 54 | ), 55 | border = FilterChipDefaults.filterChipBorder( 56 | enabled = enabled, 57 | selected = selected, 58 | borderColor = MaterialTheme.colorScheme.secondary, 59 | ) 60 | ) 61 | 62 | @Composable 63 | private fun Point( 64 | size: Dp, 65 | color: Color = LocalContentColor.current 66 | ) = Canvas( 67 | modifier = Modifier.size(size) 68 | ) { 69 | drawCircle( 70 | color = color, 71 | radius = this.size.width / 2, 72 | center = this.center 73 | ) 74 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/ktx/LazyListStateExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.ktx 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.State 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | 12 | @Composable 13 | fun LazyListState.isScrollingUp(): State { 14 | var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } 15 | var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } 16 | return remember(this) { 17 | derivedStateOf { 18 | if (previousIndex != firstVisibleItemIndex) { 19 | previousIndex > firstVisibleItemIndex 20 | } else { 21 | previousScrollOffset >= firstVisibleItemScrollOffset 22 | }.also { 23 | previousIndex = firstVisibleItemIndex 24 | previousScrollOffset = firstVisibleItemScrollOffset 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/ktx/ModifierExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.ktx 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.runtime.Stable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.clip 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.Shape 11 | 12 | @Stable 13 | fun Modifier.surface( 14 | shape: Shape, 15 | backgroundColor: Color, 16 | border: BorderStroke? = null 17 | ) = then(if (border != null) Modifier.border(border = border, shape = shape) else Modifier) 18 | .background(color = backgroundColor, shape = shape) 19 | .clip(shape = shape) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/ktx/NavControllerExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.ktx 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavOptionsBuilder 5 | 6 | fun NavController.navigateSingleTopTo( 7 | route: String, 8 | builder: NavOptionsBuilder.() -> Unit = {} 9 | ) = navigate( 10 | route = route 11 | ) { 12 | launchSingleTop = true 13 | restoreState = true 14 | builder() 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/ktx/PaddingValuesExt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package dev.sanmer.pi.ui.ktx 4 | 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.LayoutDirection 8 | import javax.annotation.concurrent.Immutable 9 | 10 | inline operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = 11 | OperatorPaddingValues(this, other, Dp::plus) 12 | 13 | inline operator fun PaddingValues.minus(other: PaddingValues): PaddingValues = 14 | OperatorPaddingValues(this, other, Dp::minus) 15 | 16 | @Immutable 17 | class OperatorPaddingValues( 18 | private val that: PaddingValues, 19 | private val other: PaddingValues, 20 | private val operator: Dp.(Dp) -> Dp, 21 | ) : PaddingValues { 22 | override fun calculateBottomPadding(): Dp = 23 | operator( 24 | that.calculateBottomPadding(), 25 | other.calculateBottomPadding() 26 | ) 27 | 28 | override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 29 | operator( 30 | that.calculateLeftPadding(layoutDirection), 31 | other.calculateLeftPadding(layoutDirection) 32 | ) 33 | 34 | override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 35 | operator( 36 | that.calculateRightPadding(layoutDirection), 37 | other.calculateRightPadding(layoutDirection) 38 | ) 39 | 40 | override fun calculateTopPadding(): Dp = 41 | operator( 42 | that.calculateTopPadding(), 43 | other.calculateTopPadding() 44 | ) 45 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/ktx/ShapExt.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.ktx 2 | 3 | import androidx.compose.foundation.shape.CornerBasedShape 4 | import androidx.compose.foundation.shape.CornerSize 5 | import androidx.compose.ui.unit.Dp 6 | 7 | fun CornerBasedShape.bottom(size: Dp) = 8 | copy(bottomStart = CornerSize(size), bottomEnd = CornerSize(size)) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/provider/LocalPreference.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.provider 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import dev.sanmer.pi.datastore.model.Preference 5 | 6 | val LocalPreference = staticCompositionLocalOf { Preference() } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/PackageInfoItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.install.component 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.OutlinedCard 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.derivedStateOf 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.unit.dp 19 | import coil.compose.AsyncImage 20 | import coil.request.ImageRequest 21 | import dev.sanmer.pi.compat.VersionCompat.getSdkVersion 22 | import dev.sanmer.pi.compat.VersionCompat.versionStr 23 | import dev.sanmer.pi.model.IPackageInfo 24 | 25 | @Composable 26 | fun PackageInfoItem( 27 | packageInfo: IPackageInfo, 28 | versionDiff: String? = null, 29 | sdkVersionDiff: String? = null, 30 | fileSize: String? = null 31 | ) = OutlinedCard( 32 | shape = MaterialTheme.shapes.large, 33 | ) { 34 | Row( 35 | modifier = Modifier 36 | .padding(15.dp) 37 | .fillMaxWidth(), 38 | verticalAlignment = Alignment.CenterVertically 39 | ) { 40 | val context = LocalContext.current 41 | AsyncImage( 42 | modifier = Modifier.size(45.dp), 43 | model = ImageRequest.Builder(context) 44 | .data(packageInfo) 45 | .build(), 46 | contentDescription = null 47 | ) 48 | 49 | Column( 50 | modifier = Modifier.padding(start = 15.dp) 51 | ) { 52 | Text( 53 | text = packageInfo.appLabel, 54 | style = MaterialTheme.typography.bodyLarge 55 | ) 56 | 57 | Text( 58 | text = packageInfo.packageName, 59 | style = MaterialTheme.typography.bodyMedium 60 | ) 61 | 62 | val versionStr by remember { 63 | derivedStateOf { versionDiff ?: packageInfo.versionStr } 64 | } 65 | Text( 66 | text = versionStr, 67 | style = MaterialTheme.typography.bodySmall, 68 | color = MaterialTheme.colorScheme.outline 69 | ) 70 | 71 | val sdkVersion by remember { 72 | derivedStateOf { sdkVersionDiff ?: packageInfo.getSdkVersion(context) } 73 | } 74 | Text( 75 | text = buildString { 76 | append(sdkVersion) 77 | fileSize?.let { 78 | append(", ") 79 | append(it) 80 | } 81 | }, 82 | style = MaterialTheme.typography.bodySmall, 83 | color = MaterialTheme.colorScheme.outline 84 | ) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SelectUserItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.install.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.items 10 | import androidx.compose.material3.CardDefaults 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.ModalBottomSheet 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.surfaceColorAtElevation 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import dev.sanmer.pi.R 23 | import dev.sanmer.pi.ui.ktx.bottom 24 | import dev.sanmer.pi.ui.ktx.surface 25 | import dev.sanmer.pi.viewmodel.InstallViewModel.UserInfoCompat 26 | 27 | @Composable 28 | fun SelectUserItem( 29 | onDismiss: () -> Unit, 30 | user: UserInfoCompat, 31 | users: List, 32 | onChange: (UserInfoCompat) -> Unit 33 | ) = ModalBottomSheet( 34 | onDismissRequest = onDismiss, 35 | shape = MaterialTheme.shapes.large.bottom(0.dp) 36 | ) { 37 | Text( 38 | text = stringResource(id = R.string.install_select_user_title), 39 | style = MaterialTheme.typography.headlineSmall, 40 | modifier = Modifier.align(Alignment.CenterHorizontally) 41 | ) 42 | 43 | LazyColumn( 44 | modifier = Modifier.padding(all = 20.dp), 45 | verticalArrangement = Arrangement.spacedBy(20.dp) 46 | ) { 47 | items(users) { 48 | UserItem( 49 | name = it.name, 50 | selected = it.id == user.id, 51 | onClick = { 52 | onChange(it) 53 | onDismiss() 54 | } 55 | ) 56 | } 57 | } 58 | } 59 | 60 | @Composable 61 | fun UserItem( 62 | name: String, 63 | selected: Boolean, 64 | onClick: () -> Unit 65 | ) = Row( 66 | modifier = Modifier 67 | .surface( 68 | shape = MaterialTheme.shapes.medium, 69 | backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation( 70 | elevation = if (selected) 3.dp else 0.dp 71 | ), 72 | border = CardDefaults.outlinedCardBorder() 73 | ) 74 | .clickable(enabled = !selected, onClick = onClick) 75 | .padding(all = 15.dp) 76 | .fillMaxWidth(), 77 | verticalAlignment = Alignment.CenterVertically, 78 | horizontalArrangement = Arrangement.spacedBy(12.dp) 79 | ) { 80 | Icon( 81 | painter = painterResource(id = R.drawable.user), 82 | contentDescription = null 83 | ) 84 | 85 | Text( 86 | text = name, 87 | style = MaterialTheme.typography.labelLarge 88 | ) 89 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SplitConfigItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.install.component 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.OutlinedCard 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.derivedStateOf 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.text.style.TextDecoration 20 | import androidx.compose.ui.unit.dp 21 | import dev.sanmer.pi.R 22 | import dev.sanmer.pi.bundle.SplitConfig 23 | 24 | @Composable 25 | fun SplitConfigItem( 26 | config: SplitConfig, 27 | isRequiredConfig: (SplitConfig) -> Boolean, 28 | toggleSplitConfig: (SplitConfig) -> Unit, 29 | ) { 30 | val required by remember { 31 | derivedStateOf { isRequiredConfig(config) } 32 | } 33 | 34 | OutlinedCard( 35 | shape = MaterialTheme.shapes.medium, 36 | onClick = { toggleSplitConfig(config) }, 37 | enabled = !config.isDisabled 38 | ) { 39 | Row( 40 | modifier = Modifier 41 | .padding(vertical = 10.dp, horizontal = 15.dp) 42 | .fillMaxWidth(), 43 | verticalAlignment = Alignment.CenterVertically 44 | ) { 45 | ConfigIcon( 46 | config = config, 47 | enable = required 48 | ) 49 | 50 | Column( 51 | modifier = Modifier.padding(start = 10.dp) 52 | ) { 53 | Text( 54 | text = config.name, 55 | style = MaterialTheme.typography.bodyMedium, 56 | color = when { 57 | !required -> MaterialTheme.colorScheme.outline 58 | else -> Color.Unspecified 59 | } 60 | ) 61 | 62 | Text( 63 | text = buildString { 64 | if (config.isConfigForSplit) { 65 | append(config.configForSplit) 66 | append(", ") 67 | } 68 | 69 | append(config.displaySize) 70 | }, 71 | style = MaterialTheme.typography.bodySmall, 72 | textDecoration = when { 73 | !required -> TextDecoration.LineThrough 74 | else -> TextDecoration.None 75 | }, 76 | color = MaterialTheme.colorScheme.outline 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | 83 | @Composable 84 | private fun ConfigIcon( 85 | config: SplitConfig, 86 | enable: Boolean 87 | ) = Icon( 88 | painter = painterResource( 89 | id = when (config) { 90 | is SplitConfig.Feature -> R.drawable.box 91 | is SplitConfig.Target -> R.drawable.cpu 92 | is SplitConfig.Density -> R.drawable.photo 93 | is SplitConfig.Language -> R.drawable.language 94 | else -> R.drawable.code 95 | } 96 | ), 97 | contentDescription = null, 98 | tint = MaterialTheme.colorScheme.onSurfaceVariant 99 | .copy(alpha = if (enable) 1f else 0.3f) 100 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/TittleItem.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.screens.install.component 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | fun TittleItem( 9 | text: String, 10 | ) = Text( 11 | text = text, 12 | style = MaterialTheme.typography.titleMedium 13 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.theme 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.ui.graphics.Color 5 | 6 | val LightColorScheme = ColorScheme( 7 | primary = Color(0xFF000000), 8 | onPrimary = Color(0xFFE2E2E2), 9 | primaryContainer = Color(0xFF3B3B3B), 10 | onPrimaryContainer = Color(0xFFFFFFFF), 11 | inversePrimary = Color(0xFFFFFFFF), 12 | secondary = Color(0xFF5E5E5E), 13 | onSecondary = Color(0xFFFFFFFF), 14 | secondaryContainer = Color(0xFFD4D4D4), 15 | onSecondaryContainer = Color(0xFF1B1B1B), 16 | tertiary = Color(0xFF3B3B3B), 17 | onTertiary = Color(0xFFE2E2E2), 18 | tertiaryContainer = Color(0xFF747474), 19 | onTertiaryContainer = Color(0xFFFFFFFF), 20 | background = Color(0xFFF9F9F9), 21 | onBackground = Color(0xFF1B1B1B), 22 | surface = Color(0xFFF9F9F9), 23 | onSurface = Color(0xFF1B1B1B), 24 | surfaceVariant = Color(0xFFE2E2E2), 25 | onSurfaceVariant = Color(0xFF474747), 26 | surfaceTint = Color(0xFF000000), 27 | inverseSurface = Color(0xFF131313), 28 | inverseOnSurface = Color(0xFFE2E2E2), 29 | error = Color(0xFFB3261E), 30 | onError = Color(0xFFFFFFFF), 31 | errorContainer = Color(0xFFF9DEDC), 32 | onErrorContainer = Color(0xFF410E0B), 33 | outline = Color(0xFF747474), 34 | outlineVariant = Color(0xFFC6C6C6), 35 | scrim = Color(0xFF000000), 36 | surfaceBright = Color(0xFFF9F9F9), 37 | surfaceDim = Color(0xFFDADADA), 38 | surfaceContainer = Color(0xFFEEEEEE), 39 | surfaceContainerHigh = Color(0xFFE8E8E8), 40 | surfaceContainerHighest = Color(0xFFE2E2E2), 41 | surfaceContainerLow = Color(0xFFF3F3F3), 42 | surfaceContainerLowest = Color(0xFFFFFFFF) 43 | ) 44 | 45 | val DarkColorScheme = ColorScheme( 46 | primary = Color(0xFFFFFFFF), 47 | onPrimary = Color(0xFF1B1B1B), 48 | primaryContainer = Color(0xFFD4D4D4), 49 | onPrimaryContainer = Color(0xFF000000), 50 | inversePrimary = Color(0xFF000000), 51 | secondary = Color(0xFFC6C6C6), 52 | onSecondary = Color(0xFF1B1B1B), 53 | secondaryContainer = Color(0xFF474747), 54 | onSecondaryContainer = Color(0xFFE2E2E2), 55 | tertiary = Color(0xFFE2E2E2), 56 | onTertiary = Color(0xFF1B1B1B), 57 | tertiaryContainer = Color(0xFF919191), 58 | onTertiaryContainer = Color(0xFF000000), 59 | background = Color(0xFF131313), 60 | onBackground = Color(0xFFE2E2E2), 61 | surface = Color(0xFF131313), 62 | onSurface = Color(0xFFE2E2E2), 63 | surfaceVariant = Color(0xFF474747), 64 | onSurfaceVariant = Color(0xFFC6C6C6), 65 | surfaceTint = Color(0xFFFFFFFF), 66 | inverseSurface = Color(0xFFF9F9F9), 67 | inverseOnSurface = Color(0xFF1B1B1B), 68 | error = Color(0xFFF2B8B5), 69 | onError = Color(0xFF601410), 70 | errorContainer = Color(0xFF8C1D18), 71 | onErrorContainer = Color(0xFFF9DEDC), 72 | outline = Color(0xFF919191), 73 | outlineVariant = Color(0xFF474747), 74 | scrim = Color(0xFF000000), 75 | surfaceBright = Color(0xFF393939), 76 | surfaceDim = Color(0xFF131313), 77 | surfaceContainer = Color(0xFF1F1F1F), 78 | surfaceContainerHigh = Color(0xFF2A2A2A), 79 | surfaceContainerHighest = Color(0xFF353535), 80 | surfaceContainerLow = Color(0xFF1B1B1B), 81 | surfaceContainerLowest = Color(0xFF0E0E0E) 82 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.theme 2 | 3 | import androidx.compose.material3.Shapes 4 | 5 | val Shapes = Shapes() -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.theme 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.SystemBarStyle 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.material3.ColorScheme 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.dynamicDarkColorScheme 10 | import androidx.compose.material3.dynamicLightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.platform.LocalContext 17 | import dev.sanmer.pi.compat.BuildCompat 18 | 19 | @Composable 20 | fun AppTheme( 21 | darkMode: Boolean = isSystemInDarkTheme(), 22 | content: @Composable () -> Unit 23 | ) { 24 | SystemBarStyle( 25 | darkMode = darkMode 26 | ) 27 | 28 | MaterialTheme( 29 | colorScheme = colorScheme(darkMode), 30 | shapes = Shapes, 31 | typography = Typography, 32 | content = content 33 | ) 34 | } 35 | 36 | @Composable 37 | private fun colorScheme(darkMode: Boolean): ColorScheme { 38 | val context = LocalContext.current 39 | return if (BuildCompat.atLeastS) { 40 | when { 41 | darkMode -> dynamicDarkColorScheme(context) 42 | else -> dynamicLightColorScheme(context) 43 | } 44 | } else { 45 | when { 46 | darkMode -> DarkColorScheme 47 | else -> LightColorScheme 48 | } 49 | } 50 | } 51 | 52 | @Composable 53 | private fun SystemBarStyle( 54 | darkMode: Boolean, 55 | statusBarScrim: Color = Color.Transparent, 56 | navigationBarScrim: Color = Color.Transparent 57 | ) { 58 | val context = LocalContext.current 59 | val activity = remember { context as ComponentActivity } 60 | 61 | SideEffect { 62 | activity.enableEdgeToEdge( 63 | statusBarStyle = SystemBarStyle.auto( 64 | statusBarScrim.toArgb(), 65 | statusBarScrim.toArgb(), 66 | ) { darkMode }, 67 | navigationBarStyle = when { 68 | darkMode -> SystemBarStyle.dark( 69 | navigationBarScrim.toArgb() 70 | ) 71 | 72 | else -> SystemBarStyle.light( 73 | navigationBarScrim.toArgb(), 74 | navigationBarScrim.toArgb(), 75 | ) 76 | } 77 | ) 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/dev/sanmer/pi/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val Typography = Typography( 10 | titleLarge = TextStyle( 11 | fontFamily = FontFamily.SansSerif, 12 | fontWeight = FontWeight.Medium, 13 | fontSize = 22.sp, 14 | lineHeight = 28.sp, 15 | letterSpacing = 0.sp, 16 | ) 17 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/alert_triangle.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/box.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/brand_github.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/check.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chevron_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_check_filled.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/code.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/command.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cpu.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 60 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/language.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/launcher_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/launcher_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mood_neutral.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mood_wink.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mood_xd.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/photo.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/player_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/settings_2.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/user.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/user_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/world.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/x.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | تثبيت 4 | وضع العمل 5 | الخدمة قيد التشغيل 6 | الخدمة لا تعمل 7 | الإصدار %1$d, %2$s 8 | يتطلب إذنًا مقدمًا من Sui أو Shizuku 9 | مقدم طلب التثبيت 10 | قائمة فارغة 11 | الإعدادات 12 | المشاركة في الترجمة 13 | ساعدنا في ترجمة PI إلى لغتك 14 | حزمة التثبيت 15 | الميزة الديناميكية 16 | كثافة الشاشة 17 | اللغة 18 | غير محدد 19 | تم التثبيت بنجاح 20 | فشل التثبيت 21 | جاري التحميل… 22 | بحث… 23 | تثبيت الخدمة 24 | خطأ غير معروف 25 | ABI 26 | يتطلب صلاحيات الروت التي يوفرها Magisk أو KernelSU أو APatch 27 | إضغط لمحاولة البدء 28 | اللغة 29 | إفتراضي تبع النظام 30 | فشل تحليل الحزمة 31 | مخول 32 | الطالب 33 | المنفذ 34 | PI محدث 35 | انقر لفتح البرنامج 36 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Instalar 4 | Configuración 5 | Modo de trabajo 6 | Se requiere permiso de Sui o Shizuku 7 | Requiere permisos Root proporcionados por Magisk, KernelSU o Apatch 8 | El servicio se está ejecutando 9 | Versión %1$d, %2$s 10 | El servicio no está en funcionamiento 11 | Haga clic para intentar iniciar 12 | Idioma 13 | Sistema por defecto 14 | Participar en la traducción 15 | Ayúdenos a traducir PI a su idioma 16 | Paquete de instalación 17 | Solicitud de instalación 18 | Función dinámica 19 | Densidad de pantalla 20 | Idioma 21 | Sin especificar 22 | Error al analizar el paquete 23 | ABI 24 | Lista vacía 25 | Instalar servicio 26 | Error desconocido 27 | Instalación correcta 28 | Instalación fallida 29 | Cargando… 30 | Buscar… 31 | Autorización 32 | Solicitante 33 | Ejecutor 34 | PI Actualizado 35 | Toca para abrir la app 36 | Servicio de análisis 37 | Analizando 38 | -------------------------------------------------------------------------------- /app/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | نصب 4 | 5 | حالت کار 6 | نیاز به مجوز ارائه شده توسط Sui یا Shizuku دارد 7 | نیاز به مجوز روت ارائه شده توسط Magisk، KernelSU یا APatch دارد 8 | 9 | درخواست کننده 10 | اجرا کننده 11 | اعطای مجوز 12 | مجاز شده 13 | 14 | تنظیمات 15 | سرویس در حال راه‌اندازی است 16 | لطفاً صبر کنید 17 | سرویس در حال اجرا است 18 | نسخه %1$d، %2$s 19 | سرویس در حال اجرا نیست 20 | برای راه‌اندازی مجدد کلیک کنید 21 | زبان 22 | پیش‌فرض سیستم 23 | مشارکت در ترجمه 24 | به ما کمک کنید PI را به زبان خود ترجمه کنید 25 | 26 | بسته نصب 27 | درخواست کننده نصب 28 | قابلیت پویا 29 | ABI 30 | تراکم صفحه 31 | زبان 32 | مشخص نشده 33 | انتخاب کاربر 34 | 35 | سرویس تجزیه 36 | در حال تجزیه 37 | تجزیه ناموفق بود 38 | سرویس نصب 39 | نصب موفقیت‌آمیز بود 40 | نصب ناموفق بود 41 | در حال بهینه‌سازی 42 | در حال بارگذاری… 43 | خطای ناشناخته 44 | PI به‌روز شد 45 | برای باز کردن برنامه ضربه بزنید 46 | 47 | جستجو… 48 | لیست خالی است 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Installer 4 | Mode de travail 5 | Nécessite l\'autorisation fournie par Sui ou Shizuku 6 | Nécessite les permissions Root fournies par Magisk, KernelSU ou APatch 7 | Service est en cours d\'exécution 8 | Le service n\'est pas en cours d\'exécution 9 | Version %1$d, %2$s 10 | Service d\'installation 11 | Erreur inconnue 12 | Cliquer pour essayer de démarrer 13 | Language 14 | Système par défaut 15 | Échec de l\'analyse du paquet 16 | Paquet d\'installation 17 | Participer à la traduction 18 | Aidez nous a traduire PI dans vôtre langue 19 | Installation réussie 20 | Échec de l\'installation 21 | Chargement… 22 | Rechercher… 23 | Liste vide 24 | Demandeur d\'installation 25 | Fonctionnalité dynamique 26 | ABI 27 | Densité d\'écran 28 | Language 29 | Non précisé 30 | Paramètres 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Setelan 4 | Mode kerja 5 | Diotorisasi 6 | Eksekutor 7 | Layanan telah berjalan 8 | Versi %1$d, %2$s 9 | Klik untuk mencoba memulai 10 | Bahasa 11 | Bawaan sistem 12 | Berpartisipasi dalam penerjemahan 13 | Paket instalasi 14 | Pengaju Instalasi 15 | Fitur dinamis 16 | ABI 17 | Kepadatan layar 18 | Memerlukan izin dari Sui atau Shizuku 19 | Pengaju 20 | Bahasa 21 | Tidak spesifik 22 | Parsing paket gagal 23 | Pemasangan berhasil 24 | Pemasangan gagal 25 | Memuat… 26 | Cari… 27 | Daftar kosong 28 | Layanan pemasangan 29 | Kesalahan tidak diketahui 30 | Pasang 31 | Memerlukan izin Root dari Magisk, KernelSU, atau APatch 32 | Layanan tidak berjalan 33 | Bantu kami menerjemahkan PI ke dalam bahasa Anda 34 | Ketuk untuk membuka apl 35 | PI Diperbarui 36 | Penguraian 37 | Layanan penguraian 38 | -------------------------------------------------------------------------------- /app/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | שיטת העבודה 4 | התקנה 5 | הגדרות 6 | דורש הרשאות המסופקות על ידי Sui או Shizuku 7 | ארכיטקטורה 8 | פיצ\'ר דינמי 9 | חבילת התקנה 10 | מבקש ההתקנה 11 | צפיפות תצוגה 12 | יש ללחוץ על מנת לנסות להפעיל 13 | השתתפות בתרגום 14 | דורשת הרשאת Root שמסופקת על ידי Magisk, KernelSU או APatch 15 | השירות פועל 16 | גרסה %1$d, %2$s 17 | השירות לא פועל 18 | שפה 19 | ברירת מחדל של המערכת 20 | עזור לנו לתרגם את PI לשפה שלך 21 | שגיאה לא ידועה 22 | התקנת שירות 23 | רשימה ריקה 24 | חיפוש… 25 | נטען… 26 | ההתקנה נכשלה 27 | ההתקנה בוצעה בהצלחה 28 | לא מוגדר 29 | שפה 30 | ניתוח החבילה כשל 31 | מורשה 32 | מבצע 33 | מבקש 34 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_accent1_200 4 | @android:color/system_accent1_800 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FBFCFD 4 | #0A0C10 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 24 | 25 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | compileOnly(libs.android.gradle) 7 | compileOnly(libs.compose.gradle) 8 | compileOnly(libs.kotlin.gradle) 9 | compileOnly(libs.ksp.gradle) 10 | } 11 | 12 | gradlePlugin { 13 | plugins { 14 | register("self.application") { 15 | id = "self.application" 16 | implementationClass = "ApplicationConventionPlugin" 17 | } 18 | 19 | register("self.library") { 20 | id = "self.library" 21 | implementationClass = "LibraryConventionPlugin" 22 | } 23 | 24 | register("self.compose") { 25 | id = "self.compose" 26 | implementationClass = "ComposeConventionPlugin" 27 | } 28 | 29 | register("self.hilt") { 30 | id = "self.hilt" 31 | implementationClass = "HiltConventionPlugin" 32 | } 33 | 34 | register("self.room") { 35 | id = "self.room" 36 | implementationClass = "RoomConventionPlugin" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | versionCatalogs { 8 | create("libs") { 9 | from(files("../gradle/libs.versions.toml")) 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ApplicationConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import org.gradle.api.JavaVersion 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.JavaPluginExtension 6 | import org.gradle.jvm.toolchain.JavaLanguageVersion 7 | import org.gradle.kotlin.dsl.apply 8 | import org.gradle.kotlin.dsl.configure 9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension 10 | 11 | class ApplicationConventionPlugin : Plugin { 12 | override fun apply(target: Project) = with(target) { 13 | apply(plugin = "com.android.application") 14 | apply(plugin = "org.jetbrains.kotlin.android") 15 | 16 | extensions.configure { 17 | compileSdk = 35 18 | buildToolsVersion = "35.0.1" 19 | 20 | defaultConfig { 21 | minSdk = 30 22 | targetSdk = compileSdk 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_21 27 | targetCompatibility = JavaVersion.VERSION_21 28 | } 29 | } 30 | 31 | extensions.configure { 32 | toolchain { 33 | languageVersion.set(JavaLanguageVersion.of(21)) 34 | } 35 | } 36 | 37 | extensions.configure { 38 | jvmToolchain(21) 39 | 40 | sourceSets.all { 41 | languageSettings { 42 | optIn("kotlinx.serialization.ExperimentalSerializationApi") 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ComposeConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import org.gradle.api.Plugin 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.VersionCatalogsExtension 5 | import org.gradle.kotlin.dsl.apply 6 | import org.gradle.kotlin.dsl.configure 7 | import org.gradle.kotlin.dsl.dependencies 8 | import org.gradle.kotlin.dsl.getByType 9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension 10 | 11 | class ComposeConventionPlugin : Plugin { 12 | override fun apply(target: Project) = with(target) { 13 | apply(plugin = "com.android.application") 14 | apply(plugin = "org.jetbrains.kotlin.android") 15 | apply(plugin = "org.jetbrains.kotlin.plugin.compose") 16 | 17 | extensions.configure { 18 | buildFeatures { 19 | compose = true 20 | } 21 | } 22 | 23 | extensions.configure { 24 | sourceSets.all { 25 | languageSettings { 26 | optIn("androidx.compose.material3.ExperimentalMaterial3Api") 27 | optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi") 28 | } 29 | } 30 | } 31 | 32 | val libs = extensions.getByType().named("libs") 33 | dependencies { 34 | "implementation"(libs.findLibrary("androidx.compose.material3").get()) 35 | "implementation"(libs.findLibrary("androidx.compose.ui").get()) 36 | "implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get()) 37 | "debugImplementation"(libs.findLibrary("androidx.compose.ui.tooling").get()) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/HiltConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.artifacts.VersionCatalogsExtension 4 | import org.gradle.kotlin.dsl.apply 5 | import org.gradle.kotlin.dsl.dependencies 6 | import org.gradle.kotlin.dsl.getByType 7 | 8 | class HiltConventionPlugin : Plugin { 9 | override fun apply(target: Project) = with(target) { 10 | apply(plugin = "dagger.hilt.android.plugin") 11 | apply(plugin = "com.google.devtools.ksp") 12 | 13 | val libs = extensions.getByType().named("libs") 14 | dependencies { 15 | "implementation"(libs.findLibrary("hilt.android").get()) 16 | "ksp"(libs.findLibrary("hilt.compiler").get()) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/LibraryConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.LibraryExtension 2 | import org.gradle.api.JavaVersion 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.JavaPluginExtension 6 | import org.gradle.jvm.toolchain.JavaLanguageVersion 7 | import org.gradle.kotlin.dsl.apply 8 | import org.gradle.kotlin.dsl.configure 9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension 10 | 11 | class LibraryConventionPlugin : Plugin { 12 | override fun apply(target: Project) = with(target) { 13 | apply(plugin = "com.android.library") 14 | apply(plugin = "org.jetbrains.kotlin.android") 15 | 16 | extensions.configure { 17 | compileSdk = 35 18 | buildToolsVersion = "35.0.1" 19 | 20 | defaultConfig { 21 | minSdk = 30 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_21 26 | targetCompatibility = JavaVersion.VERSION_21 27 | } 28 | } 29 | 30 | extensions.configure { 31 | toolchain { 32 | languageVersion.set(JavaLanguageVersion.of(21)) 33 | } 34 | } 35 | 36 | extensions.configure { 37 | jvmToolchain(21) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ProjectExt.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | import org.gradle.kotlin.dsl.extra 3 | import java.io.File 4 | import java.util.Properties 5 | 6 | val Project.commitSha: String get() = exec("git rev-parse HEAD") 7 | val Project.commitCount: Int get() = exec("git rev-list --count HEAD").toInt() 8 | 9 | @Suppress("UnstableApiUsage") 10 | fun Project.exec(command: String) = providers.exec { 11 | commandLine(command.split(" ")) 12 | }.standardOutput.asText.get().trim() 13 | 14 | val Project.releaseKeyStore: File get() = File(extra["keyStore"] as String) 15 | val Project.releaseKeyStorePassword: String get() = extra["keyStorePassword"] as String 16 | val Project.releaseKeyAlias: String get() = extra["keyAlias"] as String 17 | val Project.releaseKeyPassword: String get() = extra["keyPassword"] as String 18 | val Project.hasReleaseKeyStore: Boolean get() { 19 | signingProperties(rootDir).forEach { key, value -> 20 | extra[key as String] = value 21 | } 22 | 23 | return extra.has("keyStore") 24 | } 25 | 26 | private fun signingProperties(rootDir: File): Properties { 27 | val properties = Properties() 28 | val signingProperties = rootDir.resolve("signing.properties") 29 | if (signingProperties.isFile) { 30 | signingProperties.inputStream().use(properties::load) 31 | } 32 | 33 | return properties 34 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/RoomConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.google.devtools.ksp.gradle.KspExtension 2 | import org.gradle.api.Plugin 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.VersionCatalogsExtension 5 | import org.gradle.kotlin.dsl.apply 6 | import org.gradle.kotlin.dsl.configure 7 | import org.gradle.kotlin.dsl.dependencies 8 | import org.gradle.kotlin.dsl.getByType 9 | 10 | class RoomConventionPlugin : Plugin { 11 | override fun apply(target: Project) = with(target) { 12 | apply(plugin = "com.google.devtools.ksp") 13 | 14 | extensions.configure { 15 | arg("room.incremental", "true") 16 | arg("room.expandProjection", "true") 17 | arg("room.schemaLocation", "$projectDir/schemas") 18 | } 19 | 20 | val libs = extensions.getByType().named("libs") 21 | dependencies { 22 | "implementation"(libs.findLibrary("androidx.room.ktx").get()) 23 | "implementation"(libs.findLibrary("androidx.room.runtime").get()) 24 | "ksp"(libs.findLibrary("androidx.room.compiler").get()) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.compose.compiler) apply false 5 | alias(libs.plugins.kotlin.jvm) apply false 6 | alias(libs.plugins.kotlin.parcelize) apply false 7 | alias(libs.plugins.kotlin.serialization) apply false 8 | alias(libs.plugins.hilt) apply false 9 | alias(libs.plugins.ksp) apply false 10 | } 11 | 12 | tasks.register("clean") { 13 | delete(layout.buildDirectory) 14 | } 15 | 16 | subprojects { 17 | val baseVersionName by extra("1.2.1") 18 | 19 | apply(plugin = "maven-publish") 20 | configure { 21 | publications { 22 | all { 23 | group = "dev.sanmer.pi" 24 | version = baseVersionName 25 | } 26 | } 27 | 28 | repositories { 29 | maven { 30 | name = "GitHubPackages" 31 | url = uri("https://maven.pkg.github.com/SanmerApps/PI") 32 | credentials { 33 | username = System.getenv("GITHUB_ACTOR") 34 | password = System.getenv("GITHUB_TOKEN") 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.self.library) 3 | alias(libs.plugins.rikka.refine) 4 | alias(libs.plugins.kotlin.parcelize) 5 | `maven-publish` 6 | } 7 | 8 | android { 9 | namespace = "dev.sanmer.pi.core" 10 | 11 | defaultConfig { 12 | consumerProguardFile("proguard-rules.pro") 13 | } 14 | 15 | publishing { 16 | singleVariant("release") { 17 | withSourcesJar() 18 | } 19 | } 20 | } 21 | 22 | publishing { 23 | publications { 24 | register("core") { 25 | artifactId = "core" 26 | 27 | afterEvaluate { 28 | from(components.getByName("release")) 29 | } 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | compileOnly(projects.stub) 36 | implementation(libs.rikka.refine.runtime) 37 | 38 | implementation(libs.androidx.annotation) 39 | implementation(libs.kotlinx.coroutines.android) 40 | } -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepclassmembers class dev.sanmer.pi.delegate.AppOpsManagerDelegate$AppOpsActiveCallbackDelegate { public ; } 2 | -keepclassmembers class dev.sanmer.pi.delegate.PackageInstallerDelegate$SessionCallbackDelegate { public ; } 3 | -keepclassmembers class dev.sanmer.pi.IntentReceiverCompat$IIntentSenderDelegate { public ; } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/BuildCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.os.Build 4 | import androidx.annotation.ChecksSdkIntAtLeast 5 | 6 | internal object BuildCompat { 7 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.VANILLA_ICE_CREAM) 8 | val atLeastV inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM 9 | 10 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 11 | val atLeastU inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 12 | 13 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) 14 | val atLeastT inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 15 | 16 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) 17 | val atLeastS inline get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 18 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/ContextCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.app.ActivityThread 4 | import android.content.Context 5 | import android.content.ContextHidden 6 | import android.content.ContextWrapper 7 | import dev.rikka.tools.refine.Refine 8 | 9 | object ContextCompat { 10 | val Context.userId 11 | get() = Refine.unsafeCast(this).userId 12 | 13 | internal fun getContext(): Context { 14 | var context: Context = ActivityThread.currentApplication() 15 | while (context is ContextWrapper) { 16 | context = context.baseContext 17 | } 18 | 19 | return context 20 | } 21 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/IntentReceiverCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.content.IIntentReceiver 4 | import android.content.IIntentSender 5 | import android.content.Intent 6 | import android.content.IntentSender 7 | import android.content.IntentSenderHidden 8 | import android.os.Bundle 9 | import android.os.IBinder 10 | import dev.rikka.tools.refine.Refine 11 | import kotlinx.coroutines.suspendCancellableCoroutine 12 | import kotlin.coroutines.resume 13 | 14 | object IntentReceiverCompat { 15 | class IIntentSenderDelegate( 16 | private val onSend: (Intent) -> Unit 17 | ) : IIntentSender.Stub() { 18 | override fun send( 19 | code: Int, 20 | intent: Intent, 21 | resolvedType: String?, 22 | whitelistToken: IBinder?, 23 | finishedReceiver: IIntentReceiver?, 24 | requiredPermission: String?, 25 | options: Bundle? 26 | ) { 27 | onSend(intent) 28 | } 29 | } 30 | 31 | fun delegate(onSend: (Intent) -> Unit): IntentSender { 32 | val original = IIntentSenderDelegate(onSend) 33 | return Refine.unsafeCast(IntentSenderHidden(original)) 34 | } 35 | 36 | suspend fun onDelegate( 37 | register: (IntentSender) -> Unit 38 | ) = suspendCancellableCoroutine { continuation -> 39 | register( 40 | delegate { continuation.resume(it) } 41 | ) 42 | } 43 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/PackageInfoCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageInfoHidden 6 | import android.os.Build 7 | import androidx.annotation.RequiresApi 8 | import dev.rikka.tools.refine.Refine 9 | 10 | object PackageInfoCompat { 11 | private inline val PackageInfo.original 12 | get() = Refine.unsafeCast(this) 13 | 14 | var PackageInfo.versionCodeMajor: Int 15 | get() = original.versionCodeMajor 16 | set(v) { original.versionCodeMajor = v } 17 | 18 | var PackageInfo.isStub: Boolean 19 | get() = original.isStub 20 | set(v) { original.isStub = v } 21 | 22 | var PackageInfo.coreApp: Boolean 23 | get() = original.coreApp 24 | set(v) { original.coreApp = v } 25 | 26 | var PackageInfo.requiredForAllUsers: Boolean 27 | get() = original.requiredForAllUsers 28 | set(v) { original.requiredForAllUsers = v } 29 | 30 | var PackageInfo.restrictedAccountType: String? 31 | get() = original.restrictedAccountType 32 | set(v) { original.restrictedAccountType = v } 33 | 34 | var PackageInfo.requiredAccountType: String? 35 | get() = original.requiredAccountType 36 | set(v) { original.requiredAccountType = v } 37 | 38 | var PackageInfo.overlayTarget: String? 39 | get() = original.overlayTarget 40 | set(v) { original.overlayTarget = v } 41 | 42 | var PackageInfo.overlayCategory: String? 43 | get() = original.overlayCategory 44 | set(v) { original.overlayCategory = v } 45 | 46 | var PackageInfo.overlayPriority: Int 47 | get() = original.overlayPriority 48 | set(v) { original.overlayPriority = v } 49 | 50 | var PackageInfo.compileSdkVersion: Int 51 | get() = original.compileSdkVersion 52 | set(v) { original.compileSdkVersion = v } 53 | 54 | var PackageInfo.compileSdkVersionCodename: String? 55 | get() = original.compileSdkVersionCodename 56 | set(v) { original.compileSdkVersionCodename = v } 57 | 58 | var PackageInfo.isActiveApex: Boolean 59 | @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 60 | get() = original.isActiveApex 61 | @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 62 | set(v) { original.isActiveApex = v } 63 | 64 | val PackageInfo.isOverlayPackage 65 | get() = original.isOverlayPackage 66 | 67 | val PackageInfo?.isEmpty 68 | get() = this?.packageName == null || applicationInfo == null 69 | 70 | val PackageInfo?.isNotEmpty 71 | get() = !isEmpty 72 | 73 | val PackageInfo.isSystemApp: Boolean 74 | get() = applicationInfo?.let { 75 | it.flags and (ApplicationInfo.FLAG_SYSTEM or 76 | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 77 | } ?: false 78 | 79 | fun PackageInfo?.orEmpty() = this ?: PackageInfo() 80 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/SessionInfoCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.content.pm.PackageInstaller.SessionInfo 4 | import android.content.pm.PackageInstallerHidden 5 | import dev.rikka.tools.refine.Refine 6 | 7 | object SessionInfoCompat { 8 | val SessionInfo.userId 9 | get() = Refine.unsafeCast(this).userId 10 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/UserHandleCompat.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi 2 | 3 | import android.os.UserHandleHidden 4 | 5 | object UserHandleCompat { 6 | fun myUserId() = UserHandleHidden.myUserId() 7 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/bundle/ABI.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.bundle 2 | 3 | import android.os.Build 4 | 5 | enum class ABI(val value: String) { 6 | ARM64_V8A("arm64-v8a"), 7 | ARMEABI_V7A("armeabi-v7a"), 8 | ARMEABI("armeabi"), 9 | X86("x86"), 10 | X86_64("x86_64"); 11 | 12 | val displayName: String 13 | inline get() = value 14 | 15 | val isRequired: Boolean 16 | inline get() = value == Build.SUPPORTED_ABIS[0] 17 | 18 | val isEnabled: Boolean 19 | inline get() = value in Build.SUPPORTED_ABIS 20 | 21 | companion object Default { 22 | fun valueOfOrNull(value: String): ABI? = try { 23 | valueOf(value) 24 | } catch (_: IllegalArgumentException) { 25 | null 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/bundle/BundleInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.bundle 2 | 3 | import android.content.pm.PackageInfo 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | import java.io.File 7 | 8 | @Parcelize 9 | data class BundleInfo( 10 | val baseFile: File, 11 | val baseInfo: PackageInfo, 12 | val splitConfigs: List 13 | ) : Parcelable -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/bundle/DPI.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.bundle 2 | 3 | import android.util.DisplayMetrics 4 | import dev.sanmer.pi.ContextCompat 5 | 6 | enum class DPI(val value: Int) { 7 | LDPI(DisplayMetrics.DENSITY_LOW), 8 | MDPI(DisplayMetrics.DENSITY_MEDIUM), 9 | TVDPI(DisplayMetrics.DENSITY_TV), 10 | HDPI(DisplayMetrics.DENSITY_HIGH), 11 | XHDPI(DisplayMetrics.DENSITY_XHIGH), 12 | XXHDPI(DisplayMetrics.DENSITY_XXHIGH), 13 | XXXHDPI(DisplayMetrics.DENSITY_XXXHIGH); 14 | 15 | val displayName: String 16 | inline get() = "${value}.dpi" 17 | 18 | val isRequired: Boolean 19 | get() { 20 | val context = ContextCompat.getContext() 21 | val densityDpi = context.resources.displayMetrics.densityDpi 22 | return this == densityDpi.asDPI() 23 | } 24 | 25 | companion object Default { 26 | private fun Int.asDPI() = when { 27 | this <= DisplayMetrics.DENSITY_LOW -> LDPI 28 | this <= DisplayMetrics.DENSITY_MEDIUM -> MDPI 29 | this <= DisplayMetrics.DENSITY_TV -> TVDPI 30 | this <= DisplayMetrics.DENSITY_HIGH -> HDPI 31 | this <= DisplayMetrics.DENSITY_XHIGH -> XHDPI 32 | this <= DisplayMetrics.DENSITY_XXHIGH -> XXHDPI 33 | else -> XXXHDPI 34 | } 35 | 36 | fun valueOfOrNull(value: String): DPI? = try { 37 | valueOf(value) 38 | } catch (_: IllegalArgumentException) { 39 | null 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInfoDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.delegate 2 | 3 | import android.content.pm.PackageInfo 4 | import dev.sanmer.pi.BuildCompat 5 | import dev.sanmer.pi.ContextCompat 6 | import dev.sanmer.pi.PackageInfoCompat.compileSdkVersion 7 | import dev.sanmer.pi.PackageInfoCompat.compileSdkVersionCodename 8 | import dev.sanmer.pi.PackageInfoCompat.coreApp 9 | import dev.sanmer.pi.PackageInfoCompat.isActiveApex 10 | import dev.sanmer.pi.PackageInfoCompat.isStub 11 | import dev.sanmer.pi.PackageInfoCompat.overlayCategory 12 | import dev.sanmer.pi.PackageInfoCompat.overlayPriority 13 | import dev.sanmer.pi.PackageInfoCompat.overlayTarget 14 | import dev.sanmer.pi.PackageInfoCompat.requiredAccountType 15 | import dev.sanmer.pi.PackageInfoCompat.requiredForAllUsers 16 | import dev.sanmer.pi.PackageInfoCompat.restrictedAccountType 17 | import dev.sanmer.pi.PackageInfoCompat.versionCodeMajor 18 | 19 | @Suppress("DEPRECATION") 20 | abstract class PackageInfoDelegate( 21 | original: PackageInfo 22 | ) : PackageInfo() { 23 | init { 24 | packageName = original.packageName 25 | splitNames = original.splitNames 26 | versionCode = original.versionCode 27 | versionCodeMajor = original.versionCodeMajor 28 | versionName = original.versionName 29 | baseRevisionCode = original.baseRevisionCode 30 | splitRevisionCodes = original.splitRevisionCodes 31 | sharedUserId = original.sharedUserId 32 | sharedUserLabel = original.sharedUserLabel 33 | applicationInfo = original.applicationInfo 34 | firstInstallTime = original.firstInstallTime 35 | lastUpdateTime = original.lastUpdateTime 36 | gids = original.gids 37 | activities = original.activities 38 | receivers = original.receivers 39 | services = original.services 40 | providers = original.providers 41 | instrumentation = original.instrumentation 42 | permissions = original.permissions 43 | requestedPermissions = original.requestedPermissions 44 | requestedPermissionsFlags = original.requestedPermissionsFlags 45 | signatures = original.signatures 46 | configPreferences = original.configPreferences 47 | reqFeatures = original.reqFeatures 48 | featureGroups = original.featureGroups 49 | if (BuildCompat.atLeastS) { 50 | attributions = original.attributions 51 | } 52 | installLocation = original.installLocation 53 | isStub = original.isStub 54 | coreApp = original.coreApp 55 | requiredForAllUsers = original.requiredForAllUsers 56 | restrictedAccountType = original.restrictedAccountType 57 | requiredAccountType = original.requiredAccountType 58 | overlayTarget = original.overlayTarget 59 | overlayCategory = original.overlayCategory 60 | overlayPriority = original.overlayPriority 61 | compileSdkVersion = original.compileSdkVersion 62 | compileSdkVersionCodename = original.compileSdkVersionCodename 63 | signingInfo = original.signingInfo 64 | isApex = original.isApex 65 | if (BuildCompat.atLeastU) { 66 | isActiveApex = original.isActiveApex 67 | } 68 | } 69 | 70 | val appLabel by lazy { 71 | val context = ContextCompat.getContext() 72 | applicationInfo?.loadLabel( 73 | context.packageManager 74 | ).toString() 75 | } 76 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/delegate/PermissionManagerDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.delegate 2 | 3 | import android.companion.virtual.VirtualDeviceManagerHidden 4 | import android.content.pm.IPackageManager 5 | import android.os.IBinder 6 | import android.os.ServiceManager 7 | import android.permission.IPermissionManager 8 | import dev.sanmer.pi.BuildCompat 9 | 10 | class PermissionManagerDelegate( 11 | private val proxy: IBinder.() -> IBinder = { this } 12 | ) { 13 | private val packageManager by lazy { 14 | IPackageManager.Stub.asInterface( 15 | ServiceManager.getService("package").proxy() 16 | ) 17 | } 18 | 19 | private val permissionManager by lazy { 20 | IPermissionManager.Stub.asInterface( 21 | ServiceManager.getService("permissionmgr").proxy() 22 | ) 23 | } 24 | 25 | fun grantRuntimePermission(packageName: String, permissionName: String, userId: Int) { 26 | when { 27 | BuildCompat.atLeastV -> permissionManager.grantRuntimePermission( 28 | packageName, 29 | permissionName, 30 | VirtualDeviceManagerHidden.PERSISTENT_DEVICE_ID_DEFAULT, 31 | userId 32 | ) 33 | 34 | else -> permissionManager.grantRuntimePermission( 35 | packageName, 36 | permissionName, 37 | userId 38 | ) 39 | } 40 | } 41 | 42 | fun revokeRuntimePermission(packageName: String, permissionName: String, userId: Int) { 43 | when { 44 | BuildCompat.atLeastV -> permissionManager.revokeRuntimePermission( 45 | packageName, 46 | permissionName, 47 | VirtualDeviceManagerHidden.PERSISTENT_DEVICE_ID_DEFAULT, 48 | userId, 49 | null 50 | ) 51 | 52 | else -> permissionManager.revokeRuntimePermission( 53 | packageName, 54 | permissionName, 55 | userId, 56 | null 57 | ) 58 | } 59 | } 60 | 61 | fun checkPermission(packageName: String, permissionName: String, userId: Int): Int { 62 | return when { 63 | BuildCompat.atLeastV -> permissionManager.checkPermission( 64 | packageName, 65 | permissionName, 66 | VirtualDeviceManagerHidden.PERSISTENT_DEVICE_ID_DEFAULT, 67 | userId 68 | ) 69 | 70 | else -> packageManager.checkPermission( 71 | packageName, 72 | permissionName, 73 | userId 74 | ) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/dev/sanmer/pi/delegate/UserManagerDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.sanmer.pi.delegate 2 | 3 | import android.content.Context 4 | import android.content.pm.UserInfo 5 | import android.os.IBinder 6 | import android.os.IUserManager 7 | import android.os.ServiceManager 8 | 9 | class UserManagerDelegate( 10 | private val proxy: IBinder.() -> IBinder = { this } 11 | ) { 12 | private val userManager by lazy { 13 | IUserManager.Stub.asInterface( 14 | ServiceManager.getService(Context.USER_SERVICE).proxy() 15 | ) 16 | } 17 | 18 | fun getUsers(): List { 19 | return userManager.getUsers(true, true, true) 20 | } 21 | 22 | fun getUserInfo(userId: Int): UserInfo { 23 | return userManager.getUserInfo(userId) 24 | } 25 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | org.gradle.caching=true 3 | org.gradle.configuration-cache=true 4 | 5 | android.useAndroidX=true 6 | android.nonTransitiveRClass=true 7 | 8 | kapt.include.compile.classpath=false 9 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SanmerApps/PI/9798573bbef8a1a87de6ea1b995eb3e8f1506531/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | dependencyResolutionManagement { 5 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS 6 | repositories { 7 | google() 8 | mavenCentral() 9 | maven("https://jitpack.io") 10 | 11 | mavenLocal { 12 | content { 13 | includeGroup("dev.sanmer.su") 14 | } 15 | } 16 | } 17 | } 18 | 19 | pluginManagement { 20 | includeBuild("build-logic") 21 | repositories { 22 | google() 23 | mavenCentral() 24 | gradlePluginPortal() 25 | } 26 | } 27 | 28 | rootProject.name = "PI" 29 | include(":stub", ":core", ":app") 30 | -------------------------------------------------------------------------------- /stub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.self.library) 3 | `maven-publish` 4 | } 5 | 6 | android { 7 | namespace = "dev.sanmer.pi.stub" 8 | 9 | publishing { 10 | singleVariant("release") { 11 | withSourcesJar() 12 | } 13 | } 14 | } 15 | 16 | publishing { 17 | publications { 18 | register("stub") { 19 | artifactId = "stub" 20 | 21 | afterEvaluate { 22 | from(components.getByName("release")) 23 | } 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | annotationProcessor(libs.rikka.refine.compiler) 30 | compileOnly(libs.rikka.refine.annotation) 31 | compileOnly(libs.androidx.annotation) 32 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class ActivityThread { 4 | public static Application currentApplication() { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/android/companion/virtual/VirtualDeviceManagerHidden.java: -------------------------------------------------------------------------------- 1 | package android.companion.virtual; 2 | 3 | import dev.rikka.tools.refine.RefineAs; 4 | 5 | @RefineAs(VirtualDeviceManager.class) 6 | public class VirtualDeviceManagerHidden { 7 | public static String PERSISTENT_DEVICE_ID_DEFAULT; 8 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/ContextHidden.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import dev.rikka.tools.refine.RefineAs; 4 | 5 | @RefineAs(Context.class) 6 | public class ContextHidden { 7 | public int getUserId() { 8 | throw new RuntimeException("Stub!"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/IIntentReceiver.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | public interface IIntentReceiver { 4 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/IIntentSender.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Binder; 4 | import android.os.Bundle; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | 8 | public interface IIntentSender extends IInterface { 9 | 10 | void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, 11 | IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); 12 | 13 | abstract class Stub extends Binder implements IIntentSender { 14 | 15 | @Override 16 | public IBinder asBinder() { 17 | throw new RuntimeException("Stub!"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/IntentSenderHidden.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import dev.rikka.tools.refine.RefineAs; 4 | 5 | @RefineAs(IntentSender.class) 6 | public class IntentSenderHidden { 7 | public IntentSenderHidden(IIntentSender target) { 8 | throw new RuntimeException("Stub!"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/IPackageInstaller.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.IntentSender; 4 | import android.os.Binder; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.os.RemoteException; 8 | 9 | import androidx.annotation.RequiresApi; 10 | 11 | public interface IPackageInstaller extends IInterface { 12 | 13 | int createSession(PackageInstaller.SessionParams params, String installerPackageName, int userId) throws RemoteException; 14 | 15 | @RequiresApi(31) 16 | int createSession(PackageInstaller.SessionParams params, String installerPackageName, String installerAttributionTag, int userId); 17 | 18 | IPackageInstallerSession openSession(int sessionId) throws RemoteException; 19 | 20 | PackageInstaller.SessionInfo getSessionInfo(int sessionId); 21 | 22 | ParceledListSlice getAllSessions(int userId); 23 | 24 | void registerCallback(IPackageInstallerCallback callback, int userId) throws RemoteException; 25 | 26 | void unregisterCallback(IPackageInstallerCallback callback) throws RemoteException; 27 | 28 | void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags, IntentSender statusReceiver, int userId) throws RemoteException; 29 | 30 | abstract class Stub extends Binder implements IPackageInstaller { 31 | 32 | public static IPackageInstaller asInterface(IBinder obj) { 33 | throw new RuntimeException("Stub!"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/IPackageInstallerCallback.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | public interface IPackageInstallerCallback extends IInterface { 9 | 10 | void onSessionCreated(int sessionId) throws RemoteException; 11 | 12 | void onSessionBadgingChanged(int sessionId) throws RemoteException; 13 | 14 | void onSessionActiveChanged(int sessionId, boolean active) throws RemoteException; 15 | 16 | void onSessionProgressChanged(int sessionId, float progress) throws RemoteException; 17 | 18 | void onSessionFinished(int sessionId, boolean success) throws RemoteException; 19 | 20 | abstract class Stub extends Binder implements IPackageInstallerCallback { 21 | 22 | public static IPackageInstallerCallback asInterface(IBinder binder) { 23 | throw new RuntimeException("Stub!"); 24 | } 25 | 26 | @Override 27 | public IBinder asBinder() { 28 | throw new RuntimeException("Stub!"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/IPackageInstallerSession.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.IntentSender; 4 | import android.os.Binder; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.os.ParcelFileDescriptor; 8 | import android.os.RemoteException; 9 | 10 | public interface IPackageInstallerSession extends IInterface { 11 | 12 | ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) throws RemoteException; 13 | 14 | ParcelFileDescriptor openRead(String name) throws RemoteException; 15 | 16 | void write(String name, long offsetBytes, long lengthBytes, ParcelFileDescriptor fd) throws RemoteException; 17 | 18 | void close() throws RemoteException; 19 | 20 | void commit(IntentSender statusReceiver, boolean forTransferred) throws RemoteException; 21 | 22 | void abandon() throws RemoteException; 23 | 24 | abstract class Stub extends Binder implements IPackageInstallerSession { 25 | 26 | public static IPackageInstallerSession asInterface(IBinder obj) { 27 | throw new RuntimeException("Stub!"); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/IPackageManager.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.Intent; 4 | import android.os.Binder; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.os.RemoteException; 8 | 9 | import androidx.annotation.RequiresApi; 10 | 11 | public interface IPackageManager extends IInterface { 12 | 13 | IPackageInstaller getPackageInstaller() throws RemoteException; 14 | 15 | ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException; 16 | 17 | @RequiresApi(33) 18 | ApplicationInfo getApplicationInfo(String packageName, long flags, int userId) throws RemoteException; 19 | 20 | PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException; 21 | 22 | @RequiresApi(33) 23 | PackageInfo getPackageInfo(String packageName, long flags, int userId) throws RemoteException; 24 | 25 | int getPackageUid(String packageName, int flags, int userId) throws RemoteException; 26 | 27 | @RequiresApi(33) 28 | int getPackageUid(String packageName, long flags, int userId) throws RemoteException; 29 | 30 | ParceledListSlice getInstalledPackages(int flags, int userId) throws RemoteException; 31 | 32 | @RequiresApi(33) 33 | ParceledListSlice getInstalledPackages(long flags, int userId) throws RemoteException; 34 | 35 | ParceledListSlice getInstalledApplications(int flags, int userId) throws RemoteException; 36 | 37 | @RequiresApi(33) 38 | ParceledListSlice getInstalledApplications(long flags, int userId) throws RemoteException; 39 | 40 | ParceledListSlice queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) throws RemoteException; 41 | 42 | @RequiresApi(33) 43 | ParceledListSlice queryIntentActivities(Intent intent, String resolvedType, long flags, int userId) throws RemoteException; 44 | 45 | String[] getPackagesForUid(int uid) throws RemoteException; 46 | 47 | int checkPermission(String permName, String pkgName, int userId) throws RemoteException; 48 | 49 | boolean performDexOptMode(String packageName, boolean checkProfiles, String targetCompilerFilter, boolean force, boolean bootComplete, String splitName) throws RemoteException; 50 | 51 | void clearApplicationProfileData(String packageName) throws RemoteException; 52 | 53 | abstract class Stub extends Binder implements IPackageManager { 54 | 55 | public static IPackageManager asInterface(IBinder obj) { 56 | throw new RuntimeException("Stub!"); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/PackageInfoHidden.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import androidx.annotation.RequiresApi; 4 | 5 | import dev.rikka.tools.refine.RefineAs; 6 | 7 | @RefineAs(PackageInfo.class) 8 | public class PackageInfoHidden { 9 | 10 | public int versionCodeMajor; 11 | 12 | public boolean isStub; 13 | 14 | public boolean coreApp; 15 | 16 | public boolean requiredForAllUsers; 17 | 18 | public String restrictedAccountType; 19 | 20 | public String requiredAccountType; 21 | 22 | public String overlayTarget; 23 | 24 | public String overlayCategory; 25 | 26 | public int overlayPriority; 27 | 28 | public int compileSdkVersion; 29 | 30 | public String compileSdkVersionCodename; 31 | 32 | @RequiresApi(34) 33 | public boolean isActiveApex; 34 | 35 | public boolean isOverlayPackage() { 36 | throw new RuntimeException("Stub!"); 37 | } 38 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/PackageInstallerHidden.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import dev.rikka.tools.refine.RefineAs; 4 | 5 | @RefineAs(PackageInstaller.class) 6 | public class PackageInstallerHidden { 7 | 8 | @RefineAs(PackageInstaller.SessionParams.class) 9 | public static class SessionParamsHidden { 10 | public int installFlags; 11 | } 12 | 13 | @RefineAs(PackageInstaller.SessionInfo.class) 14 | public static class SessionInfoHidden { 15 | public int userId; 16 | } 17 | 18 | @RefineAs(PackageInstaller.Session.class) 19 | public static class SessionHidden { 20 | public SessionHidden(IPackageInstallerSession session) { 21 | throw new RuntimeException("Stub!"); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/PackageManagerHidden.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import androidx.annotation.RequiresApi; 4 | 5 | import dev.rikka.tools.refine.RefineAs; 6 | 7 | @RefineAs(PackageManager.class) 8 | public class PackageManagerHidden { 9 | 10 | public static int INSTALL_REPLACE_EXISTING; 11 | 12 | public static int INSTALL_ALLOW_TEST; 13 | 14 | public static int INSTALL_REQUEST_DOWNGRADE; 15 | 16 | @RequiresApi(34) 17 | public static int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; 18 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/PackageParser.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.pm.pkg.FrameworkPackageUserState; 4 | 5 | import androidx.annotation.RequiresApi; 6 | 7 | import java.io.File; 8 | import java.util.Set; 9 | 10 | public class PackageParser { 11 | public PackageParser() { 12 | throw new RuntimeException("Stub!"); 13 | } 14 | 15 | public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { 16 | throw new RuntimeException("Stub!"); 17 | } 18 | 19 | public static ApkLite parseApkLite(File apkFile, int flags) throws PackageParserException { 20 | throw new RuntimeException("Stub!"); 21 | } 22 | 23 | @RequiresApi(33) 24 | public static PackageInfo generatePackageInfo(PackageParser.Package p, 25 | int[] gids, int flags, long firstInstallTime, long lastUpdateTime, 26 | Set grantedPermissions, FrameworkPackageUserState state) { 27 | throw new RuntimeException("Stub!"); 28 | } 29 | 30 | public static PackageInfo generatePackageInfo(PackageParser.Package p, 31 | int[] gids, int flags, long firstInstallTime, long lastUpdateTime, 32 | Set grantedPermissions, PackageUserState state) { 33 | throw new RuntimeException("Stub!"); 34 | } 35 | 36 | public static class PackageParserException extends Exception {} 37 | 38 | public static class Package { 39 | public String packageName; 40 | } 41 | 42 | public static class ApkLite { 43 | public String packageName; 44 | public String splitName; 45 | public boolean isFeatureSplit; 46 | public String configForSplit; 47 | public String usesSplitName; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/PackageUserState.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public class PackageUserState { 4 | public PackageUserState() { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/ParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.util.List; 9 | 10 | public class ParceledListSlice implements Parcelable { 11 | public ParceledListSlice(List list) { 12 | throw new RuntimeException("Stub!"); 13 | } 14 | 15 | public List getList() { 16 | throw new RuntimeException("Stub!"); 17 | } 18 | 19 | @Override 20 | public void writeToParcel(@NonNull Parcel parcel, int i) { 21 | throw new RuntimeException("Stub!"); 22 | } 23 | 24 | @Override 25 | public int describeContents() { 26 | throw new RuntimeException("Stub!"); 27 | } 28 | 29 | @SuppressWarnings("rawtypes") 30 | public static final Parcelable.ClassLoaderCreator CREATOR = new Parcelable.ClassLoaderCreator<>() { 31 | public ParceledListSlice createFromParcel(Parcel in) { 32 | throw new RuntimeException("Stub!"); 33 | } 34 | 35 | @Override 36 | public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { 37 | throw new RuntimeException("Stub!"); 38 | } 39 | 40 | @Override 41 | public ParceledListSlice[] newArray(int size) { 42 | throw new RuntimeException("Stub!"); 43 | } 44 | }; 45 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/UserInfo.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.os.UserHandle; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | public class UserInfo implements Parcelable { 10 | public int id; 11 | public String name; 12 | 13 | public boolean isPrimary() { 14 | throw new RuntimeException("Stub!"); 15 | } 16 | 17 | public boolean isAdmin() { 18 | throw new RuntimeException("Stub!"); 19 | } 20 | 21 | public boolean isEnabled() { 22 | throw new RuntimeException("Stub!"); 23 | } 24 | 25 | public UserHandle getUserHandle() { 26 | throw new RuntimeException("Stub!"); 27 | } 28 | 29 | @Override 30 | public int describeContents() { 31 | throw new RuntimeException("Stub!"); 32 | } 33 | 34 | @Override 35 | public void writeToParcel(@NonNull Parcel dest, int flags) { 36 | throw new RuntimeException("Stub!"); 37 | } 38 | 39 | public static final Creator CREATOR = new Creator<>() { 40 | @Override 41 | public UserInfo createFromParcel(Parcel in) { 42 | throw new RuntimeException("Stub!"); 43 | } 44 | 45 | @Override 46 | public UserInfo[] newArray(int size) { 47 | throw new RuntimeException("Stub!"); 48 | } 49 | }; 50 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserState.java: -------------------------------------------------------------------------------- 1 | package android.content.pm.pkg; 2 | 3 | import androidx.annotation.RequiresApi; 4 | 5 | @RequiresApi(33) 6 | public interface FrameworkPackageUserState { 7 | FrameworkPackageUserState DEFAULT = new FrameworkPackageUserStateDefault(); 8 | } 9 | -------------------------------------------------------------------------------- /stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserStateDefault.java: -------------------------------------------------------------------------------- 1 | package android.content.pm.pkg; 2 | 3 | import androidx.annotation.RequiresApi; 4 | 5 | @RequiresApi(33) 6 | public class FrameworkPackageUserStateDefault implements FrameworkPackageUserState { 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/android/os/IUserManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | import android.content.pm.UserInfo; 4 | 5 | import java.util.List; 6 | 7 | public interface IUserManager extends IInterface { 8 | 9 | List getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException; 10 | 11 | UserInfo getUserInfo(int userId); 12 | 13 | abstract class Stub extends Binder implements IUserManager { 14 | 15 | public static IUserManager asInterface(IBinder obj) { 16 | throw new RuntimeException("Stub!"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class ServiceManager { 4 | public static IBinder getService(String name) { 5 | throw new RuntimeException("Stub!"); 6 | } 7 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/os/SystemProperties.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | public class SystemProperties { 7 | public static String get(@NonNull String key) { 8 | throw new UnsupportedOperationException("Stub!"); 9 | } 10 | 11 | public static String get(@NonNull String key, @Nullable String def) { 12 | throw new UnsupportedOperationException("Stub!"); 13 | } 14 | 15 | public static void set(@NonNull String key, @Nullable String val) { 16 | throw new UnsupportedOperationException("Stub!"); 17 | } 18 | 19 | public static boolean getBoolean(@NonNull String key, boolean def) { 20 | throw new UnsupportedOperationException("Stub!"); 21 | } 22 | 23 | public static int getInt(@NonNull String key, int def) { 24 | throw new UnsupportedOperationException("Stub!"); 25 | } 26 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/os/UserHandleHidden.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | import dev.rikka.tools.refine.RefineAs; 4 | 5 | @RefineAs(UserHandle.class) 6 | public class UserHandleHidden { 7 | public static int myUserId() { 8 | throw new RuntimeException("Stub!"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stub/src/main/java/android/permission/IPermissionManager.java: -------------------------------------------------------------------------------- 1 | package android.permission; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | public interface IPermissionManager extends IInterface { 11 | 12 | void grantRuntimePermission(String packageName, String permissionName, int userId) throws RemoteException; 13 | 14 | @RequiresApi(35) 15 | void grantRuntimePermission(String packageName, String permissionName, String persistentDeviceId, int userId) throws RemoteException; 16 | 17 | void revokeRuntimePermission(String packageName, String permissionName, int userId, String reason) throws RemoteException; 18 | 19 | @RequiresApi(35) 20 | void revokeRuntimePermission(String packageName, String permissionName, String persistentDeviceId, int userId, String reason) throws RemoteException; 21 | 22 | @RequiresApi(35) 23 | int checkPermission(String packageName, String permissionName, String persistentDeviceId, int userId) throws RemoteException; 24 | 25 | abstract class Stub extends Binder implements IPermissionManager { 26 | public static IPermissionManager asInterface(IBinder obj) { 27 | throw new RuntimeException("Stub!"); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /stub/src/main/java/com/android/internal/app/IAppOpsCallback.java: -------------------------------------------------------------------------------- 1 | package com.android.internal.app; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | public interface IAppOpsCallback extends IInterface { 11 | 12 | void opChanged(int op, int uid, String packageName) throws RemoteException; 13 | 14 | @RequiresApi(35) 15 | void opChanged(int op, int uid, String packageName, String persistentDeviceId) throws RemoteException; 16 | 17 | abstract class Stub extends Binder implements IAppOpsCallback { 18 | 19 | public static IAppOpsCallback asInterface(IBinder binder) { 20 | throw new RuntimeException("Stub!"); 21 | } 22 | 23 | @Override 24 | public IBinder asBinder() { 25 | throw new RuntimeException("Stub!"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stub/src/main/java/com/android/internal/app/IAppOpsService.java: -------------------------------------------------------------------------------- 1 | package com.android.internal.app; 2 | 3 | import android.app.AppOpsManagerHidden; 4 | import android.os.Binder; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.os.RemoteException; 8 | 9 | import java.util.List; 10 | 11 | public interface IAppOpsService extends IInterface { 12 | 13 | int checkOperation(int code, int uid, String packageName) throws RemoteException; 14 | 15 | void startWatchingMode(int op, String packageName, IAppOpsCallback callback) throws RemoteException; 16 | 17 | void stopWatchingMode(IAppOpsCallback callback) throws RemoteException; 18 | 19 | List getPackagesForOps(int[] ops) throws RemoteException; 20 | 21 | List getOpsForPackage(int uid, String packageName, int[] ops) throws RemoteException; 22 | 23 | List getUidOps(int uid, int[] ops) throws RemoteException; 24 | 25 | void setUidMode(int code, int uid, int mode) throws RemoteException; 26 | 27 | void setMode(int code, int uid, String packageName, int mode) throws RemoteException; 28 | 29 | void resetAllModes(int reqUserId, String reqPackageName) throws RemoteException; 30 | 31 | abstract class Stub extends Binder implements IAppOpsService { 32 | 33 | public static IAppOpsService asInterface(IBinder obj) { 34 | throw new RuntimeException("Stub!"); 35 | } 36 | } 37 | } --------------------------------------------------------------------------------