├── .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 | [](https://github.com/SanmerApps/PI/releases) [](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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Instalar
4 | Modo de trabalho
5 | Requer permissão fornecida por Sui ou Shizuku
6 | Requer permissões root fornecidas por Magisk, KernelSU ou APatch
7 | O serviço está em execução
8 | O serviço não está em execução
9 | Versão %1$d, %2$s
10 | Solicitante de instalação
11 | Lista vazia
12 | Configurações
13 | Pacote de instalação
14 | Instalação bem-sucedida
15 | Falha na instalação
16 | Carregando…
17 | Pesquisar…
18 | Instalar serviço
19 | Erro desconhecido
20 | Densidade da tela
21 | Recurso dinâmico
22 | ABI
23 | Idioma
24 | Não especificado
25 | Participe da tradução
26 | Ajude-nos a traduzir o PI para o seu idioma
27 | Clique para tentar começar
28 | Idioma
29 | Padrão do sistema
30 | Falha na análise do pacote
31 | Autorizar
32 | Solicitante
33 | Executor
34 | PI atualizado
35 | Toque para abrir o app
36 | Análise
37 | Serviço de análise
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Instalar
4 | Modo de trabalho
5 | Necessita de permissões root concedida por Magisk, KernelSU ou APatch
6 | O serviço está em execução
7 | O serviço não está em execução
8 | Versão %1$d, %2$s
9 | Solicitante de instalação
10 | Lista vazia
11 | Pacote de instalação
12 | Falha na instalação
13 | Carregando…
14 | Pesquisar…
15 | Necessita de permissão concedida por Sui ou Shizuku
16 | Definições
17 | Instalação bem-sucedida
18 | Instalar serviço
19 | Erro desconhecido
20 | Recurso dinâmico
21 | ABI
22 | Densidade da ecrã
23 | Língua
24 | Não especificado
25 | Participe da tradução
26 | Ajude-nos a traduzir o PI para o seu idioma
27 | Clique para tentar começar
28 | Idioma
29 | Padrão do sistema
30 | Falha na análise do pacote
31 | Autorizar
32 | Solicitante
33 | Executor
34 | PI atualizado
35 | Toque para abrir o app
36 | Serviço de análise
37 | Análise
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ro/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Necesită permisiuni Root oferite de Magisk, KernelSU, sau APatch
4 | Instalează
5 | Versiunea %1$d, %2$s
6 | Setări
7 | Serviciul rulează
8 | Solicitant
9 | Executorul
10 | Autorizează
11 | Valoarea implicită a sistemului
12 | Serviciul nu rulează
13 | Participă in traducere
14 | Limbă
15 | Ajută-ne să traducem PI în limba ta
16 | Apasă pentru a solicita pornirea
17 | ABI
18 | Analiza pachetului a eșuat
19 | Instalat cu succes
20 | Instalează serviciul
21 | Instalarea a eșuat
22 | Eroare necunoscută
23 | Apăsați aici pentru a deschide aplicația
24 | Căutare…
25 | PI a fost actualizat
26 | Se încarcă…
27 | Listă goală
28 | Modul de lucrare
29 | Caracteristică dinamică
30 | Nespecificat
31 | Densitatea ecranului
32 | Solicitantul de instalare
33 | Necesită o permisiune oferită de Sui sau Shizuku
34 | Limbă
35 | Pachetul de instalare
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Рабочий режим
4 | Требуются права root, предоставленные Magisk, KernelSU или APatch
5 | Служба работает
6 | Служба не запущена
7 | Версия %1$d, %2$s
8 | Установка
9 | Требуется разрешение Sui или Shizuku
10 | Плотность экрана
11 | Язык
12 | Установка прервана
13 | Пустой список
14 | Неизвестная ошибка
15 | Динамическая функция
16 | Неопределённый
17 | Установка завершена
18 | Сервис установки
19 | Нажмите, чтобы попытаться начать
20 | Язык
21 | Системный по умолчанию
22 | Участие в переводе
23 | Помогите нам перевести PI на ваш язык
24 | Установка приложения
25 | Запросчик установки
26 | Загрузка…
27 | ABI
28 | Настройки
29 | Сбой синтаксического анализа пакета
30 | Поиск…
31 | PI Обновлён
32 | Нажмите чтоб открыть приложение
33 | Авторизовать
34 | Запросчик
35 | Исполнитель
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values-su/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pasang
4 | Kapadetan layar
5 | Modeu gawé
6 | Ngabutuhkeun idin nu disadiakeun ku Sui atawa Shizuku
7 | Ngabutuhkeun idin Root nu disadiakeun ku Magisk, KernelSU, atawa APatch
8 | Diijinkeun
9 | Paménta
10 | Éksékutor
11 | Pangaturan
12 | Layanan geus jalan
13 | Vérsi %1$d, %2$s
14 | Layanan teu jalan
15 | Klik keur ngajalankeun
16 | Basa
17 | Ngamuat…
18 | Téang…
19 | Parsing pakét gagal
20 | Eusi kosong
21 | Pamasangan réngsé
22 | Pamasangan gagal
23 | Bawaan ti sistem
24 | Babantu pikeun tarjamahan
25 | Ngabantu pikeun tarjamahan PI kana basa anjeun
26 | Pakét pamasangan
27 | Paménta pamasangan
28 | Fitur dinamis
29 | ABI
30 | Basa
31 | Teu puguh rupana
32 | Pasang layanan
33 | Error teu dipikanyaho
34 | PI Dianyarkeun
35 | Toél pikeun muka apl
36 | Layanan panguraian
37 | Panguraian
38 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Yükle
4 | Çalışma modu
5 | Sui veya Shizuku tarafından sağlanan izin gereklidir
6 | Magisk, KernelSU veya APatch tarafından sağlanan Root izinleri gerektirir
7 | Talep Eden
8 | Yürütücü
9 | Yetkilendir
10 | Ayarlar
11 | Servis çalışıyor
12 | Sürüm %1$d, %2$s
13 | Servis çalışmıyor
14 | Başlatmayı denemek için tıklayın
15 | Dil
16 | Sistem varsayılanı
17 | Çeviriye katıl
18 | PI\'yi kendi dilinize çevirmemize yardımcı olun
19 | Kurulum paketi
20 | Kurulumu talep eden
21 | Dinamik özellik
22 | ABI
23 | Ekran yoğunluğu
24 | Dil
25 | Belirtilmemiş
26 | Paket ayrıştırma başarısız oldu
27 | Servisi yükle
28 | Yükleme başarılı
29 | Yükleme başarısız
30 | Yükleniyor…
31 | Bilinmeyen hata
32 | PI Güncellendi
33 | Uygulamayı açmak için dokunun
34 | Ara…
35 | Liste boş
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/system_accent1_600
4 | @android:color/system_accent1_0
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-vi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cài đặt
4 | Cần có sự cho phép của Sui hoặc Shizuku
5 | Dịch vụ đang chạy
6 | Dịch vụ không chạy
7 | Phiên bản %1$d, %2$s
8 | Trình yêu cầu cài đặt
9 | Danh sách trống
10 | Thiết đặt
11 | Tham gia dịch thuật
12 | Hãy giúp chúng tôi dịch PI sang ngôn ngữ của bạn
13 | Gói cài đặt
14 | Tính năng linh động
15 | Mật độ màn hình
16 | Ngôn ngữ
17 | Không cụ thể
18 | Cài đặt thành công
19 | Cài đặt thất bại
20 | Đang tải…
21 | Tìm…
22 | Cài đặt dịch vụ
23 | Lỗi không rõ
24 | ABI
25 | Chế độ làm việc
26 | Yêu cầu quyền Root do Magisk hoặc KernelSU cung cấp
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 安装
5 |
6 |
7 | 工作模式
8 | 需要由 Sui 或 Shizuku 提供权限
9 | 需要由 Magisk,KernelSU 或 APatch 提供 Root 权限
10 |
11 |
12 | 请求者
13 | 执行者
14 | 授权
15 | 已授权
16 |
17 |
18 | 设置
19 | 服务正在启动
20 | 请耐心等待
21 | 服务正在运行
22 | 版本 %1$d,%2$s
23 | 服务未运行
24 | 点击尝试启动
25 | 语言
26 | 系统默认
27 | 参与翻译
28 | 帮助我们将 PI 翻译至您的语言
29 |
30 |
31 | 安装包
32 | 请求者
33 | 动态特征
34 | ABI
35 | 屏幕密度
36 | 语言
37 | 未知
38 | 选择用户
39 |
40 |
41 | 安装服务
42 | 安装成功
43 | 安装失败
44 | 解析服务
45 | 正在解析
46 | 解析失败
47 | 正在优化
48 | 加载中…
49 | 未知错误
50 | PI 已完成更新
51 | 点按即可打开应用
52 |
53 |
54 | 搜索…
55 | 空列表
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0A0C10
4 | #FBFCFD
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Install
5 |
6 |
7 | Working mode
8 | Requires permission provided by Sui or Shizuku
9 | Requires Root permissions provided by Magisk, KernelSU or APatch
10 |
11 |
12 | Requester
13 | Executor
14 | Authorize
15 | Authorized
16 |
17 |
18 | Settings
19 | Service is starting
20 | Please wait
21 | Service is running
22 | Version %1$d, %2$s
23 | Service is not running
24 | Click to try to restart
25 | Language
26 | System default
27 | Participate in translation
28 | Help us translate PI into your language
29 |
30 |
31 | Installation package
32 | Installation requester
33 | Dynamic feature
34 | ABI
35 | Screen density
36 | Language
37 | Unspecified
38 | Select user
39 |
40 |
41 | Parsing service
42 | Parsing
43 | Parsing failed
44 | Installation service
45 | Installation successful
46 | Installation failed
47 | Optimizing
48 | Loading…
49 | Unknown error
50 | PI Updated
51 | Tap to open app
52 |
53 |
54 | Search…
55 | Empty list
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_untranslatable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PI
4 |
5 |
6 | Shizuku
7 | Root
8 |
9 |
10 | %1$s → %2$s
11 | Target: %1$s, Min: %2$s, Compile: %3$s
12 | Size: %1$s
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
17 |
18 |
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 | }
--------------------------------------------------------------------------------