├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ └── demo.cpp │ ├── java │ └── be │ │ └── mygod │ │ └── librootkotlinx │ │ └── demo │ │ ├── App.kt │ │ ├── Jni.kt │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ └── ic_launcher.webp │ ├── mipmap-mdpi │ └── ic_launcher.webp │ ├── mipmap-xhdpi │ └── ic_launcher.webp │ ├── mipmap-xxhdpi │ └── ic_launcher.webp │ ├── mipmap-xxxhdpi │ └── ic_launcher.webp │ └── values │ └── strings.xml ├── build.gradle.kts ├── detekt.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── be │ └── mygod │ └── librootkotlinx │ ├── AppProcess.kt │ ├── JniInit.kt │ ├── Logger.kt │ ├── ParcelableThrowable.kt │ ├── RootServer.kt │ ├── RootSession.kt │ ├── ServerCommands.kt │ └── Utils.kt └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/code 5 | docker: 6 | - image: cimg/android:2024.08.1-ndk 7 | environment: 8 | GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process" 9 | steps: 10 | - checkout 11 | - run: git submodule sync 12 | - run: git submodule update --init --recursive 13 | - restore_cache: 14 | key: jars-{{ checksum "build.gradle.kts" }} 15 | - run: 16 | name: Run Build and Tests 17 | command: ./gradlew assembleDebug check 18 | - save_cache: 19 | paths: 20 | - ~/.gradle 21 | - ~/.android/build-cache 22 | key: jars-{{ checksum "build.gradle.kts" }} 23 | - store_artifacts: 24 | path: app/build/outputs/apk 25 | destination: apk 26 | - store_artifacts: 27 | path: app/build/reports 28 | destination: reports/app 29 | - store_artifacts: 30 | path: lib/build/reports 31 | destination: reports/lib 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Mygod] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | androidTest/ 10 | test/ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Mygod 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # librootkotlinx 2 | 3 | [![CircleCI](https://circleci.com/gh/Mygod/librootkotlinx.svg?style=shield)](https://circleci.com/gh/Mygod/librootkotlinx) 4 | [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) 5 | [![Language: Kotlin](https://img.shields.io/github/languages/top/Mygod/librootkotlinx.svg)](https://github.com/Mygod/librootkotlinx/search?l=kotlin) 6 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ae00f3cc581f4222a126ffafeeb70987)](https://www.codacy.com/gh/Mygod/librootkotlinx/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Mygod/librootkotlinx&utm_campaign=Badge_Grade) 7 | [![License](https://img.shields.io/github/license/Mygod/librootkotlinx.svg)](LICENSE) 8 | 9 | Run rooted Kotlin JVM code made super easy with coroutines and parcelize! 10 | Check out demo at `app` to see just how easy it is. 11 | Also check out more complicated demos: 12 | * [PoGo+LE](https://github.com/Mygod/pogoplusle) 13 | * [VPN Hotspot](https://github.com/Mygod/VPNHotspot) (how this library started) 14 | 15 | Use it now! 16 | `be.mygod.librootkotlinx:librootkotlinx:1.0.0+` 17 | (see Releases page for latest version) 18 | 19 | ## Features 20 | 21 | * 100% Kotlin with coroutines and `Parcelize`! Easy to use and virtually no boilerplate code (no aidl) 22 | * Persistent root session that closes itself on inactive (optional and configurable) 23 | * Supports running native code (API 23+) 24 | 25 | ## Comparison with [libsu](https://github.com/topjohnwu/libsu) 26 | 27 | This project achieves morally the same thing as and ports compatibility code from libsu up to v6.0.0. 28 | With that said, there are a few differences. 29 | 30 | * librootkotlinx supports only API 21+ instead of 19+ for libsu. 31 | * librootkotlinx is 100% Kotlin and much easier to use with coroutines, 32 | whereas libsu uses AIDL which involves heavy boilerplate usages. 33 | * librootkotlinx is minimal and lightweight as additional features need to be manually enabled. 34 | * librootkotlinx is more reliable since it minimizes the amount of private APIs used (see listed below). 35 | This is possible also because it does not enable all features by default. 36 | * Out of the box, librootkotlinx is more secure since it uses Unix pipe instead of AIDL for IPC. 37 | * librootkotlinx works around not able to `exec` on certain devices running API 21-25. 38 | (See `RootServer.init#shouldRelocate` if you need this feature.) 39 | * libsu has some additional features such as remote file system. 40 | 41 | ## Private APIs used 42 | 43 | The following private platform APIs are invoked if you use `shouldRelocate = true` on API 29+. 44 | (So never under normal circumstances.) 45 | API restrictions are updated up to [SHA-256 checksum `2886a24b6382be8751e86e3c355516c448987c3b0550eb8bb906a34490cfaa3c`](https://dl.google.com/developers/android/tm/non-sdk/hiddenapi-flags.csv). 46 | 47 | * (relocated mode, API 29+) `Landroid/os/SystemProperties;->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;,sdk,system-api,test-api` 48 | * (relocated mode, API 29+) `Landroid/os/SystemProperties;->getBoolean(Ljava/lang/String;Z)Z,sdk,system-api,test-api` 49 | * (for JNI) `Ldalvik/system/BaseDexClassLoader;->pathList:Ldalvik/system/DexPathList;,unsupported` 50 | * (for JNI) `Ldalvik/system/DexPathList;->nativeLibraryDirectories:Ljava/util/List;,unsupported` 51 | * (relocated mode, API 29+) `Ldalvik/system/VMRuntime;->getCurrentInstructionSet()Ljava/lang/String;,core-platform-api,unsupported` 52 | * (relocated mode, API 29+) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported` 53 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | id("kotlin-parcelize") 5 | } 6 | 7 | android { 8 | namespace = "be.mygod.librootkotlinx.demo" 9 | compileSdk = 35 10 | defaultConfig { 11 | applicationId = "be.mygod.librootkotlinx.demo" 12 | minSdk = 21 13 | targetSdk = 35 14 | versionCode = 1 15 | versionName = "1.0" 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | externalNativeBuild.cmake.arguments += listOf("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") 18 | } 19 | val javaVersion = JavaVersion.VERSION_11 20 | compileOptions { 21 | sourceCompatibility = javaVersion 22 | targetCompatibility = javaVersion 23 | } 24 | kotlinOptions.jvmTarget = javaVersion.toString() 25 | buildTypes { 26 | release { 27 | isShrinkResources = true 28 | isMinifyEnabled = true 29 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 30 | } 31 | } 32 | buildFeatures.buildConfig = true 33 | ndkVersion = "27.0.12077973" 34 | externalNativeBuild { 35 | cmake { 36 | path = file("src/main/cpp/CMakeLists.txt") 37 | version = "3.22.1" 38 | } 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(project(":lib")) 44 | implementation("androidx.activity:activity:1.9.3") 45 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6") 46 | testImplementation("junit:junit:4.13.2") 47 | androidTestImplementation("androidx.test:runner:1.6.2") 48 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 49 | } 50 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes SourceFile,LineNumberTable 2 | -dontobfuscate 3 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project("demo") 14 | 15 | # Creates and names a library, sets it as either STATIC 16 | # or SHARED, and provides the relative paths to its source code. 17 | # You can define multiple libraries, and CMake builds them for you. 18 | # Gradle automatically packages shared libraries with your APK. 19 | # 20 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 21 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 22 | # is preferred for the same purpose. 23 | # 24 | # In order to load a library into your app from Java/Kotlin, you must call 25 | # System.loadLibrary() and pass the name of the library defined here; 26 | # for GameActivity/NativeActivity derived applications, the same library name must be 27 | # used in the AndroidManifest.xml file. 28 | add_library(${CMAKE_PROJECT_NAME} SHARED 29 | # List C/C++ source files with relative paths to this CMakeLists.txt. 30 | demo.cpp) 31 | 32 | # Specifies libraries CMake should link to your target library. You 33 | # can link libraries from various origins, such as libraries defined in this 34 | # build script, prebuilt third-party libraries, or Android system libraries. 35 | target_link_libraries(${CMAKE_PROJECT_NAME} 36 | # List libraries link to the target library 37 | android 38 | log) 39 | -------------------------------------------------------------------------------- /app/src/main/cpp/demo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern "C" 5 | JNIEXPORT jint JNICALL 6 | Java_be_mygod_librootkotlinx_demo_Jni_getuid(JNIEnv *env, jobject thiz) { 7 | return (jint) getuid(); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/be/mygod/librootkotlinx/demo/App.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx.demo 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import be.mygod.librootkotlinx.AppProcess 6 | import be.mygod.librootkotlinx.Logger 7 | import be.mygod.librootkotlinx.RootServer 8 | import be.mygod.librootkotlinx.RootSession 9 | import kotlinx.coroutines.DelicateCoroutinesApi 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import java.util.concurrent.TimeUnit 13 | 14 | class App : Application() { 15 | companion object { 16 | lateinit var rootManager: RootSession 17 | } 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | // simply initialize a global instance, this is a very lightweight operation 22 | rootManager = object : RootSession() { 23 | override val timeout get() = TimeUnit.SECONDS.toMillis(5) 24 | override suspend fun initServer(server: RootServer) { 25 | if (BuildConfig.DEBUG) Logger.me = object : Logger { 26 | override fun d(m: String?, t: Throwable?) { 27 | Log.d("RootServer", m, t) 28 | } 29 | } 30 | server.init(this@App, AppProcess.shouldRelocateHeuristics) 31 | } 32 | } 33 | Log.d("App", "init") 34 | } 35 | 36 | @OptIn(DelicateCoroutinesApi::class) 37 | override fun onTrimMemory(level: Int) { 38 | super.onTrimMemory(level) 39 | // optional: try to close existing root session when memory is running low 40 | if (level == TRIM_MEMORY_RUNNING_CRITICAL || level >= TRIM_MEMORY_BACKGROUND) GlobalScope.launch { 41 | rootManager.closeExisting() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/be/mygod/librootkotlinx/demo/Jni.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx.demo 2 | 3 | object Jni { 4 | init { 5 | System.loadLibrary("demo") 6 | } 7 | external fun getuid(): Int 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/be/mygod/librootkotlinx/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx.demo 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.Process 6 | import android.text.method.ScrollingMovementMethod 7 | import android.widget.TextView 8 | import androidx.activity.ComponentActivity 9 | import androidx.lifecycle.lifecycleScope 10 | import be.mygod.librootkotlinx.JniInit 11 | import be.mygod.librootkotlinx.ParcelableString 12 | import be.mygod.librootkotlinx.RootCommand 13 | import be.mygod.librootkotlinx.RootCommandChannel 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.channels.produce 17 | import kotlinx.coroutines.channels.toList 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.withContext 20 | import kotlinx.parcelize.Parcelize 21 | 22 | class MainActivity : ComponentActivity() { 23 | @Parcelize 24 | class SimpleTest : RootCommand { 25 | override suspend fun execute() = ParcelableString("uid: " + 26 | (if (Build.VERSION.SDK_INT >= 23) Jni.getuid() else Process.myUid()) + "\n" + 27 | withContext(Dispatchers.IO) { 28 | // try to execute a restricted subprocess command 29 | val process = ProcessBuilder("/system/bin/iptables", "-L", "INPUT").start() 30 | var output = process.inputStream.reader().readText() 31 | when (val exit = process.waitFor()) { 32 | 0 -> { } 33 | else -> output += "Process exited with $exit".toByteArray() 34 | } 35 | output 36 | }) 37 | } 38 | 39 | @Parcelize 40 | class ChannelDemo : RootCommandChannel { 41 | override fun create(scope: CoroutineScope) = scope.produce { 42 | send(ParcelableString("Hello")) 43 | send(ParcelableString("World")) 44 | } 45 | } 46 | 47 | override fun onCreate(savedInstanceState: Bundle?) { 48 | super.onCreate(savedInstanceState) 49 | setContentView(R.layout.activity_main) 50 | val text = findViewById(android.R.id.text1) 51 | text.movementMethod = ScrollingMovementMethod() 52 | lifecycleScope.launch { 53 | text.text = try { 54 | App.rootManager.use { 55 | // it is safe to call this multiple times if you don't feel like remembering in client 56 | if (Build.VERSION.SDK_INT >= 23) it.execute(JniInit()) 57 | it.execute(SimpleTest()).value + '\n' + it.create(ChannelDemo(), lifecycleScope).toList() 58 | .joinToString { it.value } 59 | } 60 | } catch (e: Exception) { 61 | e.printStackTrace() 62 | e.stackTraceToString() 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | librootkotlinx 3 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | val androidGradleVersion = "8.7.1" 3 | id("com.android.application") version androidGradleVersion apply false 4 | id("com.android.library") version androidGradleVersion apply false 5 | id("com.github.ben-manes.versions") version "0.51.0" 6 | id("org.jetbrains.kotlin.android") version "2.0.21" apply false 7 | } 8 | 9 | buildscript { 10 | dependencies { 11 | classpath("com.vanniktech:gradle-maven-publish-plugin:0.30.0") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | comments: 2 | active: false 3 | 4 | complexity: 5 | LabeledExpression: 6 | active: false 7 | TooManyFunctions: 8 | ignoreDeprecated: true 9 | ignoreOverridden: true 10 | 11 | exceptions: 12 | TooGenericExceptionCaught: 13 | active: false 14 | 15 | formatting: 16 | CommentSpacing: 17 | active: false 18 | Indentation: 19 | active: false 20 | 21 | naming: 22 | MemberNameEqualsClassName: 23 | active: false 24 | 25 | style: 26 | MagicNumber: 27 | active: false 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/librootkotlinx/108cf14da53448ef480253459d680ad6bfbd325e/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.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("com.vanniktech.maven.publish") 4 | kotlin("android") 5 | id("kotlin-parcelize") 6 | } 7 | 8 | android { 9 | namespace = "be.mygod.librootkotlinx" 10 | compileSdk = 35 11 | defaultConfig { 12 | minSdk = 21 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | val javaVersion = JavaVersion.VERSION_1_8 17 | compileOptions { 18 | sourceCompatibility = javaVersion 19 | targetCompatibility = javaVersion 20 | } 21 | kotlinOptions.jvmTarget = javaVersion.toString() 22 | } 23 | 24 | dependencies { 25 | api("androidx.collection:collection-ktx:1.2.0") 26 | api("androidx.core:core:1.10.1") 27 | api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") 28 | testImplementation("junit:junit:4.13.2") 29 | androidTestImplementation("androidx.test:runner:1.6.2") 30 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 31 | } 32 | -------------------------------------------------------------------------------- /lib/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -if public class be.mygod.librootkotlinx.RootServer { 2 | private void doInit(android.content.Context, boolean, java.lang.String, java.lang.String); 3 | } 4 | -keep class be.mygod.librootkotlinx.RootServer { 5 | public static void main(java.lang.String[]); 6 | } 7 | 8 | # Strip out debugging stuffs 9 | -assumenosideeffects class be.mygod.librootkotlinx.AppProcess { 10 | boolean hasStartupAgents*(android.content.Context) return false; 11 | } 12 | -assumenosideeffects class android.os.Debug { 13 | public static boolean isDebuggerConnected() return false; 14 | } 15 | -------------------------------------------------------------------------------- /lib/gradle.properties: -------------------------------------------------------------------------------- 1 | SONATYPE_HOST=DEFAULT 2 | RELEASE_SIGNING_ENABLED=true 3 | 4 | GROUP=be.mygod.librootkotlinx 5 | POM_ARTIFACT_ID=librootkotlinx 6 | VERSION_NAME=1.2.1 7 | 8 | POM_NAME=librootkotlinx 9 | POM_DESCRIPTION=Run rooted Kotlin JVM code made super easy with coroutines and parcelize! 10 | POM_INCEPTION_YEAR=2022 11 | POM_URL=https://github.com/Mygod/librootkotlinx 12 | 13 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 14 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 15 | POM_LICENSE_DIST=repo 16 | 17 | POM_SCM_URL=https://github.com/Mygod/librootkotlinx 18 | POM_SCM_CONNECTION=scm:git@github.com:Mygod/librootkotlinx.git 19 | POM_SCM_DEV_CONNECTION=scm:git@github.com:Mygod/librootkotlinx.git 20 | 21 | POM_DEVELOPER_ID=Mygod 22 | POM_DEVELOPER_NAME=Mygod 23 | POM_DEVELOPER_URL=https://github.com/Mygod 24 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/AppProcess.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.Debug 6 | import android.system.Os 7 | import androidx.annotation.RequiresApi 8 | import java.io.File 9 | import java.io.IOException 10 | 11 | object AppProcess { 12 | /** 13 | * Based on: https://android.googlesource.com/platform/bionic/+/aff9a34/linker/linker.cpp#3397 14 | */ 15 | @get:RequiresApi(28) 16 | val genericLdConfigFilePath: String get() { 17 | "/system/etc/ld.config.$currentInstructionSet.txt".let { if (File(it).isFile) return it } 18 | if (Build.VERSION.SDK_INT >= 30) "/linkerconfig/ld.config.txt".let { 19 | if (File(it).isFile) return it 20 | Logger.me.w("Failed to find generated linker configuration from \"$it\"") 21 | } 22 | if (isVndkLite) { 23 | "/system/etc/ld.config.vndk_lite.txt".let { if (File(it).isFile) return it } 24 | } else when (vndkVersion) { 25 | "", "current" -> { } 26 | else -> "/system/etc/ld.config.$vndkVersion.txt".let { if (File(it).isFile) return it } 27 | } 28 | return "/system/etc/ld.config.txt" 29 | } 30 | 31 | /** 32 | * Based on: https://android.googlesource.com/platform/bionic/+/30f2f05/linker/linker_config.cpp#182 33 | */ 34 | @RequiresApi(26) 35 | fun findLinkerSection(lines: Sequence, binaryRealPath: String): String { 36 | for (untrimmed in lines) { 37 | val line = untrimmed.substringBefore('#').trim() 38 | if (line.isEmpty()) continue 39 | if (line[0] == '[' && line.last() == ']') break 40 | if (line.contains("+=")) continue 41 | val chunks = line.split('=', limit = 2) 42 | if (chunks.size < 2) { 43 | Logger.me.w("warning: couldn't parse invalid format: $line (ignoring this line)") 44 | continue 45 | } 46 | var (name, value) = chunks.map { it.trim() } 47 | if (!name.startsWith("dir.")) { 48 | Logger.me.w("warning: unexpected property name \"$name\", " + 49 | "expected format dir. (ignoring this line)") 50 | continue 51 | } 52 | if (value.endsWith('/')) value = value.dropLast(1) 53 | if (value.isEmpty()) { 54 | Logger.me.w("warning: property value is empty (ignoring this line)") 55 | continue 56 | } 57 | try { 58 | value = File(value).canonicalPath 59 | } catch (e: IOException) { 60 | Logger.me.i("warning: path \"$value\" couldn't be resolved: ${e.message}") 61 | } 62 | if (binaryRealPath.startsWith(value) && binaryRealPath[value.length] == '/') return name.substring(4) 63 | } 64 | throw IllegalArgumentException("No valid linker section found") 65 | } 66 | 67 | val myExe get() = "/proc/${Os.getpid()}/exe" 68 | val myExeCanonical get() = try { 69 | File("/proc/self/exe").canonicalPath.also { require(!it.startsWith("/proc/")) { it } } 70 | } catch (e: Exception) { 71 | Logger.me.d("warning: couldn't resolve self exe", e) 72 | "/system/bin/app_process" 73 | } 74 | 75 | /** 76 | * Try to guess whether enabling relocation would work best. 77 | * It seems some Android 5-7 devices give random permission denials without relocation. 78 | * See also VPNHotspot#173. 79 | */ 80 | val shouldRelocateHeuristics get() = Build.VERSION.SDK_INT < 26 || myExeCanonical.startsWith("/data/") 81 | 82 | /** 83 | * To workaround Samsung's stupid kernel patch that prevents exec, we need to relocate exe outside of /data. 84 | * See also: https://github.com/Chainfire/librootjava/issues/19 85 | * 86 | * @return The script to be executed to perform relocation and the relocated binary path. 87 | */ 88 | fun relocateScript(token: String): Pair { 89 | val script = StringBuilder() 90 | val (baseDir, relocated) = if (Build.VERSION.SDK_INT < 29) "/dev" to "/dev/app_process_$token" else { 91 | val apexPath = "/apex/$token" 92 | script.appendLine("[ -d $apexPath ] || " + 93 | "mkdir $apexPath && " + 94 | // we need to mount a new tmpfs to override noexec flag 95 | "mount -t tmpfs -o size=1M tmpfs $apexPath || exit 1") 96 | // unfortunately native ld.config.txt only recognizes /data,/system,/system_ext as system directories; 97 | // to link correctly, we need to add our path to the linker config too 98 | val ldConfig = "$apexPath/etc/ld.config.txt" 99 | val masterLdConfig = genericLdConfigFilePath 100 | val section = try { 101 | File(masterLdConfig).useLines { findLinkerSection(it, myExeCanonical) } 102 | } catch (e: Exception) { 103 | Logger.me.w("Failed to locate system section", e) 104 | "system" 105 | } 106 | script.appendLine("[ -f $ldConfig ] || " + 107 | "mkdir -p $apexPath/etc && " + 108 | "echo dir.$section = $apexPath >$ldConfig && " + 109 | "cat $masterLdConfig >>$ldConfig || exit 1") 110 | "$apexPath/bin" to "$apexPath/bin/app_process" 111 | } 112 | script.appendLine("[ -f $relocated ] || " + 113 | "mkdir -p $baseDir && " + 114 | "cp $myExe $relocated && " + 115 | "chmod 700 $relocated || exit 1") 116 | return script to relocated 117 | } 118 | 119 | /** 120 | * Compute the shell script line that exec into the corresponding [clazz]. 121 | * Extra params can be simply appended to the string. 122 | * 123 | * See also: https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh 124 | */ 125 | fun launchString(packageCodePath: String, clazz: String, appProcess: String, niceName: String? = null): String { 126 | val debugParams = if (Debug.isDebuggerConnected()) when (Build.VERSION.SDK_INT) { 127 | in 29..Int.MAX_VALUE -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y" 128 | 28 -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable" 129 | 27 -> "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" 130 | else -> "" 131 | } else "" 132 | val extraParams = if (niceName != null) " --nice-name=$niceName" else "" 133 | // https://github.com/topjohnwu/libsu/pull/162 134 | return "CLASSPATH=$packageCodePath exec $appProcess $debugParams -Xnoimage-dex2oat /system/bin$extraParams " + 135 | clazz 136 | } 137 | 138 | /** 139 | * See also: https://github.com/topjohnwu/libsu/pull/120 140 | */ 141 | internal fun hasStartupAgents(context: Context) = Build.VERSION.SDK_INT >= 30 && 142 | File(context.codeCacheDir, "startup_agents").isDirectory 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/JniInit.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.RequiresApi 5 | import dalvik.system.BaseDexClassLoader 6 | import kotlinx.parcelize.Parcelize 7 | import java.io.File 8 | 9 | @Parcelize 10 | @RequiresApi(23) 11 | data class JniInit(private val nativeDirs: List = nativeLibraryDirs) : RootCommandNoResult { 12 | companion object { 13 | private var initialized = false 14 | @Suppress("JAVA_CLASS_ON_COMPANION") 15 | private val nativeLibraryDirs by lazy { 16 | val pathList = BaseDexClassLoader::class.java.getDeclaredField("pathList").apply { 17 | isAccessible = true 18 | }.get(javaClass.classLoader) 19 | @Suppress("UNCHECKED_CAST") 20 | pathList.javaClass.getDeclaredField("nativeLibraryDirectories").apply { 21 | isAccessible = true 22 | }.get(pathList) as ArrayList 23 | } 24 | } 25 | 26 | override suspend fun execute(): Parcelable? { 27 | if (!initialized) { 28 | nativeLibraryDirs.addAll(nativeDirs) 29 | initialized = true 30 | } 31 | return null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/Logger.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.util.Log 4 | 5 | interface Logger { 6 | companion object { 7 | /** 8 | * Override this variable to change default behavior, 9 | * which is to print to [android.util.Log] under tag "RootServer" except for [d]. 10 | */ 11 | @JvmStatic 12 | var me = object : Logger { } 13 | 14 | private const val TAG = "RootServer" 15 | } 16 | 17 | fun d(m: String?, t: Throwable? = null) { } 18 | fun e(m: String?, t: Throwable? = null) { 19 | Log.e(TAG, m, t) 20 | } 21 | fun i(m: String?, t: Throwable? = null) { 22 | Log.i(TAG, m, t) 23 | } 24 | fun w(m: String?, t: Throwable? = null) { 25 | Log.w(TAG, m, t) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/ParcelableThrowable.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.os.Parcelable 4 | import android.os.RemoteException 5 | import kotlinx.parcelize.Parcelize 6 | import java.io.ByteArrayOutputStream 7 | import java.io.NotSerializableException 8 | import java.io.ObjectInputStream 9 | import java.io.ObjectOutputStream 10 | import java.io.ObjectStreamClass 11 | 12 | /** 13 | * More robust Parcelable implementation for Throwable. 14 | */ 15 | sealed class ParcelableThrowable : Parcelable { 16 | @Parcelize 17 | internal data class Direct(val t: Parcelable) : ParcelableThrowable() { 18 | override fun unwrap(classLoader: ClassLoader?) = RemoteException().apply { initCause(t as Throwable) } 19 | } 20 | 21 | @Parcelize 22 | internal data class Serialized(val b: ByteArray) : ParcelableThrowable() { 23 | override fun unwrap(classLoader: ClassLoader?) = RemoteException().apply { 24 | initCause(parseSerializable(b, classLoader) as Throwable) 25 | } 26 | } 27 | 28 | @Parcelize 29 | internal data class Other(val s: String) : ParcelableThrowable() { 30 | override fun unwrap(classLoader: ClassLoader?) = RemoteException().apply { 31 | initCause(parseThrowable(s, classLoader)) 32 | } 33 | } 34 | 35 | abstract fun unwrap(classLoader: ClassLoader? = ParcelableThrowable::class.java.classLoader): RemoteException 36 | 37 | companion object { 38 | internal fun parseSerializable(b: ByteArray, classLoader: ClassLoader?) = 39 | object : ObjectInputStream(b.inputStream()) { 40 | override fun resolveClass(desc: ObjectStreamClass) = try { 41 | Class.forName(desc.name, false, classLoader) 42 | } catch (e: ClassNotFoundException) { 43 | Class.forName(desc.name, false, ParcelableThrowable::class.java.classLoader) 44 | } 45 | }.readObject() 46 | 47 | private fun initException(targetClass: Class<*>, message: String): Throwable { 48 | @Suppress("NAME_SHADOWING") 49 | var targetClass = targetClass 50 | while (true) { 51 | try { 52 | // try to find a message constructor 53 | return targetClass.getDeclaredConstructor(String::class.java).newInstance(message) as Throwable 54 | } catch (_: ReflectiveOperationException) { } 55 | targetClass = targetClass.superclass 56 | } 57 | } 58 | internal fun parseThrowable(s: String, classLoader: ClassLoader?): Throwable { 59 | val name = s.split(':', limit = 2)[0] 60 | return initException(try { 61 | classLoader?.loadClass(name) 62 | } catch (_: ClassNotFoundException) { 63 | null 64 | } ?: Class.forName(name), s) 65 | } 66 | } 67 | } 68 | 69 | fun ParcelableThrowable(t: Throwable) = if (t is Parcelable) ParcelableThrowable.Direct(t) else try { 70 | ParcelableThrowable.Serialized(ByteArrayOutputStream().apply { 71 | ObjectOutputStream(this).use { it.writeObject(t) } 72 | }.toByteArray()) 73 | } catch (_: NotSerializableException) { 74 | ParcelableThrowable.Other(t.stackTraceToString()) 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/RootServer.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.Looper 6 | import android.os.Parcelable 7 | import android.os.RemoteException 8 | import android.system.ErrnoException 9 | import android.system.Os 10 | import android.system.OsConstants 11 | import androidx.collection.LongSparseArray 12 | import androidx.collection.valueIterator 13 | import kotlinx.coroutines.CancellationException 14 | import kotlinx.coroutines.CompletableDeferred 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Deferred 17 | import kotlinx.coroutines.DelicateCoroutinesApi 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.ExperimentalCoroutinesApi 20 | import kotlinx.coroutines.GlobalScope 21 | import kotlinx.coroutines.Job 22 | import kotlinx.coroutines.NonCancellable 23 | import kotlinx.coroutines.SupervisorJob 24 | import kotlinx.coroutines.TimeoutCancellationException 25 | import kotlinx.coroutines.async 26 | import kotlinx.coroutines.channels.Channel 27 | import kotlinx.coroutines.channels.ClosedSendChannelException 28 | import kotlinx.coroutines.channels.SendChannel 29 | import kotlinx.coroutines.channels.consumeEach 30 | import kotlinx.coroutines.channels.onClosed 31 | import kotlinx.coroutines.channels.onFailure 32 | import kotlinx.coroutines.channels.produce 33 | import kotlinx.coroutines.coroutineScope 34 | import kotlinx.coroutines.launch 35 | import kotlinx.coroutines.runBlocking 36 | import kotlinx.coroutines.withContext 37 | import kotlinx.coroutines.withTimeout 38 | import kotlinx.coroutines.withTimeoutOrNull 39 | import java.io.BufferedReader 40 | import java.io.ByteArrayOutputStream 41 | import java.io.DataInputStream 42 | import java.io.DataOutputStream 43 | import java.io.EOFException 44 | import java.io.File 45 | import java.io.FileDescriptor 46 | import java.io.FileNotFoundException 47 | import java.io.FileOutputStream 48 | import java.io.IOException 49 | import java.io.NotSerializableException 50 | import java.io.ObjectOutputStream 51 | import java.util.UUID 52 | import java.util.concurrent.CountDownLatch 53 | import kotlin.system.exitProcess 54 | 55 | class RootServer { 56 | private sealed class Callback(private val server: RootServer, private val index: Long, 57 | protected val classLoader: ClassLoader?) { 58 | var active = true 59 | 60 | abstract fun cancel(e: CancellationException? = null) 61 | abstract fun shouldRemove(result: Byte): Boolean 62 | abstract operator fun invoke(input: DataInputStream, result: Byte) 63 | fun sendClosed() = server.execute(CancelCommand(index)) 64 | 65 | private fun makeRemoteException(cause: Throwable) = 66 | if (cause is CancellationException) cause else RemoteException().initCause(cause) 67 | protected fun DataInputStream.readException(result: Byte) = when (result.toInt()) { 68 | EX_GENERIC -> makeRemoteException(ParcelableThrowable.parseThrowable(readUTF(), classLoader)) 69 | EX_PARCELABLE -> makeRemoteException(readParcelable(classLoader) as Throwable) 70 | EX_SERIALIZABLE -> makeRemoteException(ParcelableThrowable.parseSerializable(readByteArray(), classLoader) 71 | as Throwable) 72 | else -> throw IllegalArgumentException("Unexpected result $result") 73 | } 74 | 75 | class Ordinary(server: RootServer, index: Long, classLoader: ClassLoader?, 76 | private val callback: CompletableDeferred) : Callback(server, index, classLoader) { 77 | override fun cancel(e: CancellationException?) = callback.cancel(e) 78 | override fun shouldRemove(result: Byte) = true 79 | override fun invoke(input: DataInputStream, result: Byte) { 80 | if (result.toInt() == SUCCESS) callback.complete(input.readParcelable(classLoader)) 81 | else callback.completeExceptionally(input.readException(result)) 82 | } 83 | } 84 | 85 | class Channel(server: RootServer, index: Long, classLoader: ClassLoader?, 86 | private val channel: SendChannel) : Callback(server, index, classLoader) { 87 | val finish: CompletableDeferred = CompletableDeferred() 88 | override fun cancel(e: CancellationException?) = finish.cancel(e) 89 | override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS 90 | override fun invoke(input: DataInputStream, result: Byte) { 91 | when (result.toInt()) { 92 | SUCCESS -> channel.trySend(input.readParcelable(classLoader)).onClosed { 93 | active = false 94 | sendClosed() 95 | finish.completeExceptionally(it 96 | ?: ClosedSendChannelException("Channel was closed normally")) 97 | return 98 | }.onFailure { throw it!! } // the channel we are supporting should never block 99 | CHANNEL_CONSUMED -> finish.complete(Unit) 100 | else -> finish.completeExceptionally(input.readException(result)) 101 | } 102 | } 103 | } 104 | } 105 | 106 | class LaunchException(cause: Throwable) : RuntimeException("Failed to launch root daemon", cause) 107 | class UnexpectedExitException : RemoteException("Root process exited unexpectedly") 108 | 109 | private lateinit var process: Process 110 | /** 111 | * Thread safety: needs to be protected by callbackLookup. 112 | */ 113 | private lateinit var output: DataOutputStream 114 | 115 | @Volatile 116 | var active = false 117 | private var counter = 0L 118 | private var callbackListenerExit: Deferred? = null 119 | private val callbackLookup = LongSparseArray() 120 | 121 | private fun readUnexpectedStderr(): String? { 122 | if (!this::process.isInitialized) return null 123 | Logger.me.d("Attempting to read stderr") 124 | var available = process.errorStream.available() 125 | return if (available <= 0) null else String(ByteArrayOutputStream().apply { 126 | try { 127 | while (available > 0) { 128 | val bytes = ByteArray(available) 129 | val len = process.errorStream.read(bytes) 130 | if (len < 0) throw EOFException() // should not happen 131 | write(bytes, 0, len) 132 | available = process.errorStream.available() 133 | } 134 | } catch (e: IOException) { 135 | Logger.me.w("Reading stderr was cut short", e) 136 | } 137 | }.toByteArray()) 138 | } 139 | 140 | private fun BufferedReader.lookForToken(token: String) { 141 | while (true) { 142 | val line = readLine() ?: throw EOFException() 143 | if (line.endsWith(token)) { 144 | val extraLength = line.length - token.length 145 | if (extraLength > 0) Logger.me.w(line.substring(0, extraLength)) 146 | break 147 | } 148 | Logger.me.w(line) 149 | } 150 | } 151 | private fun doInit(context: Context, shouldRelocate: Boolean, niceName: String, 152 | appProcess: String = AppProcess.myExe) { 153 | try { 154 | val (reader, writer) = try { 155 | process = ProcessBuilder("su").start() 156 | val token1 = UUID.randomUUID().toString() 157 | val writer = DataOutputStream(process.outputStream.buffered()) 158 | writer.writeBytes("echo $token1\n") 159 | writer.flush() 160 | val reader = process.inputStream.bufferedReader() 161 | reader.lookForToken(token1) 162 | Logger.me.d("Root shell initialized") 163 | reader to writer 164 | } catch (e: Exception) { 165 | throw NoShellException(e) 166 | } 167 | try { 168 | val token2 = UUID.randomUUID().toString() 169 | writer.writeBytes(if (shouldRelocate) { 170 | val persistence = File((if (Build.VERSION.SDK_INT >= 24) { 171 | context.createDeviceProtectedStorageContext() 172 | } else context).codeCacheDir, ".librootkotlinx-uuid") 173 | val uuid = context.packageName + '@' + try { 174 | persistence.readText() 175 | } catch (_: FileNotFoundException) { 176 | UUID.randomUUID().toString().also { persistence.writeText(it) } 177 | } 178 | val (script, relocated) = AppProcess.relocateScript(uuid) 179 | script.appendLine(AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, 180 | relocated, niceName) + " $token2") 181 | script.toString() 182 | } else { 183 | AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, appProcess, 184 | niceName) + " $token2\n" 185 | }) 186 | writer.flush() 187 | reader.lookForToken(token2) // wait for ready signal 188 | } catch (e: Exception) { 189 | if (appProcess == AppProcess.myExe && e is EOFException) try { 190 | doInit(context, shouldRelocate, niceName, AppProcess.myExeCanonical) 191 | Logger.me.d("Launched from fallback mode", e) 192 | return 193 | } catch (e2: Exception) { 194 | throw e2.apply { addSuppressed(e) } 195 | } 196 | throw LaunchException(e) 197 | } 198 | output = writer 199 | require(!active) 200 | active = true 201 | Logger.me.d("Root server initialized") 202 | } finally { 203 | try { 204 | readUnexpectedStderr()?.let { Logger.me.e(it) } 205 | } catch (e: IOException) { 206 | Logger.me.e("Failed to read from stderr", e) // avoid the real exception being swallowed 207 | } 208 | } 209 | } 210 | 211 | private fun callbackSpin() { 212 | val input = DataInputStream(process.inputStream.buffered()) 213 | while (active) { 214 | val index = try { 215 | input.readLong() 216 | } catch (_: EOFException) { 217 | break 218 | } 219 | val result = input.readByte() 220 | val callback = synchronized(callbackLookup) { 221 | if (active) (callbackLookup[index] ?: error("Empty callback #$index")).also { 222 | if (it.shouldRemove(result)) { 223 | callbackLookup.remove(index) 224 | it.active = false 225 | } 226 | } else null 227 | } ?: break 228 | try { 229 | Logger.me.d("Received callback #$index: $result") 230 | callback(input, result) 231 | } catch (e: Throwable) { 232 | callback.cancel(if (e is CancellationException) e else CancellationException().apply { initCause(e) }) 233 | throw e 234 | } 235 | } 236 | } 237 | 238 | /** 239 | * Initialize a RootServer synchronously, can throw a lot of exceptions. 240 | * 241 | * @param context Any [Context] from the app. 242 | * @param shouldRelocate Whether app process should be copied first. See also [AppProcess.shouldRelocateHeuristics]. 243 | * @param niceName Name to call the rooted Java process. 244 | */ 245 | @OptIn(DelicateCoroutinesApi::class) 246 | suspend fun init(context: Context, shouldRelocate: Boolean = false, 247 | niceName: String = "${context.packageName}:root") { 248 | if (AppProcess.hasStartupAgents(context)) Logger.me.w("JVMTI agent is enabled. Please enable the " + 249 | "'Always install with package manager' option in Android Studio.") 250 | withContext(Dispatchers.IO) { doInit(context, shouldRelocate, niceName) } 251 | callbackListenerExit = GlobalScope.async(Dispatchers.IO) { 252 | val errorReader = async(Dispatchers.IO) { 253 | try { 254 | process.errorStream.bufferedReader().forEachLine(Logger.me::w) 255 | } catch (_: IOException) { } 256 | } 257 | var cause: Throwable? = null 258 | try { 259 | callbackSpin() 260 | if (active) throw UnexpectedExitException() 261 | } catch (e: Throwable) { 262 | cause = e 263 | Logger.me.d("Shutting down from worker due to error", e) 264 | process.destroy() 265 | if (e !is EOFException) throw e 266 | } finally { 267 | Logger.me.d("Waiting for exit") 268 | withContext(NonCancellable) { errorReader.await() } 269 | process.waitFor() 270 | closeInternal(cause) 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * Caller should check for active. 277 | */ 278 | private fun sendLocked(command: Parcelable) { 279 | try { 280 | output.writeParcelable(command) 281 | output.flush() 282 | } catch (e: IOException) { 283 | if (e.isEBADF || (e.cause as? ErrnoException)?.errno == OsConstants.EPIPE) { 284 | throw CancellationException().initCause(e) 285 | } else throw e 286 | } 287 | Logger.me.d("Sent #$counter: $command") 288 | counter++ 289 | } 290 | 291 | fun execute(command: RootCommandOneWay) = synchronized(callbackLookup) { if (active) sendLocked(command) } 292 | @Throws(RemoteException::class) 293 | suspend inline fun > execute(command: C) = 294 | execute(command, C::class.java.classLoader) 295 | @Throws(RemoteException::class) 296 | suspend fun execute(command: RootCommand, classLoader: ClassLoader?): T { 297 | val future = CompletableDeferred() 298 | val callback = synchronized(callbackLookup) { 299 | @Suppress("UNCHECKED_CAST") 300 | val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred) 301 | if (active) { 302 | callbackLookup.append(counter, callback) 303 | sendLocked(command) 304 | } else future.cancel() 305 | callback 306 | } 307 | try { 308 | return future.await() 309 | } finally { 310 | if (callback.active) callback.sendClosed() 311 | callback.active = false 312 | } 313 | } 314 | 315 | @ExperimentalCoroutinesApi 316 | @Throws(RemoteException::class) 317 | inline fun > create(command: C, scope: CoroutineScope) = 318 | create(command, scope, C::class.java.classLoader) 319 | @ExperimentalCoroutinesApi 320 | @Throws(RemoteException::class) 321 | fun create(command: RootCommandChannel, scope: CoroutineScope, 322 | classLoader: ClassLoader?) = scope.produce( 323 | SupervisorJob(), command.capacity.also { 324 | when (it) { 325 | Channel.UNLIMITED, Channel.CONFLATED -> { } 326 | else -> throw IllegalArgumentException("Unsupported channel capacity $it") 327 | } 328 | }) { 329 | val callback = synchronized(callbackLookup) { 330 | @Suppress("UNCHECKED_CAST") 331 | val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel) 332 | if (active) { 333 | callbackLookup.append(counter, callback) 334 | sendLocked(command) 335 | } else callback.finish.cancel() 336 | callback 337 | } 338 | try { 339 | callback.finish.await() 340 | } finally { 341 | if (callback.active) callback.sendClosed() 342 | callback.active = false 343 | } 344 | } 345 | 346 | private fun closeInternal(cause: Throwable? = null) = synchronized(callbackLookup) { 347 | if (active) { 348 | active = false 349 | try { 350 | sendLocked(Shutdown()) 351 | output.close() 352 | process.outputStream.close() 353 | } catch (_: CancellationException) { 354 | } catch (e: IOException) { 355 | Logger.me.w("send Shutdown failed", e) 356 | } 357 | Logger.me.d("Client closed") 358 | } 359 | for (callback in callbackLookup.valueIterator()) callback.cancel( 360 | if (cause is CancellationException) cause else CancellationException().apply { initCause(cause) }) 361 | callbackLookup.clear() 362 | } 363 | /** 364 | * Shutdown the instance gracefully. 365 | */ 366 | suspend fun close() { 367 | Logger.me.d("Shutting down from client") 368 | closeInternal() 369 | val callbackListenerExit = callbackListenerExit ?: return 370 | try { 371 | withTimeout(10000) { callbackListenerExit.await() } 372 | } catch (e: TimeoutCancellationException) { 373 | Logger.me.w("Closing the instance has timed out", e) 374 | if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly() 375 | } catch (e: UnexpectedExitException) { 376 | Logger.me.w(e.message) 377 | } 378 | } 379 | 380 | companion object { 381 | private const val SUCCESS = 0 382 | private const val EX_GENERIC = 1 383 | private const val EX_PARCELABLE = 2 384 | private const val EX_SERIALIZABLE = 4 385 | private const val CHANNEL_CONSUMED = 3 386 | 387 | private fun DataInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) } 388 | 389 | private inline fun DataInputStream.readParcelable(classLoader: ClassLoader?) = 390 | readByteArray().toParcelable(classLoader) 391 | private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) { 392 | val bytes = data.toByteArray(parcelableFlags) 393 | writeInt(bytes.size) 394 | write(bytes) 395 | } 396 | 397 | @JvmStatic 398 | fun main(args: Array) { 399 | Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> 400 | Logger.me.e("Uncaught exception from $thread", throwable) 401 | throwable.printStackTrace() // stderr will be read by listener 402 | exitProcess(1) 403 | } 404 | rootMain(args) 405 | exitProcess(0) // there might be other non-daemon threads 406 | } 407 | 408 | private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) { 409 | writeLong(callback) 410 | if (e is Parcelable) { 411 | writeByte(EX_PARCELABLE) 412 | writeParcelable(e) 413 | } else try { 414 | val bytes = ByteArrayOutputStream().apply { 415 | ObjectOutputStream(this).use { it.writeObject(e) } 416 | }.toByteArray() 417 | writeByte(EX_SERIALIZABLE) 418 | writeInt(bytes.size) 419 | write(bytes) 420 | } catch (_: NotSerializableException) { 421 | writeByte(EX_GENERIC) 422 | writeUTF(e.stackTraceToString()) 423 | } 424 | flush() 425 | } 426 | private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) { 427 | writeLong(callback) 428 | writeByte(SUCCESS) 429 | writeParcelable(result) 430 | flush() 431 | } 432 | 433 | private fun rootMain(args: Array) { 434 | require(args.isNotEmpty()) 435 | val mainInitialized = CountDownLatch(1) 436 | val main = Thread({ 437 | @Suppress("DEPRECATION") 438 | Looper.prepareMainLooper() 439 | mainInitialized.countDown() 440 | Looper.loop() 441 | }, "main") 442 | main.start() 443 | val job = Job() 444 | val defaultWorker by lazy { 445 | mainInitialized.await() 446 | CoroutineScope(Dispatchers.Main.immediate + job) 447 | } 448 | val callbackWorker by lazy { 449 | mainInitialized.await() 450 | Dispatchers.IO.limitedParallelism(1, "callbackWorker") 451 | } 452 | // access to cancellables shall be wrapped in defaultWorker 453 | val cancellables = LongSparseArray<() -> Unit>() 454 | 455 | // thread safety: usage of output should be guarded by callbackWorker 456 | val output = DataOutputStream(FileOutputStream(Os.dup(FileDescriptor.out)).buffered().apply { 457 | // prevent future write attempts to System.out, possibly from Samsung changes (again) 458 | Os.dup2(FileDescriptor.err, OsConstants.STDOUT_FILENO) 459 | System.setOut(System.err) 460 | val writer = writer() 461 | writer.appendLine(args[0]) // echo ready signal 462 | writer.flush() 463 | }) 464 | // thread safety: usage of input should be in main thread 465 | val input = DataInputStream(System.`in`.buffered()) 466 | var counter = 0L 467 | Logger.me.d("Server entering main loop") 468 | loop@ while (true) { 469 | val command = try { 470 | input.readParcelable(RootServer::class.java.classLoader) 471 | } catch (_: EOFException) { 472 | break 473 | } 474 | val callback = counter 475 | Logger.me.d("Received #$callback: $command") 476 | when (command) { 477 | is CancelCommand -> defaultWorker.launch { cancellables[command.index]?.invoke() } 478 | is RootCommandOneWay -> defaultWorker.launch { 479 | try { 480 | command.execute() 481 | } catch (e: Throwable) { 482 | Logger.me.e("Unexpected exception in RootCommandOneWay ($command.javaClass.simpleName)", e) 483 | } 484 | } 485 | is RootCommand<*> -> { 486 | val commandJob = Job() 487 | defaultWorker.launch(commandJob) { 488 | cancellables.append(callback) { commandJob.cancel() } 489 | val result = try { 490 | val result = command.execute(); 491 | { output.pushResult(callback, result) } 492 | } catch (e: Throwable) { 493 | val worker = { output.pushThrowable(callback, e) } 494 | worker 495 | } finally { 496 | cancellables.remove(callback) 497 | } 498 | withContext(callbackWorker + NonCancellable) { result() } 499 | } 500 | } 501 | is RootCommandChannel<*> -> defaultWorker.launch { 502 | val result = try { 503 | coroutineScope { 504 | command.create(this).also { 505 | cancellables.append(callback) { it.cancel() } 506 | }.consumeEach { result -> 507 | withContext(callbackWorker) { output.pushResult(callback, result) } 508 | } 509 | }; 510 | @Suppress("BlockingMethodInNonBlockingContext") { 511 | output.writeLong(callback) 512 | output.writeByte(CHANNEL_CONSUMED) 513 | output.flush() 514 | } 515 | } catch (e: Throwable) { 516 | val worker = { output.pushThrowable(callback, e) } 517 | worker 518 | } finally { 519 | cancellables.remove(callback) 520 | } 521 | withContext(callbackWorker + NonCancellable) { result() } 522 | } 523 | is Shutdown -> break@loop 524 | else -> throw IllegalArgumentException("Unrecognized input: $command") 525 | } 526 | counter++ 527 | } 528 | job.cancel() 529 | Logger.me.d("Clean up initiated before exit. Jobs: ${job.children.joinToString()}") 530 | if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) { 531 | Logger.me.w("Clean up timeout: ${job.children.joinToString()}") 532 | } else Logger.me.d("Clean up finished, exiting") 533 | } 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/RootSession.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import java.util.concurrent.TimeUnit 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes. 11 | */ 12 | abstract class RootSession { 13 | protected abstract suspend fun initServer(server: RootServer) 14 | /** 15 | * Timeout to close [RootServer] in milliseconds. 16 | */ 17 | protected open val timeout get() = TimeUnit.MINUTES.toMillis(5) 18 | protected open val timeoutContext: CoroutineContext get() = Dispatchers.Default 19 | 20 | private val mutex = Mutex() 21 | private var server: RootServer? = null 22 | private var timeoutJob: Job? = null 23 | private var usersCount = 0L 24 | private var closePending = false 25 | 26 | private suspend fun ensureServerLocked(): RootServer { 27 | server?.let { 28 | if (it.active) return it 29 | usersCount = 0 30 | closeLocked() 31 | } 32 | check(usersCount == 0L) { "Unexpected $server, $usersCount" } 33 | val server = RootServer() 34 | try { 35 | initServer(server) 36 | this.server = server 37 | return server 38 | } catch (e: Throwable) { 39 | try { 40 | server.close() 41 | } catch (eClose: Throwable) { 42 | e.addSuppressed(eClose) 43 | } 44 | throw e 45 | } 46 | } 47 | 48 | private suspend fun closeLocked() { 49 | closePending = false 50 | val server = server 51 | this.server = null 52 | server?.close() 53 | } 54 | @OptIn(DelicateCoroutinesApi::class) 55 | private fun startTimeoutLocked() { 56 | check(timeoutJob == null) 57 | timeoutJob = GlobalScope.launch(timeoutContext, CoroutineStart.UNDISPATCHED) { 58 | delay(timeout) 59 | mutex.withLock { 60 | ensureActive() 61 | check(usersCount == 0L) 62 | timeoutJob = null 63 | closeLocked() 64 | } 65 | } 66 | } 67 | private fun haltTimeoutLocked() { 68 | timeoutJob?.cancel() 69 | timeoutJob = null 70 | } 71 | 72 | suspend fun acquire() = withContext(NonCancellable) { 73 | mutex.withLock { 74 | haltTimeoutLocked() 75 | closePending = false 76 | ensureServerLocked().also { ++usersCount } 77 | } 78 | } 79 | suspend fun release(server: RootServer) = withContext(NonCancellable) { 80 | mutex.withLock { 81 | if (this@RootSession.server != server) return@withLock // outdated reference 82 | require(usersCount > 0) 83 | when { 84 | !server.active -> { 85 | usersCount = 0 86 | closeLocked() 87 | return@withLock 88 | } 89 | --usersCount > 0L -> return@withLock 90 | closePending -> closeLocked() 91 | else -> startTimeoutLocked() 92 | } 93 | } 94 | } 95 | suspend inline fun use(block: (RootServer) -> T): T { 96 | val server = acquire() 97 | try { 98 | return block(server) 99 | } finally { 100 | release(server) 101 | } 102 | } 103 | 104 | suspend fun closeExisting() = mutex.withLock { 105 | if (usersCount > 0) closePending = true else { 106 | haltTimeoutLocked() 107 | closeLocked() 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.librootkotlinx 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.MainThread 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.channels.ReceiveChannel 8 | import kotlinx.parcelize.Parcelize 9 | 10 | interface RootCommand : Parcelable { 11 | /** 12 | * If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable]. 13 | */ 14 | @MainThread 15 | suspend fun execute(): Result 16 | } 17 | 18 | typealias RootCommandNoResult = RootCommand 19 | 20 | /** 21 | * Execute a command and discards its result, even if an exception occurs. 22 | * 23 | * If you want to catch exception, use e.g. [RootCommandNoResult] and return null. 24 | */ 25 | interface RootCommandOneWay : Parcelable { 26 | @MainThread 27 | suspend fun execute() 28 | } 29 | 30 | interface RootCommandChannel : Parcelable { 31 | /** 32 | * The capacity of the channel that is returned by [create] to be used by client. 33 | * Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection. 34 | */ 35 | val capacity: Int get() = Channel.UNLIMITED 36 | 37 | @MainThread 38 | fun create(scope: CoroutineScope): ReceiveChannel 39 | } 40 | 41 | @Parcelize 42 | internal data class CancelCommand(val index: Long) : RootCommandOneWay { 43 | override suspend fun execute() = error("Internal implementation") 44 | } 45 | 46 | @Parcelize 47 | internal class Shutdown : Parcelable 48 | -------------------------------------------------------------------------------- /lib/src/main/java/be/mygod/librootkotlinx/Utils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Utils") 2 | 3 | package be.mygod.librootkotlinx 4 | 5 | import android.annotation.SuppressLint 6 | import android.content.Context 7 | import android.content.res.Resources 8 | import android.os.Parcel 9 | import android.os.Parcelable 10 | import android.system.ErrnoException 11 | import android.system.OsConstants 12 | import android.util.Size 13 | import android.util.SizeF 14 | import android.util.SparseBooleanArray 15 | import android.util.SparseIntArray 16 | import android.util.SparseLongArray 17 | import androidx.annotation.RequiresApi 18 | import androidx.core.os.ParcelCompat 19 | import kotlinx.parcelize.Parcelize 20 | import java.io.IOException 21 | import java.util.Locale 22 | 23 | class NoShellException(cause: Throwable) : Exception("Root missing", cause) 24 | 25 | internal val currentInstructionSet by lazy { 26 | val classVMRuntime = Class.forName("dalvik.system.VMRuntime") 27 | val runtime = classVMRuntime.getDeclaredMethod("getRuntime").invoke(null) 28 | classVMRuntime.getDeclaredMethod("getCurrentInstructionSet").invoke(runtime) as String 29 | } 30 | 31 | private val classSystemProperties by lazy { Class.forName("android.os.SystemProperties") } 32 | @get:RequiresApi(26) 33 | internal val isVndkLite by lazy { 34 | classSystemProperties.getDeclaredMethod("getBoolean", String::class.java, Boolean::class.java).invoke(null, 35 | "ro.vndk.lite", false) as Boolean 36 | } 37 | @get:RequiresApi(26) 38 | internal val vndkVersion by lazy { 39 | classSystemProperties.getDeclaredMethod("get", String::class.java, String::class.java).invoke(null, 40 | "ro.vndk.version", "") as String 41 | } 42 | 43 | /** 44 | * Calling many system APIs can crash on some LG ROMs. Override the system resources object to prevent crashing. 45 | * https://github.com/topjohnwu/libsu/blob/78c60dcecb9ac2047704324e161659a2ddb0f034/service/src/main/java/com/topjohnwu/superuser/internal/RootServerMain.java#L165 46 | */ 47 | private val patchLgeIfNeeded by lazy { 48 | try { 49 | // This class only exists on LG ROMs with broken implementations 50 | Class.forName("com.lge.systemservice.core.integrity.IntegrityManager") 51 | } catch (_: ClassNotFoundException) { 52 | return@lazy 53 | } 54 | try { 55 | // If control flow goes here, we need the resource hack 56 | val res = Resources.getSystem() 57 | Resources::class.java.getDeclaredField("mSystem").apply { 58 | isAccessible = true 59 | }.set(null, object : Resources(res.assets, res.displayMetrics, res.configuration) { 60 | init { 61 | val getImpl = Resources::class.java.getDeclaredMethod("getImpl").apply { isAccessible = true } 62 | Resources::class.java.getDeclaredMethod("setImpl", getImpl.returnType).apply { 63 | isAccessible = true 64 | }(this, getImpl(res)) 65 | } 66 | 67 | override fun getBoolean(id: Int) = try { 68 | super.getBoolean(id) 69 | } catch (e: NotFoundException) { 70 | false 71 | } 72 | }) 73 | } catch (e: ReflectiveOperationException) { 74 | Logger.me.w("Failed to patch system resources", e) 75 | } 76 | } 77 | val systemContext by lazy { 78 | patchLgeIfNeeded 79 | val classActivityThread = Class.forName("android.app.ActivityThread") 80 | val activityThread = classActivityThread.getMethod("systemMain").invoke(null) 81 | classActivityThread.getMethod("getSystemContext").invoke(activityThread) as Context 82 | } 83 | 84 | @Parcelize 85 | data class ParcelableByte(val value: Byte) : Parcelable 86 | 87 | @Parcelize 88 | data class ParcelableShort(val value: Short) : Parcelable 89 | 90 | @Parcelize 91 | data class ParcelableInt(val value: Int) : Parcelable 92 | 93 | @Parcelize 94 | data class ParcelableLong(val value: Long) : Parcelable 95 | 96 | @Parcelize 97 | data class ParcelableFloat(val value: Float) : Parcelable 98 | 99 | @Parcelize 100 | data class ParcelableDouble(val value: Double) : Parcelable 101 | 102 | @Parcelize 103 | data class ParcelableBoolean(val value: Boolean) : Parcelable 104 | 105 | @Parcelize 106 | data class ParcelableString(val value: String) : Parcelable 107 | 108 | @Parcelize 109 | data class ParcelableByteArray(val value: ByteArray) : Parcelable { 110 | override fun equals(other: Any?): Boolean { 111 | if (this === other) return true 112 | if (javaClass != other?.javaClass) return false 113 | 114 | other as ParcelableByteArray 115 | 116 | if (!value.contentEquals(other.value)) return false 117 | 118 | return true 119 | } 120 | 121 | override fun hashCode(): Int { 122 | return value.contentHashCode() 123 | } 124 | } 125 | 126 | @Parcelize 127 | data class ParcelableIntArray(val value: IntArray) : Parcelable { 128 | override fun equals(other: Any?): Boolean { 129 | if (this === other) return true 130 | if (javaClass != other?.javaClass) return false 131 | 132 | other as ParcelableIntArray 133 | 134 | if (!value.contentEquals(other.value)) return false 135 | 136 | return true 137 | } 138 | 139 | override fun hashCode(): Int { 140 | return value.contentHashCode() 141 | } 142 | } 143 | 144 | @Parcelize 145 | data class ParcelableLongArray(val value: LongArray) : Parcelable { 146 | override fun equals(other: Any?): Boolean { 147 | if (this === other) return true 148 | if (javaClass != other?.javaClass) return false 149 | 150 | other as ParcelableLongArray 151 | 152 | if (!value.contentEquals(other.value)) return false 153 | 154 | return true 155 | } 156 | 157 | override fun hashCode(): Int { 158 | return value.contentHashCode() 159 | } 160 | } 161 | 162 | @Parcelize 163 | data class ParcelableFloatArray(val value: FloatArray) : Parcelable { 164 | override fun equals(other: Any?): Boolean { 165 | if (this === other) return true 166 | if (javaClass != other?.javaClass) return false 167 | 168 | other as ParcelableFloatArray 169 | 170 | if (!value.contentEquals(other.value)) return false 171 | 172 | return true 173 | } 174 | 175 | override fun hashCode(): Int { 176 | return value.contentHashCode() 177 | } 178 | } 179 | 180 | @Parcelize 181 | data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable { 182 | override fun equals(other: Any?): Boolean { 183 | if (this === other) return true 184 | if (javaClass != other?.javaClass) return false 185 | 186 | other as ParcelableDoubleArray 187 | 188 | if (!value.contentEquals(other.value)) return false 189 | 190 | return true 191 | } 192 | 193 | override fun hashCode(): Int { 194 | return value.contentHashCode() 195 | } 196 | } 197 | 198 | @Parcelize 199 | data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable { 200 | override fun equals(other: Any?): Boolean { 201 | if (this === other) return true 202 | if (javaClass != other?.javaClass) return false 203 | 204 | other as ParcelableBooleanArray 205 | 206 | if (!value.contentEquals(other.value)) return false 207 | 208 | return true 209 | } 210 | 211 | override fun hashCode(): Int { 212 | return value.contentHashCode() 213 | } 214 | } 215 | 216 | @Parcelize 217 | data class ParcelableStringArray(val value: Array) : Parcelable { 218 | override fun equals(other: Any?): Boolean { 219 | if (this === other) return true 220 | if (javaClass != other?.javaClass) return false 221 | 222 | other as ParcelableStringArray 223 | 224 | if (!value.contentEquals(other.value)) return false 225 | 226 | return true 227 | } 228 | 229 | override fun hashCode(): Int { 230 | return value.contentHashCode() 231 | } 232 | } 233 | 234 | @Parcelize 235 | data class ParcelableStringList(val value: List) : Parcelable 236 | 237 | @Parcelize 238 | data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable 239 | 240 | @Parcelize 241 | data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable 242 | 243 | @Parcelize 244 | data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable 245 | 246 | @Parcelize 247 | data class ParcelableCharSequence(val value: CharSequence) : Parcelable 248 | 249 | @Parcelize 250 | data class ParcelableSize(val value: Size) : Parcelable 251 | 252 | @Parcelize 253 | data class ParcelableSizeF(val value: SizeF) : Parcelable 254 | 255 | @Parcelize 256 | data class ParcelableArray(val value: Array) : Parcelable { 257 | override fun equals(other: Any?): Boolean { 258 | if (this === other) return true 259 | if (javaClass != other?.javaClass) return false 260 | 261 | other as ParcelableArray 262 | 263 | if (!value.contentEquals(other.value)) return false 264 | 265 | return true 266 | } 267 | 268 | override fun hashCode(): Int { 269 | return value.contentHashCode() 270 | } 271 | } 272 | 273 | @Parcelize 274 | data class ParcelableList(val value: List) : Parcelable 275 | 276 | @SuppressLint("Recycle") 277 | inline fun useParcel(block: (Parcel) -> T) = Parcel.obtain().run { 278 | try { 279 | block(this) 280 | } finally { 281 | recycle() 282 | } 283 | } 284 | 285 | fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p -> 286 | p.writeParcelable(this, parcelableFlags) 287 | p.marshall() 288 | } 289 | inline fun ByteArray.toParcelable(classLoader: ClassLoader?) = useParcel { p -> 290 | p.unmarshall(this, 0, size) 291 | p.setDataPosition(0) 292 | ParcelCompat.readParcelable(p, classLoader, T::class.java) 293 | } 294 | 295 | // Stream closed caused in NullOutputStream 296 | val IOException.isEBADF get() = (cause as? ErrnoException)?.errno == OsConstants.EBADF || 297 | message?.lowercase(Locale.ENGLISH) == "stream closed" 298 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | include(":app", ":lib") 16 | --------------------------------------------------------------------------------