diff --git a/README.md b/README.md index c9b3b6365..f0ef15b01 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Key features of Ackpine: - **Deferred**: Ackpine allows to defer user's intervention via customizable high-priority notification. - **Progress and state updates**: it's easy to observe every session's state and progress updates. - **Advanced features**: Ackpine allows to benefit from advanced features without juggling complex OS APIs (such as install pre-approval) introduced in later Android versions. -- **Elevated permissions**: Shizuku and root shell are supported as installer backends with advanced install flags. +- **Elevated permissions**: Shizuku, root shell and Dhizuku are supported as installer backends with advanced install flags. - **100% Java and Kotlin-friendly**: while maintaining full Java compatibility, Ackpine was developed as a Kotlin-first library. - **Compatibility**: Ackpine supports Android versions starting from API level 16. @@ -69,6 +69,12 @@ ackpine-libsu = { module = "ru.solrudev.ackpine:ackpine-libsu", version.ref = "a # optional - Kotlin extensions for libsu plugin ackpine-libsu-ktx = { module = "ru.solrudev.ackpine:ackpine-libsu-ktx", version.ref = "ackpine" } +# optional - plugin enabling installs/uninstalls through Dhizuku +ackpine-dhizuku = { module = "ru.solrudev.ackpine:ackpine-dhizuku", version.ref = "ackpine" } + +# optional - Kotlin extensions for Dhizuku plugin +ackpine-dhizuku-ktx = { module = "ru.solrudev.ackpine:ackpine-dhizuku-ktx", version.ref = "ackpine" } + # optional - testing utilities ackpine-test = { module = "ru.solrudev.ackpine:ackpine-test", version.ref = "ackpine" } @@ -82,7 +88,9 @@ ackpine = [ "ackpine-shizuku", "ackpine-shizuku-ktx", "ackpine-libsu", - "ackpine-libsu-ktx" + "ackpine-libsu-ktx", + "ackpine-dhizuku", + "ackpine-dhizuku-ktx" ] ``` @@ -119,6 +127,12 @@ dependencies { // optional - Kotlin extensions for libsu plugin implementation("ru.solrudev.ackpine:ackpine-libsu-ktx:$ackpineVersion") + // optional - plugin enabling installs/uninstalls through Dhizuku + implementation("ru.solrudev.ackpine:ackpine-dhizuku:$ackpineVersion") + + // optional - Kotlin extensions for Dhizuku plugin + implementation("ru.solrudev.ackpine:ackpine-dhizuku-ktx:$ackpineVersion") + // optional - testing utilities testImplementation("ru.solrudev.ackpine:ackpine-test:$ackpineVersion") } diff --git a/ackpine-plugins/dhizuku-ktx/api/dhizuku-ktx.api b/ackpine-plugins/dhizuku-ktx/api/dhizuku-ktx.api new file mode 100644 index 000000000..8bbf9a7b0 --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/api/dhizuku-ktx.api @@ -0,0 +1,52 @@ +public abstract interface class ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDsl { + public abstract fun getRequestDowngrade ()Z + public abstract fun setRequestDowngrade (Z)V +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDslBuilder : ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDsl { + public fun ()V + public final fun build ()Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters; + public fun getRequestDowngrade ()Z + public fun setRequestDowngrade (Z)V +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersKt { + public static final fun DhizukuInstallParameters (Lkotlin/jvm/functions/Function1;)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters; + public static synthetic fun DhizukuInstallParameters$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters; +} + +public abstract interface class ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDsl { + public abstract fun getAllUsers ()Z + public abstract fun getKeepData ()Z + public abstract fun getSystemApp ()Z + public abstract fun setAllUsers (Z)V + public abstract fun setKeepData (Z)V + public abstract fun setSystemApp (Z)V +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDslBuilder : ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDsl { + public fun ()V + public final fun build ()Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters; + public fun getAllUsers ()Z + public fun getKeepData ()Z + public fun getSystemApp ()Z + public fun setAllUsers (Z)V + public fun setKeepData (Z)V + public fun setSystemApp (Z)V +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersKt { + public static final fun DhizukuUninstallParameters (Lkotlin/jvm/functions/Function1;)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters; + public static synthetic fun DhizukuUninstallParameters$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters; +} + +public final class ru/solrudev/ackpine/dhizuku/InstallParametersDslKt { + public static final fun dhizuku (Lru/solrudev/ackpine/installer/parameters/InstallParametersDsl;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun dhizuku$default (Lru/solrudev/ackpine/installer/parameters/InstallParametersDsl;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public final class ru/solrudev/ackpine/dhizuku/UninstallParametersDslKt { + public static final fun dhizuku (Lru/solrudev/ackpine/uninstaller/parameters/UninstallParametersDsl;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun dhizuku$default (Lru/solrudev/ackpine/uninstaller/parameters/UninstallParametersDsl;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + diff --git a/ackpine-plugins/dhizuku-ktx/build.gradle.kts b/ackpine-plugins/dhizuku-ktx/build.gradle.kts new file mode 100644 index 000000000..2e304bf48 --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = "Kotlin extensions for Ackpine Dhizuku plugin" + +plugins { + id("ru.solrudev.ackpine.library") + id("ru.solrudev.ackpine.library-publish") + id("ru.solrudev.ackpine.dokka") +} + +ackpine { + id = "dhizuku-ktx" + minSdk = 26 + artifact { + name = "Ackpine Dhizuku Plugin KTX" + inceptionYear = "2026" + } +} + +dependencies { + api(projects.ackpineKtx) + api(projects.ackpinePlugins.dhizuku) +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/consumer-rules.pro b/ackpine-plugins/dhizuku-ktx/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParameters.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParameters.kt new file mode 100644 index 000000000..4a096c134 --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParameters.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +/** + * Constructs a new instance of [DhizukuPlugin.InstallParameters]. + */ +@Suppress("FunctionName") +public inline fun DhizukuInstallParameters( + configure: DhizukuInstallParametersDsl.() -> Unit = {} +): DhizukuPlugin.InstallParameters { + return DhizukuInstallParametersDslBuilder().apply(configure).build() +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDsl.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDsl.kt new file mode 100644 index 000000000..a1ec913ab --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuInstallParametersDsl.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.DelicateAckpineApi +import ru.solrudev.ackpine.session.parameters.SessionParametersDsl + +/** + * DSL allowing to configure [install parameters for DhizukuPlugin][DhizukuPlugin.InstallParameters]. + */ +@SessionParametersDsl +public interface DhizukuInstallParametersDsl { + + /** + * Whether an upgrade to a lower version of a package than currently installed is requested. + * + * Ackpine sets downgrade-request flag which is honored for debuggable installed apps or debuggable OS builds, but + * does not add the unrestricted downgrade permission flag used by root, shell, or system identities. + * The device owner must still be authorized by Android to perform the downgrade, so setting this option does + * not guarantee that a downgrade will succeed. + * + * This is a **delicate** API. The outcome of using this flag depends on the app being installed, on a + * specific OS build, and on the permissions that the used device owner identity has. + */ + @set:DelicateAckpineApi + public var requestDowngrade: Boolean +} + +@PublishedApi +internal class DhizukuInstallParametersDslBuilder : DhizukuInstallParametersDsl { + + private val delegate = DhizukuPlugin.InstallParameters.Builder() + + @set:DelicateAckpineApi + override var requestDowngrade: Boolean + get() = delegate.requestDowngrade + set(value) { + delegate.setRequestDowngrade(value) + } + + fun build(): DhizukuPlugin.InstallParameters = delegate.build() +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParameters.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParameters.kt new file mode 100644 index 000000000..af19af5c1 --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParameters.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +/** + * Constructs a new instance of [DhizukuPlugin.UninstallParameters]. + */ +@Suppress("FunctionName") +public inline fun DhizukuUninstallParameters( + configure: DhizukuUninstallParametersDsl.() -> Unit = {} +): DhizukuPlugin.UninstallParameters { + return DhizukuUninstallParametersDslBuilder().apply(configure).build() +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDsl.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDsl.kt new file mode 100644 index 000000000..7aeeadcbb --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuUninstallParametersDsl.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.session.parameters.SessionParametersDsl + +/** + * DSL allowing to configure [uninstall parameters for DhizukuPlugin][DhizukuPlugin.UninstallParameters]. + */ +@SessionParametersDsl +public interface DhizukuUninstallParametersDsl { + + /** + * Whether to retain the package's data directory. + */ + public var keepData: Boolean + + /** + * Whether to delete the package for all users. + */ + public var allUsers: Boolean + + /** + * Whether a system app should be marked as uninstalled for the current user. + */ + public var systemApp: Boolean +} + +@PublishedApi +internal class DhizukuUninstallParametersDslBuilder : DhizukuUninstallParametersDsl { + + private val delegate = DhizukuPlugin.UninstallParameters.Builder() + + override var keepData: Boolean + get() = delegate.keepData + set(value) { + delegate.setKeepData(value) + } + + override var allUsers: Boolean + get() = delegate.allUsers + set(value) { + delegate.setAllUsers(value) + } + + override var systemApp: Boolean + get() = delegate.systemApp + set(value) { + delegate.setSystemApp(value) + } + + fun build(): DhizukuPlugin.UninstallParameters = delegate.build() +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/InstallParametersDsl.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/InstallParametersDsl.kt new file mode 100644 index 000000000..c2bb1c3dc --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/InstallParametersDsl.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.installer.parameters.InstallParametersDsl + +/** + * Registers [DhizukuPlugin] for the session. + */ +public inline fun InstallParametersDsl.dhizuku( + configure: DhizukuInstallParametersDsl.() -> Unit = {} +) { + plugin(DhizukuPlugin::class, DhizukuInstallParameters(configure)) +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/UninstallParametersDsl.kt b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/UninstallParametersDsl.kt new file mode 100644 index 000000000..772c07708 --- /dev/null +++ b/ackpine-plugins/dhizuku-ktx/src/main/kotlin/ru/solrudev/ackpine/dhizuku/UninstallParametersDsl.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl + +/** + * Registers [DhizukuPlugin] for the session. + */ +public inline fun UninstallParametersDsl.dhizuku( + configure: DhizukuUninstallParametersDsl.() -> Unit = {} +) { + plugin(DhizukuPlugin::class, DhizukuUninstallParameters(configure)) +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/api/dhizuku.api b/ackpine-plugins/dhizuku/api/dhizuku.api new file mode 100644 index 000000000..b1abbbbd1 --- /dev/null +++ b/ackpine-plugins/dhizuku/api/dhizuku.api @@ -0,0 +1,76 @@ +public final class ru/solrudev/ackpine/dhizuku/DhizukuInstallCapabilities : ru/solrudev/ackpine/capabilities/PluginCapability { + public fun equals (Ljava/lang/Object;)Z + public final fun getRequestDowngrade ()Lru/solrudev/ackpine/capabilities/CapabilityStatus; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin : ru/solrudev/ackpine/capabilities/InstallCapabilityProvider, ru/solrudev/ackpine/capabilities/UninstallCapabilityProvider, ru/solrudev/ackpine/plugability/AckpineInstallPlugin, ru/solrudev/ackpine/plugability/AckpineUninstallPlugin { + public fun ()V + public fun apply (Lru/solrudev/ackpine/plugability/InstallPluginScope;)V + public fun apply (Lru/solrudev/ackpine/plugability/UninstallPluginScope;)V + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getCapabilities (Lru/solrudev/ackpine/capabilities/InstallCapabilityContext;)Lru/solrudev/ackpine/capabilities/PluginCapability; + public fun getCapabilities (Lru/solrudev/ackpine/capabilities/InstallCapabilityContext;)Lru/solrudev/ackpine/dhizuku/DhizukuInstallCapabilities; + public synthetic fun getCapabilities (Lru/solrudev/ackpine/capabilities/UninstallCapabilityContext;)Lru/solrudev/ackpine/capabilities/PluginCapability; + public fun getCapabilities (Lru/solrudev/ackpine/capabilities/UninstallCapabilityContext;)Lru/solrudev/ackpine/dhizuku/DhizukuUninstallCapabilities; + public fun getId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters : ru/solrudev/ackpine/plugability/AckpinePlugin$Parameters { + public static final field Companion Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters$Companion; + public static final field DEFAULT Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters; + public synthetic fun (ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getRequestDowngrade ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters$Builder { + public fun ()V + public final fun build ()Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters; + public final fun getRequestDowngrade ()Z + public final fun setRequestDowngrade (Z)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters$Builder; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$InstallParameters$Companion { +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters : ru/solrudev/ackpine/plugability/AckpinePlugin$Parameters { + public static final field Companion Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Companion; + public static final field DEFAULT Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters; + public synthetic fun (ZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAllUsers ()Z + public final fun getKeepData ()Z + public final fun getSystemApp ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Builder { + public fun ()V + public final fun build ()Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters; + public final fun getAllUsers ()Z + public final fun getKeepData ()Z + public final fun getSystemApp ()Z + public final fun setAllUsers (Z)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Builder; + public final fun setKeepData (Z)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Builder; + public final fun setSystemApp (Z)Lru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Builder; +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuPlugin$UninstallParameters$Companion { +} + +public final class ru/solrudev/ackpine/dhizuku/DhizukuUninstallCapabilities : ru/solrudev/ackpine/capabilities/PluginCapability { + public fun equals (Ljava/lang/Object;)Z + public final fun getAllUsers ()Lru/solrudev/ackpine/capabilities/CapabilityStatus; + public final fun getKeepData ()Lru/solrudev/ackpine/capabilities/CapabilityStatus; + public final fun getSystemApp ()Lru/solrudev/ackpine/capabilities/CapabilityStatus; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/ackpine-plugins/dhizuku/build.gradle.kts b/ackpine-plugins/dhizuku/build.gradle.kts new file mode 100644 index 000000000..9aff896a4 --- /dev/null +++ b/ackpine-plugins/dhizuku/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = "Ackpine plugin providing Dhizuku support for installing packages" + +plugins { + id("ru.solrudev.ackpine.library") + id("ru.solrudev.ackpine.library-publish") + id("ru.solrudev.ackpine.dokka") + alias(libs.plugins.hiddenApiRefine) + alias(libs.plugins.kotlin.ksp) + alias(androidx.plugins.room) +} + +ackpine { + id = "dhizuku" + minSdk = 26 + artifact { + name = "Ackpine Dhizuku Plugin" + inceptionYear = "2026" + } + internalPackages("ru.solrudev.ackpine.dhizuku.database") +} + +room { + schemaDirectory(layout.projectDirectory.dir("schemas")) +} + +dependencies { + ksp(androidx.room.compiler) + api(projects.ackpineCore) + compileOnly(projects.ackpinePlugins.androidStubs) + implementation(projects.ackpinePlugins.privileged) + implementation(projects.ackpineRuntime) + implementation(androidx.room.runtime) + implementation(libs.dhizuku.api) +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/consumer-rules.pro b/ackpine-plugins/dhizuku/consumer-rules.pro new file mode 100644 index 000000000..c3ebf3710 --- /dev/null +++ b/ackpine-plugins/dhizuku/consumer-rules.pro @@ -0,0 +1,8 @@ +-dontwarn android.content.pm.IPackageInstaller$Stub +-dontwarn android.content.pm.IPackageInstaller +-dontwarn android.content.pm.IPackageInstallerSession$Stub +-dontwarn android.content.pm.IPackageInstallerSession +-dontwarn android.content.pm.IPackageManager$Stub +-dontwarn android.content.pm.IPackageManager +-dontwarn android.os.ServiceManager +-keep class ru.solrudev.ackpine.dhizuku.database.DhizukuDatabase_Impl { (); } \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/schemas/ru.solrudev.ackpine.dhizuku.database.DhizukuDatabase/1.json b/ackpine-plugins/dhizuku/schemas/ru.solrudev.ackpine.dhizuku.database.DhizukuDatabase/1.json new file mode 100644 index 000000000..c446699a6 --- /dev/null +++ b/ackpine-plugins/dhizuku/schemas/ru.solrudev.ackpine.dhizuku.database.DhizukuDatabase/1.json @@ -0,0 +1,78 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "ff2f524c6574103c7622d5d16ebb1236", + "entities": [ + { + "tableName": "dhizuku_install_parameters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `request_downgrade` INTEGER NOT NULL, PRIMARY KEY(`session_id`))", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestDowngrade", + "columnName": "request_downgrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dhizuku_uninstall_parameters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `keep_data` INTEGER NOT NULL, `all_users` INTEGER NOT NULL, `system_app` INTEGER NOT NULL, PRIMARY KEY(`session_id`))", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keepData", + "columnName": "keep_data", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allUsers", + "columnName": "all_users", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemApp", + "columnName": "system_app", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff2f524c6574103c7622d5d16ebb1236')" + ] + } +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuCapabilities.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuCapabilities.kt new file mode 100644 index 000000000..455b4dc40 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuCapabilities.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.capabilities.CapabilityStatus +import ru.solrudev.ackpine.capabilities.PluginCapability +import ru.solrudev.ackpine.installer.parameters.InstallerType +import ru.solrudev.ackpine.uninstaller.parameters.UninstallerType + +/** + * Plugin-specific install capabilities reported by [DhizukuPlugin]. + * + * Mirrors [DhizukuPlugin.InstallParameters]. Parameters are only effective when [DhizukuPlugin] actually applies, + * but support here is determined solely from the effective installer type. + */ +public class DhizukuInstallCapabilities internal constructor( + + /** + * Whether [DhizukuPlugin.InstallParameters.requestDowngrade] is supported. + * + * [CapabilityStatus.UNRELIABLE] with [InstallerType.SESSION_BASED]. + */ + public val requestDowngrade: CapabilityStatus +) : PluginCapability { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as DhizukuInstallCapabilities + return requestDowngrade == other.requestDowngrade + } + + override fun hashCode(): Int = requestDowngrade.hashCode() + override fun toString(): String = "DhizukuInstallCapabilities(requestDowngrade=$requestDowngrade)" +} + +/** + * Plugin-specific uninstall capabilities reported by [DhizukuPlugin]. + * + * Mirrors [DhizukuPlugin.UninstallParameters]. Parameters are only effective when [DhizukuPlugin] actually applies, + * but support here is determined solely from the effective uninstaller type. + */ +public class DhizukuUninstallCapabilities internal constructor( + + /** + * Whether [DhizukuPlugin.UninstallParameters.keepData] is supported. + * + * [CapabilityStatus.SUPPORTED] with [UninstallerType.PACKAGE_INSTALLER_BASED]. + */ + public val keepData: CapabilityStatus, + + /** + * Whether [DhizukuPlugin.UninstallParameters.allUsers] is supported. + * + * [CapabilityStatus.SUPPORTED] with [UninstallerType.PACKAGE_INSTALLER_BASED]. + */ + public val allUsers: CapabilityStatus, + + /** + * Whether [DhizukuPlugin.UninstallParameters.systemApp] is supported. + * + * [CapabilityStatus.SUPPORTED] with [UninstallerType.PACKAGE_INSTALLER_BASED]. + */ + public val systemApp: CapabilityStatus +) : PluginCapability { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as DhizukuUninstallCapabilities + if (keepData != other.keepData) return false + if (allUsers != other.allUsers) return false + if (systemApp != other.systemApp) return false + return true + } + + override fun hashCode(): Int { + var result = keepData.hashCode() + result = 31 * result + allUsers.hashCode() + result = 31 * result + systemApp.hashCode() + return result + } + + override fun toString(): String = "DhizukuUninstallCapabilities(" + + "keepData=$keepData, " + + "allUsers=$allUsers, " + + "systemApp=$systemApp" + + ")" +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPackageInstaller.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPackageInstaller.kt new file mode 100644 index 000000000..0841784c0 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPackageInstaller.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import android.content.Context +import android.content.pm.IPackageInstaller +import android.content.pm.IPackageManager +import android.content.pm.PackageInstaller +import android.os.IBinder +import android.os.ServiceManager +import androidx.annotation.RestrictTo +import com.rosan.dhizuku.api.Dhizuku +import ru.solrudev.ackpine.impl.services.PackageInstallerService +import ru.solrudev.ackpine.plugability.AckpinePlugin +import ru.solrudev.ackpine.privileged.PackageInstallerProxy +import ru.solrudev.ackpine.privileged.PrivilegedInstallParameters +import ru.solrudev.ackpine.privileged.PrivilegedUninstallParameters +import ru.solrudev.ackpine.privileged.TargetUser +import java.util.UUID + +/** + * Implementation of [PackageInstallerService] which delegates work to [PackageInstaller] obtained through Dhizuku. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY) +internal class DhizukuPackageInstaller( + context: Context, + remotePackageInstaller: IPackageInstaller, + ownerPackageName: String, + ownerUid: Int +) : PackageInstallerProxy(context, remotePackageInstaller, ownerPackageName, ownerUid) { + + override fun applyParameters(sessionId: UUID, parameters: AckpinePlugin.Parameters) { + when (parameters) { + is DhizukuPlugin.InstallParameters -> applyInstallParameters( + sessionId, + DhizukuInstallParametersBridge.create(parameters) + ) + + is DhizukuPlugin.UninstallParameters -> applyUninstallParameters( + sessionId, + DhizukuUninstallParametersBridge.create(parameters) + ) + } + } + + override fun wrapBinder(original: IBinder): IBinder = Dhizuku.binderWrapper(original) + + internal companion object Factory { + + @JvmSynthetic + internal fun create(context: Context): DhizukuPackageInstaller { + check(Dhizuku.init(context)) { "Failed to initialize Dhizuku" } + val remotePackageManager = IPackageManager.Stub.asInterface( + Dhizuku.binderWrapper(ServiceManager.getService("package")) + ) + val remotePackageInstaller = IPackageInstaller.Stub.asInterface( + Dhizuku.binderWrapper(remotePackageManager.packageInstaller.asBinder()) + ) + val ownerPackageName = Dhizuku.getOwnerPackageName() + val ownerUid = context.packageManager.getApplicationInfo(ownerPackageName, 0).uid + return DhizukuPackageInstaller( + context, + remotePackageInstaller, + ownerPackageName, + ownerUid + ) + } + } +} + +private class DhizukuInstallParametersBridge private constructor( + snapshot: Snapshot +) : PrivilegedInstallParameters(snapshot) { + + override fun getName(): String = "DhizukuInstallParametersBridge" + + private class Builder : PrivilegedInstallParameters.Builder() { + override fun build(): DhizukuInstallParametersBridge = DhizukuInstallParametersBridge(buildSnapshot()) + } + + companion object { + fun create(parameters: DhizukuPlugin.InstallParameters): DhizukuInstallParametersBridge = Builder() + .setRequestDowngrade(parameters.requestDowngrade) + .setTargetUser(TargetUser.CURRENT) + .build() + } +} + +private class DhizukuUninstallParametersBridge private constructor( + snapshot: Snapshot +) : PrivilegedUninstallParameters(snapshot) { + + override fun getName(): String = "DhizukuUninstallParametersBridge" + + private class Builder : PrivilegedUninstallParameters.Builder() { + override fun build(): DhizukuUninstallParametersBridge = DhizukuUninstallParametersBridge(buildSnapshot()) + } + + companion object { + fun create(parameters: DhizukuPlugin.UninstallParameters): DhizukuUninstallParametersBridge = Builder() + .setKeepData(parameters.keepData) + .setAllUsers(parameters.allUsers) + .setSystemApp(parameters.systemApp) + .setTargetUser(TargetUser.CURRENT) + .build() + } +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPlugin.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPlugin.kt new file mode 100644 index 000000000..c668ec48f --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPlugin.kt @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import ru.solrudev.ackpine.DelicateAckpineApi +import ru.solrudev.ackpine.capabilities.CapabilityStatus +import ru.solrudev.ackpine.capabilities.InstallCapabilityContext +import ru.solrudev.ackpine.capabilities.InstallCapabilityProvider +import ru.solrudev.ackpine.capabilities.UninstallCapabilityContext +import ru.solrudev.ackpine.capabilities.UninstallCapabilityProvider +import ru.solrudev.ackpine.installer.parameters.InstallerType +import ru.solrudev.ackpine.plugability.AckpineInstallPlugin +import ru.solrudev.ackpine.plugability.AckpinePlugin +import ru.solrudev.ackpine.plugability.AckpineUninstallPlugin +import ru.solrudev.ackpine.plugability.InstallPluginScope +import ru.solrudev.ackpine.plugability.UninstallPluginScope +import ru.solrudev.ackpine.uninstaller.parameters.UninstallerType + +/** + * Ackpine plugin which enables installation and uninstallation through Dhizuku when applied. + * + * Dhizuku performs package installer operations as the active device owner application. Accordingly, + * [InstallParameters] and [UninstallParameters] expose only options supported for that identity. + * + * **Note:** Dhizuku activation and permission are not managed by this Ackpine plugin. You must handle them in your app + * to successfully use Dhizuku. + */ +public class DhizukuPlugin : + AckpineInstallPlugin, + AckpineUninstallPlugin, + InstallCapabilityProvider, + UninstallCapabilityProvider { + + override val id: String = PLUGIN_ID + + override fun apply(scope: InstallPluginScope) { + scope.installerType = InstallerType.SESSION_BASED + scope.requireUserAction = false + scope.disablePreapproval() + } + + override fun apply(scope: UninstallPluginScope) { + scope.uninstallerType = UninstallerType.PACKAGE_INSTALLER_BASED + } + + override fun getCapabilities(context: InstallCapabilityContext): DhizukuInstallCapabilities { + val requestDowngradeStatus = if (context.installerType == InstallerType.SESSION_BASED) { + CapabilityStatus.UNRELIABLE + } else { + CapabilityStatus.UNSUPPORTED + } + return DhizukuInstallCapabilities(requestDowngrade = requestDowngradeStatus) + } + + override fun getCapabilities(context: UninstallCapabilityContext): DhizukuUninstallCapabilities { + val status = if (context.uninstallerType == UninstallerType.PACKAGE_INSTALLER_BASED) { + CapabilityStatus.SUPPORTED + } else { + CapabilityStatus.UNSUPPORTED + } + return DhizukuUninstallCapabilities( + keepData = status, + allUsers = status, + systemApp = status + ) + } + + override fun equals(other: Any?): Boolean = this === other || other?.javaClass == javaClass + override fun hashCode(): Int = id.hashCode() + override fun toString(): String = javaClass.simpleName + + /** + * Install parameters for [DhizukuPlugin]. + */ + public class InstallParameters private constructor( + + /** + * Whether an upgrade to a lower version of a package than currently installed is requested. + * + * Ackpine sets downgrade-request flag which is honored for debuggable installed apps or debuggable OS builds, + * but does not add the unrestricted downgrade permission flag used by root, shell, or system identities. + * The device owner must still be authorized by Android to perform the downgrade, so setting this option does + * not guarantee that a downgrade will succeed. + */ + public val requestDowngrade: Boolean + ) : AckpinePlugin.Parameters { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as InstallParameters + return requestDowngrade == other.requestDowngrade + } + + override fun hashCode(): Int = requestDowngrade.hashCode() + override fun toString(): String = "InstallParameters(requestDowngrade=$requestDowngrade)" + + /** + * Builder for [DhizukuPlugin.InstallParameters]. + */ + public class Builder { + + /** + * Whether an upgrade to a lower version of a package than currently installed is requested. + * + * Ackpine sets downgrade-request flag which is honored for debuggable installed apps or debuggable OS + * builds, but does not add the unrestricted downgrade permission flag used by root, shell, or system + * identities. + * The device owner must still be authorized by Android to perform the downgrade, so setting this option + * does not guarantee that a downgrade will succeed. + */ + public var requestDowngrade: Boolean = false + private set + + /** + * Sets [InstallParameters.requestDowngrade]. + * + * This is a **delicate** API. The outcome of using this flag depends on the app being installed, on a + * specific OS build, and on the permissions that the used device owner identity has. + */ + @DelicateAckpineApi + public fun setRequestDowngrade(value: Boolean): Builder = apply { + requestDowngrade = value + } + + /** + * Constructs a new instance of [InstallParameters]. + */ + public fun build(): InstallParameters = InstallParameters(requestDowngrade) + } + + public companion object { + + /** + * Default [DhizukuPlugin] install parameters with [requestDowngrade] disabled. + */ + @JvmField + public val DEFAULT: InstallParameters = Builder().build() + } + } + + /** + * Uninstall parameters for [DhizukuPlugin]. + * + * Dhizuku uninstall operations always target the current Android user unless [allUsers] is enabled. + */ + public class UninstallParameters private constructor( + + /** + * Whether to retain the package's data directory. + */ + public val keepData: Boolean, + + /** + * Whether to delete the package for all users. + */ + public val allUsers: Boolean, + + /** + * Whether a system app should be marked as uninstalled for the current user. + * + * This does not remove the app from the system partition. For an updated system app, it prevents the update + * from being rolled back globally when uninstalling it for the current user. + */ + public val systemApp: Boolean + ) : AckpinePlugin.Parameters { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as UninstallParameters + if (keepData != other.keepData) return false + if (allUsers != other.allUsers) return false + if (systemApp != other.systemApp) return false + return true + } + + override fun hashCode(): Int { + var result = keepData.hashCode() + result = 31 * result + allUsers.hashCode() + result = 31 * result + systemApp.hashCode() + return result + } + + override fun toString(): String = "UninstallParameters(" + + "keepData=$keepData, " + + "allUsers=$allUsers, " + + "systemApp=$systemApp" + + ")" + + /** + * Builder for [DhizukuPlugin.UninstallParameters]. + */ + public class Builder { + + /** + * Whether to retain the package's data directory. + */ + public var keepData: Boolean = false + private set + + /** + * Whether to delete the package for all users. + */ + public var allUsers: Boolean = false + private set + + /** + * Whether a system app should be marked as uninstalled for the current user. + */ + public var systemApp: Boolean = false + private set + + /** + * Sets [UninstallParameters.keepData]. + */ + public fun setKeepData(value: Boolean): Builder = apply { + keepData = value + } + + /** + * Sets [UninstallParameters.allUsers]. + */ + public fun setAllUsers(value: Boolean): Builder = apply { + allUsers = value + } + + /** + * Sets [UninstallParameters.systemApp]. + */ + public fun setSystemApp(value: Boolean): Builder = apply { + systemApp = value + } + + /** + * Constructs a new instance of [UninstallParameters]. + */ + public fun build(): UninstallParameters = UninstallParameters( + keepData, + allUsers, + systemApp + ) + } + + public companion object { + + /** + * Default [DhizukuPlugin] uninstall parameters with all flags disabled. + */ + @JvmField + public val DEFAULT: UninstallParameters = Builder().build() + } + } + + internal companion object { + @JvmSynthetic + internal const val PLUGIN_ID = "ru.solrudev.ackpine.dhizuku.DhizukuPlugin" + } +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPluginParametersStore.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPluginParametersStore.kt new file mode 100644 index 000000000..f49b28a47 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuPluginParametersStore.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import androidx.annotation.RestrictTo +import ru.solrudev.ackpine.dhizuku.database.DhizukuInstallParametersEntity +import ru.solrudev.ackpine.dhizuku.database.DhizukuInstallParamsDao +import ru.solrudev.ackpine.dhizuku.database.DhizukuUninstallParametersEntity +import ru.solrudev.ackpine.dhizuku.database.DhizukuUninstallParamsDao +import ru.solrudev.ackpine.impl.plugability.PluginParametersStore +import ru.solrudev.ackpine.plugability.AckpinePlugin +import java.util.UUID + +@RestrictTo(RestrictTo.Scope.LIBRARY) +internal class DhizukuPluginParametersStore( + private val dhizukuInstallParamsDao: DhizukuInstallParamsDao, + private val dhizukuUninstallParamsDao: DhizukuUninstallParamsDao +) : PluginParametersStore { + + override fun getForSession(sessionId: UUID): AckpinePlugin.Parameters { + val id = sessionId.toString() + dhizukuInstallParamsDao.getBySessionId(id)?.let { dhizukuParams -> + return DhizukuPlugin.InstallParameters.Builder() + .setRequestDowngrade(dhizukuParams.requestDowngrade) + .build() + } + dhizukuUninstallParamsDao.getBySessionId(id)?.let { dhizukuParams -> + return DhizukuPlugin.UninstallParameters.Builder() + .setKeepData(dhizukuParams.keepData) + .setAllUsers(dhizukuParams.allUsers) + .setSystemApp(dhizukuParams.systemApp) + .build() + } + return AckpinePlugin.Parameters.None + } + + override fun setForSession(sessionId: UUID, params: AckpinePlugin.Parameters) = when (params) { + is DhizukuPlugin.InstallParameters -> { + val dhizukuParams = DhizukuInstallParametersEntity( + sessionId = sessionId.toString(), + requestDowngrade = params.requestDowngrade + ) + dhizukuInstallParamsDao.insertParameters(dhizukuParams) + } + + is DhizukuPlugin.UninstallParameters -> { + val dhizukuParams = DhizukuUninstallParametersEntity( + sessionId = sessionId.toString(), + keepData = params.keepData, + allUsers = params.allUsers, + systemApp = params.systemApp + ) + dhizukuUninstallParamsDao.insertParameters(dhizukuParams) + } + + else -> { + } + } +} \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuServiceProvider.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuServiceProvider.kt new file mode 100644 index 000000000..fe3fa2ea4 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/DhizukuServiceProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku + +import androidx.annotation.RestrictTo +import ru.solrudev.ackpine.AckpineThreadPool +import ru.solrudev.ackpine.dhizuku.database.DhizukuDatabase +import ru.solrudev.ackpine.impl.plugability.AbstractAckpineServiceProvider +import ru.solrudev.ackpine.impl.services.PackageInstallerService + +@RestrictTo(RestrictTo.Scope.LIBRARY) +internal class DhizukuServiceProvider : AbstractAckpineServiceProvider( + serviceFactories = setOf( + ServiceFactory(PackageInstallerService::class, DhizukuPackageInstaller::create) + ), + pluginEntries = setOf( + PluginEntry(DhizukuPlugin.PLUGIN_ID) { context -> + val database = DhizukuDatabase.getInstance(context, AckpineThreadPool) + DhizukuPluginParametersStore( + database.dhizukuInstallParamsDao(), + database.dhizukuUninstallParamsDao() + ) + } + ) +) \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/database/DhizukuDatabase.kt b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/database/DhizukuDatabase.kt new file mode 100644 index 000000000..2587a28d1 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/kotlin/ru/solrudev/ackpine/dhizuku/database/DhizukuDatabase.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2026 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.solrudev.ackpine.dhizuku.database + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.RoomDatabase +import ru.solrudev.ackpine.impl.database.DatabaseSingleton + +private const val DATABASE_NAME = "ackpine_dhizuku.paramsdb" + +@Database( + entities = [DhizukuInstallParametersEntity::class, DhizukuUninstallParametersEntity::class], + exportSchema = true, + version = 1 +) +internal abstract class DhizukuDatabase : RoomDatabase() { + + abstract fun dhizukuInstallParamsDao(): DhizukuInstallParamsDao + abstract fun dhizukuUninstallParamsDao(): DhizukuUninstallParamsDao + + internal companion object : DatabaseSingleton( + databaseClass = DhizukuDatabase::class.java, + databaseName = DATABASE_NAME + ) +} + +@Dao +internal interface DhizukuInstallParamsDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertParameters(params: DhizukuInstallParametersEntity) + + @Query("SELECT * FROM dhizuku_install_parameters WHERE session_id = :sessionId") + fun getBySessionId(sessionId: String): DhizukuInstallParametersEntity? +} + +@Dao +internal interface DhizukuUninstallParamsDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertParameters(params: DhizukuUninstallParametersEntity) + + @Query("SELECT * FROM dhizuku_uninstall_parameters WHERE session_id = :sessionId") + fun getBySessionId(sessionId: String): DhizukuUninstallParametersEntity? +} + +@Entity(tableName = "dhizuku_install_parameters") +internal class DhizukuInstallParametersEntity( + @JvmField + @PrimaryKey + @ColumnInfo(name = "session_id") + val sessionId: String, + @JvmField + @ColumnInfo(name = "request_downgrade") + val requestDowngrade: Boolean +) + +@Entity(tableName = "dhizuku_uninstall_parameters") +internal class DhizukuUninstallParametersEntity( + @JvmField + @PrimaryKey + @ColumnInfo(name = "session_id") + val sessionId: String, + @JvmField + @ColumnInfo(name = "keep_data") + val keepData: Boolean, + @JvmField + @ColumnInfo(name = "all_users") + val allUsers: Boolean, + @JvmField + @ColumnInfo(name = "system_app") + val systemApp: Boolean +) \ No newline at end of file diff --git a/ackpine-plugins/dhizuku/src/main/resources/META-INF/services/ru.solrudev.ackpine.impl.plugability.AckpineServiceProvider b/ackpine-plugins/dhizuku/src/main/resources/META-INF/services/ru.solrudev.ackpine.impl.plugability.AckpineServiceProvider new file mode 100644 index 000000000..1b96f1b99 --- /dev/null +++ b/ackpine-plugins/dhizuku/src/main/resources/META-INF/services/ru.solrudev.ackpine.impl.plugability.AckpineServiceProvider @@ -0,0 +1 @@ +ru.solrudev.ackpine.dhizuku.DhizukuServiceProvider \ No newline at end of file diff --git a/ackpine-plugins/privileged/src/main/kotlin/ru/solrudev/ackpine/privileged/PackageInstallerProxy.kt b/ackpine-plugins/privileged/src/main/kotlin/ru/solrudev/ackpine/privileged/PackageInstallerProxy.kt index 80e23699d..4324574ec 100644 --- a/ackpine-plugins/privileged/src/main/kotlin/ru/solrudev/ackpine/privileged/PackageInstallerProxy.kt +++ b/ackpine-plugins/privileged/src/main/kotlin/ru/solrudev/ackpine/privileged/PackageInstallerProxy.kt @@ -39,6 +39,10 @@ import ru.solrudev.ackpine.plugability.AckpinePlugin import java.util.UUID import java.util.concurrent.ConcurrentHashMap +private const val ROOT_UID = 0 +private const val SYSTEM_UID = 1000 +private const val SHELL_UID = 2000 + /** * Base [PackageInstallerService] implementation for backends that proxy package installer binders. */ @@ -258,13 +262,16 @@ public abstract class PackageInstallerProxy protected constructor( flags = applyFlag(flags, bypassLowTargetSdkBlock, INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK) flags = applyFlag(flags, allowTest, INSTALL_ALLOW_TEST) flags = applyFlag(flags, replaceExisting, INSTALL_REPLACE_EXISTING) - flags = applyFlag(flags, requestDowngrade, INSTALL_REQUEST_DOWNGRADE or INSTALL_ALLOW_DOWNGRADE) + flags = applyFlag(flags, requestDowngrade, INSTALL_REQUEST_DOWNGRADE) + flags = applyFlag(flags, requestDowngrade && hasUnrestrictedUid(), INSTALL_ALLOW_DOWNGRADE) flags = applyFlag(flags, grantAllRequestedPermissions, INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS) flags = applyFlag(flags, allUsers, INSTALL_ALL_USERS) } params.installFlags = flags } + private fun hasUnrestrictedUid() = uid == ROOT_UID || uid == SYSTEM_UID || uid == SHELL_UID + private fun PrivilegedUninstallParameters.toFlags(): Int { var flags = 0 flags = applyFlag(flags, keepData, DELETE_KEEP_DATA) diff --git a/ackpine-splits/splits-main/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpers.kt b/ackpine-splits/splits-main/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpers.kt index 9c66b337b..c55bf6f17 100644 --- a/ackpine-splits/splits-main/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpers.kt +++ b/ackpine-splits/splits-main/src/main/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpers.kt @@ -43,7 +43,7 @@ internal fun localeFromSplitName(name: String): Locale? { if (language.isEmpty() || language !in availableLanguages) { return null } - return Locale(language) + return Locale.forLanguageTag(language) } @JvmSynthetic diff --git a/ackpine-splits/splits-main/src/test/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpersTest.kt b/ackpine-splits/splits-main/src/test/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpersTest.kt index 8ddcd6fa4..f0fc75212 100644 --- a/ackpine-splits/splits-main/src/test/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpersTest.kt +++ b/ackpine-splits/splits-main/src/test/kotlin/ru/solrudev/ackpine/splits/helpers/LocaleHelpersTest.kt @@ -25,7 +25,11 @@ import kotlin.test.assertTrue class LocaleHelpersTest { - private val deviceLocales = listOf(Locale("en", "US"), Locale("fr", "FR"), Locale("de", "DE")) + private val deviceLocales = listOf( + Locale.forLanguageTag("en-US"), + Locale.forLanguageTag("fr-FR"), + Locale.forLanguageTag("de-DE") + ) @Test fun localeFromSplitNameReturnsLocaleForLanguage() { @@ -59,12 +63,12 @@ class LocaleHelpersTest { @Test fun matchScoreReturnsMaxValueForEmptyDeviceLocales() { - assertEquals(Int.MAX_VALUE, Locale("en").matchScore(emptyList())) + assertEquals(Int.MAX_VALUE, Locale.forLanguageTag("en").matchScore(emptyList())) } @Test fun matchScoreReturnsMaxValueForNonMatchingLanguage() { - assertEquals(Int.MAX_VALUE, Locale("ja").matchScore(deviceLocales)) + assertEquals(Int.MAX_VALUE, Locale.forLanguageTag("ja").matchScore(deviceLocales)) } @Test @@ -75,30 +79,30 @@ class LocaleHelpersTest { @Test fun matchScoreIgnoresCountry() { assertEquals( - Locale("en", "GB").matchScore(listOf(Locale("en", "US"))), - Locale("en").matchScore(listOf(Locale("en", "US"))) + Locale("en", "GB").matchScore(listOf(Locale.forLanguageTag("en-US"))), + Locale("en").matchScore(listOf(Locale.forLanguageTag("en-US"))) ) } @Test fun matchScoreEarlierExactMatchIsBetterThanLaterExactMatch() { - val english = Locale("en", "US").matchScore(deviceLocales) - val french = Locale("fr", "FR").matchScore(deviceLocales) + val english = Locale.forLanguageTag("en-US").matchScore(deviceLocales) + val french = Locale.forLanguageTag("fr-FR").matchScore(deviceLocales) assertTrue(english < french) } @Test fun matchScoreEarlierLanguageOnlyMatchIsBetterThanLaterLanguageOnlyMatch() { - val english = Locale("en", "GB").matchScore(deviceLocales) - val german = Locale("de", "AT").matchScore(deviceLocales) + val english = Locale.forLanguageTag("en-GB").matchScore(deviceLocales) + val german = Locale.forLanguageTag("de-AT").matchScore(deviceLocales) assertTrue(english < german) } @Test fun matchScoreEarlierPositionWithLanguageOnlyMatchBeatsLaterExactMatch() { - val locales = listOf(Locale("en"), Locale("fr", "FR")) - val englishLanguageOnly = Locale("en", "GB").matchScore(locales) - val frenchExact = Locale("fr", "FR").matchScore(locales) + val locales = listOf(Locale.forLanguageTag("en"), Locale.forLanguageTag("fr-FR")) + val englishLanguageOnly = Locale.forLanguageTag("en-GB").matchScore(locales) + val frenchExact = Locale.forLanguageTag("fr-FR").matchScore(locales) assertTrue(englishLanguageOnly < frenchExact) } } \ No newline at end of file diff --git a/api-documentation/build.gradle.kts b/api-documentation/build.gradle.kts index f03aec4dc..bd628c28a 100644 --- a/api-documentation/build.gradle.kts +++ b/api-documentation/build.gradle.kts @@ -41,6 +41,8 @@ dependencies { dokka(projects.ackpineResources) dokka(projects.ackpinePlugins.privileged) dokka(projects.ackpinePlugins.privilegedKtx) + dokka(projects.ackpinePlugins.dhizuku) + dokka(projects.ackpinePlugins.dhizukuKtx) dokka(projects.ackpinePlugins.shizuku) dokka(projects.ackpinePlugins.shizukuKtx) dokka(projects.ackpinePlugins.libsu) diff --git a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaConventionPlugin.kt b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaConventionPlugin.kt index ddd64bbca..e97cf7bf3 100644 --- a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaConventionPlugin.kt +++ b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaConventionPlugin.kt @@ -40,7 +40,7 @@ public class DokkaConventionPlugin : Plugin { override fun apply(target: Project): Unit = target.run { pluginManager.apply(DokkaPlugin::class) - dependencies.add("dokkaPlugin", dokkaPlugin("suppress-annotated-api")) + dependencies.add("dokkaPlugin", dependencies.dokkaPlugin("suppress-annotated-api")) configureDokka() pluginManager.withPlugin(AckpineLibraryBasePlugin.PLUGIN_ID) { val ackpineLibraryExtension = the() diff --git a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaPlugin.kt b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaPlugin.kt index 6c0f1cf0a..204cea532 100644 --- a/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaPlugin.kt +++ b/build-logic/src/main/kotlin/ru/solrudev/ackpine/gradle/documentation/DokkaPlugin.kt @@ -16,6 +16,6 @@ package ru.solrudev.ackpine.gradle.documentation -import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.DependencyHandler -internal fun Project.dokkaPlugin(name: String) = project(":dokka-plugins:$name") \ No newline at end of file +internal fun DependencyHandler.dokkaPlugin(name: String) = project(":dokka-plugins:$name") \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 3f3f342b9..15b9d8533 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { library(projects.ackpineResources) library(projects.ackpinePlugins.privileged) library(projects.ackpinePlugins.privilegedKtx) + library(projects.ackpinePlugins.dhizuku) + library(projects.ackpinePlugins.dhizukuKtx) library(projects.ackpinePlugins.shizuku) library(projects.ackpinePlugins.shizukuKtx) library(projects.ackpinePlugins.libsu) diff --git a/docs/architecture.md b/docs/architecture.md index 1a9afd883..aa7b45725 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -89,6 +89,9 @@ graph TD ackpine-libsu --> ackpine-core ackpine-libsu-ktx --> ackpine-libsu ackpine-libsu-ktx --> ackpine-ktx + ackpine-dhizuku --> ackpine-core + ackpine-dhizuku-ktx --> ackpine-dhizuku + ackpine-dhizuku-ktx --> ackpine-ktx ackpine-splits-ktx --> ackpine-splits ackpine-assets ``` @@ -105,6 +108,8 @@ graph TD | `ackpine-shizuku-ktx` | Kotlin DSL for Shizuku plugin configuration | `ackpine-shizuku`, `ackpine-ktx` | | `ackpine-libsu` | [libsu](guide/libsu.md)-backed `PackageInstallerService` plugin | `ackpine-core` | | `ackpine-libsu-ktx` | Kotlin DSL for libsu plugin configuration | `ackpine-libsu`, `ackpine-ktx` | +| `ackpine-dhizuku` | [Dhizuku](guide/dhizuku.md)-backed `PackageInstallerService` plugin | `ackpine-core` | +| `ackpine-dhizuku-ktx` | Kotlin DSL for Dhizuku plugin configuration | `ackpine-dhizuku`, `ackpine-ktx` | | `ackpine-test` | [Test doubles](guide/testing.md): `TestPackageInstaller`, `TestPackageUninstaller`, scripted sessions | `ackpine-api` | | `ackpine-resources` | Abstractions for persistable Android resources: `ResolvableString` | — | @@ -153,7 +158,7 @@ This design means: Plugin instances are created reflectively via no-arg constructors and cached as singletons in `AckpinePluginCache`. Because of this, plugin class names are persisted (they are stored as part of session parameters), and plugins must have a no-arg constructor available at runtime. Consumer ProGuard rules for `ackpine-api` keep all `AckpinePlugin` implementations, preventing class renaming or constructor removal that would break reflection. -See the [Shizuku](guide/shizuku.md) and [libsu](guide/libsu.md) pages for the currently available plugins and [Configuration](guide/configuration.md#plugins) for usage in parameters. +See the [Shizuku](guide/shizuku.md), [libsu](guide/libsu.md) and [Dhizuku](guide/dhizuku.md) pages for the currently available plugins and [Configuration](guide/configuration.md#plugins) for usage in parameters. Design decisions ---------------- diff --git a/docs/changelog.md b/docs/changelog.md index 3d403bf7e..74f24880c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,21 @@ hide: Change Log ========== +Version 0.25.3 (2026-07-29) +--------------------------- + +### Dependencies + +- Updated `apksig` to 9.3.1. + +### Bug fixes and improvements + +- Introduce Dhizuku plugin giving device-owner privileges for package installation and uninstallation. See docs on usage. + +### Public API changes + +- Added `ackpine-dhizuku` and `ackpine-dhizuku-ktx` artifacts. + Version 0.25.2 (2026-07-20) --------------------------- diff --git a/docs/faq.md b/docs/faq.md index 55b48a028..a04f3ba43 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -27,7 +27,7 @@ To fix this, either ensure that your app can post notifications, or change confi - **Kotlin coroutines**: add `ackpine-ktx` for `Session.await()` and DSL parameter builders. - **Split APKs**: add `ackpine-splits` (and `ackpine-splits-ktx` for Kotlin extensions) for reading and manipulating split APK packages. See [Split APKs](guide/split_apks.md). - **APKs from assets**: add `ackpine-assets` for a `ContentProvider` that reads APKs bundled in your app's assets. -- **Privileged installs**: add `ackpine-shizuku` for Shizuku-backed installs or `ackpine-libsu` for performing installs under root user and their `ktx` counterparts for Kotlin DSLs. See [Shizuku](guide/shizuku.md) and [libsu](guide/libsu.md). +- **Privileged installs**: add `ackpine-shizuku` for Shizuku-backed installs, `ackpine-libsu` for performing installs under root user or `ackpine-dhizuku` for device-owner installs through Dhizuku, and their `ktx` counterparts for Kotlin DSLs. See [Shizuku](guide/shizuku.md), [libsu](guide/libsu.md) and [Dhizuku](guide/dhizuku.md). - **Testing**: add `ackpine-test` as a test dependency for in-memory test doubles. See [Testing](guide/testing.md). See the [module dependency graph](architecture.md#module-dependency-graph) for a visual overview. @@ -38,6 +38,7 @@ Only in specific scenarios: - **Via Shizuku plugin**: when your app has root or ADB shell access through [Shizuku](guide/shizuku.md), installs and uninstalls can bypass user confirmation entirely. - **Via libsu plugin**: when your app has root access, installs and uninstalls can bypass user confirmation entirely. +- **Via Dhizuku plugin**: when your app has device owner access through [Dhizuku](guide/dhizuku.md), installs and uninstalls can bypass user confirmation. - **Via `requireUserAction = false` (delicate API)**: on API level 31+, if certain conditions are met (e.g. the app is updating itself, or the installer has the appropriate permissions), the system may skip user confirmation. See [User's confirmation](guide/configuration.md#users-confirmation) for details and caveats. - **Via being a device owner / affiliated profile owner / system-signed app**: Ackpine uses Android's `PackageInstaller` which will install silently if an app has such privileges. diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 4d712f34f..f8bbd68c4 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -176,6 +176,7 @@ Most configuration options are applied on a best-effort basis — options unavai | Install constraints | 34 | Ignored | | Shizuku plugin | 24 | N/A (requires `ackpine-shizuku` dependency) | | libsu plugin | 21 | N/A (requires `ackpine-libsu` dependency) | +| Dhizuku plugin | 26 | N/A (requires `ackpine-dhizuku` dependency) | User's confirmation ------------------- @@ -352,8 +353,9 @@ Built-in Ackpine plugins: - [`ShizukuPlugin`](shizuku.md) — uses Shizuku to obtain package installer service on behalf of root user or ADB shell. - [`LibsuPlugin`](libsu.md) — uses libsu to perform package installer operations under root user. +- [`DhizukuPlugin`](dhizuku.md) — uses Dhizuku to obtain package installer service as the device owner. -Both plugins support install and uninstall sessions. See [Architecture](../architecture.md#plugin-system) for how the plugin system works internally. +All plugins support install and uninstall sessions. See [Architecture](../architecture.md#plugin-system) for how the plugin system works internally. Querying capabilities --------------------- @@ -423,7 +425,7 @@ Plugins may be passed to include their effect on the resolved result: var capabilities = PackageInstaller.getCapabilities(InstallerType.INTENT_BASED, ShizukuPlugin.class); ``` -Plugins that implement [`InstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-install-capability-provider/index.html) or [`UninstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-uninstall-capability-provider/index.html) also expose plugin-specific capability data accessible via `InstallerCapabilities.plugin()` / `UninstallerCapabilities.plugin()`. See [Shizuku capabilities](shizuku.md#capabilities) or [libsu capabilities](libsu.md#capabilities) for examples. +Plugins that implement [`InstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-install-capability-provider/index.html) or [`UninstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-uninstall-capability-provider/index.html) also expose plugin-specific capability data accessible via `InstallerCapabilities.plugin()` / `UninstallerCapabilities.plugin()`. See [Shizuku capabilities](shizuku.md#capabilities), [libsu capabilities](libsu.md#capabilities) or [Dhizuku capabilities](dhizuku.md#capabilities) for examples. !!! Note Install capabilities are not split-aware. The split-install invariant that forces `SESSION_BASED` is enforced at session creation time and is not reflected in `getCapabilities()` results. \ No newline at end of file diff --git a/docs/guide/dhizuku.md b/docs/guide/dhizuku.md new file mode 100644 index 000000000..6fe1019ce --- /dev/null +++ b/docs/guide/dhizuku.md @@ -0,0 +1,282 @@ +--- +icon: lucide/shield-check +--- + +Dhizuku +======= + +`ackpine-dhizuku` artifact provides a plugin for Ackpine which, when applied, uses Dhizuku to obtain package installer service instead of plain `context.getPackageManager().getPackageInstaller()`, so that Ackpine can manage sessions on behalf of the active device owner even if your app doesn't have such privileges. This can bypass the requirement for user confirmation for uninstalls and fresh installs, request version downgrades, and use selected uninstall flags. + +Dhizuku calls run as the device owner application's UID. They do not run as root, ADB shell, or the Android system. Ackpine therefore exposes only the package installer options applicable to that identity. + +Kotlin examples below use APIs from `ackpine-dhizuku-ktx` artifact. For general plugin usage in session parameters, see [Configuration](configuration.md#plugins). + +!!! warning "Attention" + Dhizuku plugin for Ackpine doesn't activate Dhizuku or manage Dhizuku permission. You must handle these in your app to successfully use Dhizuku. See the [Dhizuku](https://github.com/iamr0s/Dhizuku) and [Dhizuku API documentation](https://github.com/iamr0s/Dhizuku-API). + +Setting up Dhizuku +------------------ + +Latest Dhizuku API version: ![Dhizuku API version](https://img.shields.io/maven-central/v/io.github.iamr0s/Dhizuku-API) + +First of all, if you don't already have Dhizuku support in your app, you need to explicitly add a Dhizuku-API dependency to your build: + +=== "Gradle version catalog" + + ```toml + [versions] + dhizuku = "see above" + + [libraries] + dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" } + ``` + +=== "build.gradle.kts" + + ```kotlin + dependencies { + val dhizukuVersion = "see above" + implementation("io.github.iamr0s:Dhizuku-API:$dhizukuVersion") + } + ``` + +Then add an `ackpine-dhizuku` dependency: + +=== "Gradle version catalog" + + ```toml + [libraries] + ackpine-dhizuku = { module = "ru.solrudev.ackpine:ackpine-dhizuku", version.ref = "ackpine" } + + # Kotlin extensions + ackpine-dhizuku-ktx = { module = "ru.solrudev.ackpine:ackpine-dhizuku-ktx", version.ref = "ackpine" } + ``` + +=== "build.gradle.kts" + + ```kotlin + dependencies { + implementation("ru.solrudev.ackpine:ackpine-dhizuku:$ackpineVersion") + + // Kotlin extensions + implementation("ru.solrudev.ackpine:ackpine-dhizuku-ktx:$ackpineVersion") + } + ``` + + +Initialize Dhizuku and obtain permission before launching an Ackpine session that uses the plugin: + +```kotlin +if (!Dhizuku.init(context)) { + return +} +if (!Dhizuku.isPermissionGranted()) { + Dhizuku.requestPermission(permissionListener) + return +} +``` + +The plugin supports Android 8.0 (API 26) and newer. + +Using the plugin +---------------- + +!!! warning "Google Play" + This plugin uses hidden Android APIs. This may cause your app to fail app review on Google Play. Disable reporting information about SDK dependencies in `build.gradle.kts`: + ``` + android { + dependenciesInfo { + includeInApk = false + includeInBundle = false + } + } + ``` + +To apply the plugin to an install session, just add this to your install parameters configuration: + +=== "Kotlin" + + ```kotlin + val session = packageInstaller.createSession(uri) { + // ...some session configuration... + dhizuku() + + // Or, if you want to configure some parameters for the plugin + dhizuku { + requestDowngrade = true + } + } + ``` + +=== "Java" + + ```java + var parameters = new InstallParameters.Builder(uri) + // ...some session configuration... + .registerPlugin(DhizukuPlugin.class, DhizukuPlugin.InstallParameters.DEFAULT) + .build(); + + // Or, if you want to configure some parameters for the plugin + var dhizukuParameters = new DhizukuPlugin.InstallParameters.Builder() + .setRequestDowngrade(true) + .build(); + var parameters = new InstallParameters.Builder(uri) + .registerPlugin(DhizukuPlugin.class, dhizukuParameters) + .build(); + ``` + +Also, you can use Dhizuku for uninstall sessions: + +=== "Kotlin" + + ```kotlin + val session = packageUninstaller.createSession(packageName) { + // ...some session configuration... + dhizuku() + + // Or, if you want to configure some parameters for the plugin + dhizuku { + keepData = true + allUsers = true + systemApp = true + } + } + ``` + +=== "Java" + + ```java + var parameters = new UninstallParameters.Builder(packageName) + // ...some session configuration... + .registerPlugin(DhizukuPlugin.class, DhizukuPlugin.UninstallParameters.DEFAULT) + .build(); + + // Or, if you want to configure some parameters for the plugin + var dhizukuParameters = new DhizukuPlugin.UninstallParameters.Builder() + .setKeepData(true) + .setAllUsers(true) + .setSystemApp(true) + .build(); + var parameters = new UninstallParameters.Builder(packageName) + .registerPlugin(DhizukuPlugin.class, dhizukuParameters) + .build(); + ``` + +If Dhizuku is not active, or if Dhizuku permission is not granted for your app, session will fail. + +Hidden APIs +----------- + +This plugin initializes hidden Android API exemptions through AndroidX Startup using [LSPosed's HiddenApiBypass](https://github.com/LSPosed/AndroidHiddenApiBypass). In case you have your own additional exemptions: + +1. Disable the initializer in `AndroidManifest.xml`: + + ```xml + + + + ``` + +2. Add this list to your `HiddenApiBypass.setHiddenApiExemptions` call: + + ```kotlin + HiddenApiBypass.setHiddenApiExemptions( + "Landroid/content/pm/IPackageManager", + "Landroid/content/pm/IPackageInstaller", + "Landroid/content/pm/IPackageInstallerSession", + "Landroid/content/pm/PackageInstaller", + "Landroid/os/UserHandle", + "Landroid/os/ServiceManager" + ) + ``` + +Plugin parameters +----------------- + +By default, all flags are disabled. + +Dhizuku operations only target the current Android user (unlike Shizuku and libsu plugins), because device owner identity doesn't give access to cross-user operations on its own without `android.permission.INTERACT_ACROSS_USERS_FULL` permission, which has `signature|installer|role` protection levels. + +### Install flags + +#### `requestDowngrade` + +Indicates that an upgrade to a lower version of a package than currently installed has been requested. + +Ackpine sets downgrade-request flag which is honored for debuggable installed apps or debuggable OS builds, but does not add the unrestricted downgrade permission flag used by root, shell, or system identities. The device owner must still be authorized by Android to perform the downgrade, so setting this option does not guarantee that a downgrade will succeed. + +This is a **delicate** API. + +### Uninstall flags + +#### `keepData` + +Flag parameter to indicate that you don't want to delete the package's data directory. + +#### `allUsers` + +Flag parameter to indicate that you want the package deleted for all users. + +#### `systemApp` + +Flag parameter to indicate that a system app should be marked as uninstalled for the current user. + +This does not remove the app from the system partition. For an updated system app, it prevents the update from being rolled back globally when uninstalling it for the current user. + +Capabilities +------------ + +`DhizukuPlugin` implements [`InstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-install-capability-provider/index.html) and [`UninstallCapabilityProvider`](../api/ackpine-api/api-main/ru.solrudev.ackpine.capabilities/-uninstall-capability-provider/index.html), so you can query whether individual Dhizuku install/uninstall parameters are supported for a given configuration via `getCapabilities()`. See [Querying capabilities](configuration.md#querying-capabilities) for an overview of the capabilities API. + +Each field in [`DhizukuInstallCapabilities`](../api/ackpine-plugins/dhizuku/ru.solrudev.ackpine.dhizuku/-dhizuku-install-capabilities/index.html) mirrors the corresponding `DhizukuPlugin.InstallParameters` flag and reports whether it is supported for the resolved configuration. Similarly, [`DhizukuUninstallCapabilities`](../api/ackpine-plugins/dhizuku/ru.solrudev.ackpine.dhizuku/-dhizuku-uninstall-capabilities/index.html) mirrors `DhizukuPlugin.UninstallParameters`. + +!!! Note + Dhizuku capability support is determined from the effective installer/uninstaller type. Whether Dhizuku is active, whether permission is granted to the app at runtime, and whether Android authorizes a specific downgrade are not taken into account. + +=== "Kotlin" + + ```kotlin + val capabilities = PackageInstaller.getCapabilities(InstallerType.SESSION_BASED, DhizukuPlugin::class) + val dhizukuCaps = capabilities.plugin(DhizukuPlugin::class) ?: return + if (dhizukuCaps.requestDowngrade.isAvailable) { + // The plugin can request a downgrade for this configuration + } + ``` + +=== "Java" + + ```java + var capabilities = PackageInstaller.getCapabilities(InstallerType.SESSION_BASED, DhizukuPlugin.class); + var dhizukuCaps = capabilities.plugin(DhizukuPlugin.class); + if (dhizukuCaps != null && dhizukuCaps.getRequestDowngrade().isAvailable()) { + // The plugin can request a downgrade for this configuration + } + ``` + +For uninstall: + +=== "Kotlin" + + ```kotlin + val capabilities = PackageUninstaller.getCapabilities(UninstallerType.PACKAGE_INSTALLER_BASED, DhizukuPlugin::class) + val dhizukuCaps = capabilities.plugin(DhizukuPlugin::class) ?: return + if (dhizukuCaps.keepData.isSupported) { + // keepData is effective on this device + } + ``` + +=== "Java" + + ```java + var capabilities = PackageUninstaller.getCapabilities(UninstallerType.PACKAGE_INSTALLER_BASED, DhizukuPlugin.class); + var dhizukuCaps = capabilities.plugin(DhizukuPlugin.class); + if (dhizukuCaps != null && dhizukuCaps.getKeepData().isSupported()) { + // keepData is effective on this device + } + ``` \ No newline at end of file diff --git a/docs/guide/shizuku.md b/docs/guide/shizuku.md index f36f187f1..466241fdc 100644 --- a/docs/guide/shizuku.md +++ b/docs/guide/shizuku.md @@ -5,7 +5,7 @@ icon: lucide/toy-brick Shizuku ======= -`ackpine-shizuku` artifact provides plugins for Ackpine which, when applied, use Shizuku to obtain package installer service instead of plain `context.getPackageManager().getPackageInstaller()`, so that Ackpine can manage sessions on behalf of root user or ADB shell even if your app doesn't have such privileges. This enables possibility of using such flags as bypassing low target SDK of the installed app on Android 14+, requesting version downgrade, keeping app data when uninstalling and others. Also it can bypass requirement for user's confirmation for uninstalls and fresh installs. +`ackpine-shizuku` artifact provides a plugin for Ackpine which, when applied, uses Shizuku to obtain package installer service instead of plain `context.getPackageManager().getPackageInstaller()`, so that Ackpine can manage sessions on behalf of root user or ADB shell even if your app doesn't have such privileges. This enables possibility of using such flags as bypassing low target SDK of the installed app on Android 14+, requesting version downgrade, keeping app data when uninstalling and others. Also it can bypass requirement for user's confirmation for uninstalls and fresh installs. Kotlin examples below use APIs from `ackpine-shizuku-ktx` artifact. For general plugin usage in session parameters, see [Configuration](configuration.md#plugins). diff --git a/docs/index.md b/docs/index.md index de842602f..5b787be88 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ Key features of Ackpine: - **Deferred**: Ackpine allows to defer user's intervention via customizable high-priority notification. - **Progress and state updates**: it's easy to observe every session's state and progress updates. - **Advanced features**: Ackpine allows to benefit from advanced features without juggling complex OS APIs (such as install pre-approval) introduced in later Android versions. -- **Elevated permissions**: Shizuku and root shell are supported as installer backends with advanced install flags. +- **Elevated permissions**: Shizuku, root shell and Dhizuku are supported as installer backends with advanced install flags. - **100% Java and Kotlin-friendly**: while maintaining full Java compatibility, Ackpine was developed as a Kotlin-first library. - **Compatibility**: Ackpine supports Android versions starting from API level 16. @@ -61,15 +61,21 @@ Latest version: ![Maven Central](https://img.shields.io/maven-central/v/ru.solru # optional - Kotlin extensions for Shizuku plugin ackpine-shizuku-ktx = { module = "ru.solrudev.ackpine:ackpine-shizuku-ktx", version.ref = "ackpine" } - # optional - testing utilities - ackpine-test = { module = "ru.solrudev.ackpine:ackpine-test", version.ref = "ackpine" } - # optional - plugin enabling installs/uninstalls under root user ackpine-libsu = { module = "ru.solrudev.ackpine:ackpine-libsu", version.ref = "ackpine" } # optional - Kotlin extensions for libsu plugin ackpine-libsu-ktx = { module = "ru.solrudev.ackpine:ackpine-libsu-ktx", version.ref = "ackpine" } + # optional - plugin enabling installs/uninstalls through Dhizuku + ackpine-dhizuku = { module = "ru.solrudev.ackpine:ackpine-dhizuku", version.ref = "ackpine" } + + # optional - Kotlin extensions for Dhizuku plugin + ackpine-dhizuku-ktx = { module = "ru.solrudev.ackpine:ackpine-dhizuku-ktx", version.ref = "ackpine" } + + # optional - testing utilities + ackpine-test = { module = "ru.solrudev.ackpine:ackpine-test", version.ref = "ackpine" } + [bundles] ackpine = [ "ackpine-core", @@ -80,7 +86,9 @@ Latest version: ![Maven Central](https://img.shields.io/maven-central/v/ru.solru "ackpine-shizuku", "ackpine-shizuku-ktx", "ackpine-libsu", - "ackpine-libsu-ktx" + "ackpine-libsu-ktx", + "ackpine-dhizuku", + "ackpine-dhizuku-ktx" ] ``` @@ -114,6 +122,12 @@ Latest version: ![Maven Central](https://img.shields.io/maven-central/v/ru.solru // optional - Kotlin extensions for libsu plugin implementation("ru.solrudev.ackpine:ackpine-libsu-ktx:$ackpineVersion") + + // optional - plugin enabling installs/uninstalls through Dhizuku + implementation("ru.solrudev.ackpine:ackpine-dhizuku:$ackpineVersion") + + // optional - Kotlin extensions for Dhizuku plugin + implementation("ru.solrudev.ackpine:ackpine-dhizuku-ktx:$ackpineVersion") // optional - testing utilities testImplementation("ru.solrudev.ackpine:ackpine-test:$ackpineVersion") diff --git a/gradle.properties b/gradle.properties index 293c21041..2d8a5853c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2g org.gradle.daemon=true org.gradle.parallel=true +org.gradle.tooling.parallel=true org.gradle.configuration-cache=true org.gradle.configuration-cache.parallel=true org.gradle.configureondemand=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2da5705ac..59c2151c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -android-plugin = "9.2.1" +android-plugin = "9.3.1" bundletool = "1.18.3" dokka = "2.2.0" hidden-api-refine = "4.4.0" @@ -7,7 +7,7 @@ hidden-api-refine = "4.4.0" jacoco = "0.8.14" #noinspection UnusedVersionCatalogEntry kotlin-for-consumers = "2.1.21" -kotlin = "2.4.0" +kotlin = "2.4.10" kotlin-ksp = "2.3.9" libsu = "6.0.0" shizuku = "13.1.5" @@ -24,6 +24,7 @@ plugin-toolchains = "org.gradle.toolchains.foojay-resolver-convention:org.gradle apache-commons-io = "commons-io:commons-io:2.20.0" apksig = { module = "com.android.tools.build:apksig", version.ref = "android-plugin" } bundletool = { module = "com.android.tools.build:bundletool", version.ref = "bundletool" } +dhizuku-api = "io.github.iamr0s:Dhizuku-API:2.5.4" dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" } dokka-core = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka" } guava = "com.google.guava:guava:33.4.8-android" diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index a559f6e34..2e9cb25be 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -357,6 +357,42 @@ uxyN4W6Uwu+Uov12cGV2sAI+H5qPhF4= =52vp -----END PGP PUBLIC KEY BLOCK----- +pub A2897DF851709C93 +uid iamr0s <271257581@qq.com> + +sub 185767EE85079A39 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGRCdBABDADdR74+kAC35ra91tX3T0H2Ki6h6cF5dwRrcAXTSX6g8C3xrZC3 +2koylJCciNEdsZstW6WIMsMvDJvge/dgE3pekumksmpFjtMq/Lw7rTgfAvFzNHxk +8a34K0pWDkwLBph1c6EBuVhJJt6UnZjaf/vVBnKmw88nzK0T9cTHrog/lJwuOZPW +qjy1VPkb/VMuxRx6ZW3oz/7FeFpGqLb1HTowbnykUsuSAEf1+A98CYGf2XA7lmeh +XL9nNFDB898evd9TOVg7A4mcL4OV1w+rlJyuF4qukJYMvF8EdzwTNR0L7k9ns4fX +Vi1qdE9EPkAdQcgp2DFK8GMHO9EpVL9yZY8+7fDoVA7bWUjyP7yGY+9qfCU2VjmT +6JXxUdsD+7uLsXiBtI/WeNVlh6sfBnrQ3qicEqlyvw2+gqFfxp1rVkQC3jdznQQM +DWOrT9bQGr4ATj4TWw3kYdwUxaZoS3uU2Gj7cxS4uDm/5OCwqgVD1r9KPYj7lg7i +aXDev9u03pVJFu0AEQEAAbQZaWFtcjBzIDwyNzEyNTc1ODFAcXEuY29tPs7AzQRk +QnQQAQwAznVkiDIlEihoaE0+siJm+OF30TqLI8FjZ2vSAG05ZL7RGGmBiXw/YLWv +XjnE1btgnh6isZ3fZRWZp8+/bi2DVshm+iA5LJXS8n18cywzRzKkW+vRDDK6aFtM +RxTpE2JYyWNSib204Qoqa7bRf9MlXRxyP10zQ9hvUCwMt3kOgJndMT1KNQM9s4QH +vcho+bRd16/jQu1Cb0ThlBLnk8uYT0aWZYcVgM1+QBcHq1DJi5cmy55CVFM17d6N +pqxZE8WSXzsSpn5KD6GKR2tCRoqtvoWA1STGodayX9POY78chm8Fbiw1lBAz6Xd2 ++SaehMrUNHW4GKtff6sIWe8LWL9n1HVoiPhbAkruVzaZFdlV0RqdidZ6jqjjiMz6 +jmi7vpp9OGI1ImArc5nMYnEXGXyo8uUv9YI0w/1BqKXHUEp1L/oAqAF5vP9FcCbr +uePRhaID1wlC/oEGgyUtRv1Rkyz4ECPhEuHfWDnRLtQPgH2fOkZVKVo04uqvOCbA +hemalYetABEBAAHCwPwEGAEIACYWIQRyDKu2AAMcuhb7G26iiX34UXCckwUCZEJ0 +EAIbDAUJXfwPAAAKCRCiiX34UXCck1LbC/9WSrIQ/0RdsiHh5mu35ax2iGBQ2woh +H+X89LMxsmRxXXGcLq8X7C+esLPsTDUa+wctq5ZA/h7jCUfQVQraNMj6aODZYEJb +JChi2y/QLfJBVIVS644kOlzWG/UH7dAOjYJM7eETZ65OPlgUMFAh4DJQPPZ0CxbP +DLGItJtk7qSG6mPQMXI0nkDHCi+JQemk43ebVYLy7DHI1JcCqn4xVpDWto529wCz +ExTMDbvvpGocF3MWb9TCSJuY1PIlbjq8nuLVy85DIOIaOzHHAWlhzx55DCGDUtJ5 +jcrdXe1v8XOLYeGqaPKw1rAyz4YwjNOvFlMQeXe/2D86DZG6HatLhFosdvhFU/7E +u16oT0vsz/bpErYCKJRblMRw7go7CycZR3M5+4qHmvUJ/XWxIGjHBVMohphuseME +T+ZUhaMqPNmLfuwBMTON04s6rdJSa+C1PFEjOepqTDq/NcQJl7vzhmekNtY8rO9/ +vzkv54LSTkoGVs+lKx5H15LwlFLVC0FwIIk= +=GfpE +-----END PGP PUBLIC KEY BLOCK----- + pub A40E24B5B408DBD5 uid Robolectric Developers diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1a73bfceb..fd7404a84 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -124,6 +124,7 @@ + diff --git a/version.json b/version.json index 7c06057c6..76f3d2ce0 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { "majorVersion": 0, "minorVersion": 25, - "patchVersion": 2, + "patchVersion": 3, "suffix": "", "isSnapshot": false } \ No newline at end of file diff --git a/zensical.toml b/zensical.toml index 1b52c7f8c..09dda8099 100644 --- a/zensical.toml +++ b/zensical.toml @@ -16,6 +16,7 @@ nav = [ { "Split APKs" = "guide/split_apks.md" }, { "Shizuku" = "guide/shizuku.md" }, { "Root permissions" = "guide/libsu.md" }, + { "Dhizuku" = "guide/dhizuku.md" }, { "Testing" = "guide/testing.md" }, { "Logging" = "guide/logging.md" }, { "Samples" = "guide/samples.md" }