Repository: RikkaApps/Shizuku Branch: master Commit: b844bc491f17 Files: 287 Total size: 841.3 KB Directory structure: gitextract_t0twfram/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── config.yml │ └── workflows/ │ └── app.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.gradle ├── common/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── moe/ │ └── shizuku/ │ └── common/ │ └── util/ │ ├── BuildUtils.java │ └── OsUtils.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── manager/ │ ├── .gitignore │ ├── aapt2-resources.cfg │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── .gitignore │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── rish │ ├── java/ │ │ └── moe/ │ │ └── shizuku/ │ │ └── manager/ │ │ ├── AppConstants.java │ │ ├── Helps.java │ │ ├── MainActivity.java │ │ ├── Manifest.java │ │ ├── ShizukuApplication.kt │ │ ├── ShizukuManagerProvider.kt │ │ ├── ShizukuSettings.java │ │ ├── adb/ │ │ │ ├── AdbClient.kt │ │ │ ├── AdbException.kt │ │ │ ├── AdbKey.kt │ │ │ ├── AdbMdns.kt │ │ │ ├── AdbMessage.kt │ │ │ ├── AdbPairingClient.kt │ │ │ ├── AdbPairingService.kt │ │ │ ├── AdbPairingTutorialActivity.kt │ │ │ └── AdbProtocol.kt │ │ ├── app/ │ │ │ ├── AppActivity.kt │ │ │ ├── AppBarActivity.kt │ │ │ └── ThemeHelper.java │ │ ├── authorization/ │ │ │ ├── AuthorizationManager.kt │ │ │ └── RequestPermissionActivity.kt │ │ ├── home/ │ │ │ ├── AdbDialogFragment.kt │ │ │ ├── AdbPairDialogFragment.kt │ │ │ ├── AdbPermissionLimitedViewHolder.kt │ │ │ ├── HomeActivity.kt │ │ │ ├── HomeAdapter.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── LearnMoreViewHolder.kt │ │ │ ├── ManageAppsViewHolder.kt │ │ │ ├── ServerStatusViewHolder.kt │ │ │ ├── StartAdbViewHolder.kt │ │ │ ├── StartRootViewHolder.kt │ │ │ ├── StartWirelessAdbViewHolder.kt │ │ │ ├── TerminalViewHolder.kt │ │ │ └── WadbNotEnabledDialogFragment.kt │ │ ├── ktx/ │ │ │ ├── Context.kt │ │ │ ├── Log.kt │ │ │ ├── PackageManager.kt │ │ │ ├── RecyclerView.kt │ │ │ └── String.kt │ │ ├── legacy/ │ │ │ ├── LegacyIsNotSupportedActivity.kt │ │ │ └── ShellRequestHandlerActivity.kt │ │ ├── management/ │ │ │ ├── AppViewHolder.kt │ │ │ ├── ApplicationManagementActivity.kt │ │ │ ├── AppsAdapter.java │ │ │ ├── AppsViewModel.kt │ │ │ └── EmptyViewHolder.kt │ │ ├── model/ │ │ │ └── ServiceStatus.kt │ │ ├── receiver/ │ │ │ ├── BootCompleteReceiver.kt │ │ │ └── ShizukuReceiver.kt │ │ ├── settings/ │ │ │ ├── IntegerSimpleMenuPreference.java │ │ │ ├── SettingsActivity.kt │ │ │ └── SettingsFragment.kt │ │ ├── shell/ │ │ │ ├── Shell.java │ │ │ ├── ShellBinderRequestHandler.kt │ │ │ └── ShellTutorialActivity.kt │ │ ├── starter/ │ │ │ ├── Starter.kt │ │ │ └── StarterActivity.kt │ │ ├── utils/ │ │ │ ├── AppIconCache.kt │ │ │ ├── CustomTabsHelper.java │ │ │ ├── EmptySharedPreferencesImpl.java │ │ │ ├── EnvironmentUtils.kt │ │ │ ├── Logger.java │ │ │ ├── MultiLocaleEntity.java │ │ │ ├── ShizukuSystemApis.kt │ │ │ ├── UserHandleCompat.java │ │ │ └── UserInfoCompat.java │ │ └── widget/ │ │ ├── CheckedImageView.java │ │ └── VerticalPaddingDecoration.java │ ├── jni/ │ │ ├── CMakeLists.txt │ │ ├── adb_pairing.cpp │ │ ├── adb_pairing.h │ │ ├── android.cpp │ │ ├── android.h │ │ ├── cgroup.cpp │ │ ├── cgroup.h │ │ ├── helper.cpp │ │ ├── logging.h │ │ ├── misc.cpp │ │ ├── misc.h │ │ ├── selinux.cpp │ │ ├── selinux.h │ │ └── starter.cpp │ └── res/ │ ├── animator/ │ │ └── alpha_animator.xml │ ├── color/ │ │ ├── grant_permissions_button_ripple_color_selector.xml │ │ ├── home_card_background_color.xml │ │ └── home_card_foreground_color.xml │ ├── color-night/ │ │ ├── home_card_background_color.xml │ │ └── home_card_foreground_color.xml │ ├── drawable/ │ │ ├── card_btn_background.xml │ │ ├── grant_permissions_buttons_bottom.xml │ │ ├── grant_permissions_buttons_top.xml │ │ ├── home_card_foreground.xml │ │ ├── ic_action_about_24dp.xml │ │ ├── ic_action_settings_24dp.xml │ │ ├── ic_adb_24dp.xml │ │ ├── ic_baseline_link_24.xml │ │ ├── ic_close_24.xml │ │ ├── ic_code_24dp.xml │ │ ├── ic_default_app_icon_background.xml │ │ ├── ic_help_outline_24dp.xml │ │ ├── ic_launcher.xml │ │ ├── ic_learn_more_24dp.xml │ │ ├── ic_monochrome.xml │ │ ├── ic_numeric_1_circle_outline_24.xml │ │ ├── ic_numeric_2_circle_outline_24.xml │ │ ├── ic_numeric_3_circle_outline_24.xml │ │ ├── ic_outline_arrow_upward_24.xml │ │ ├── ic_outline_dark_mode_24.xml │ │ ├── ic_outline_info_24.xml │ │ ├── ic_outline_notifications_active_24.xml │ │ ├── ic_outline_open_in_new_24.xml │ │ ├── ic_outline_play_arrow_24.xml │ │ ├── ic_outline_translate_24.xml │ │ ├── ic_root_24dp.xml │ │ ├── ic_server_error_24dp.xml │ │ ├── ic_server_ok_24dp.xml │ │ ├── ic_server_restart.xml │ │ ├── ic_server_start_24dp.xml │ │ ├── ic_settings_outline_24dp.xml │ │ ├── ic_system_icon.xml │ │ ├── ic_terminal_24.xml │ │ ├── ic_wadb_24.xml │ │ ├── ic_warning_24.xml │ │ └── shape_circle_icon_background.xml │ ├── drawable-v24/ │ │ └── ic_default_app_icon_foreground.xml │ ├── drawable-v26/ │ │ ├── ic_default_app_icon.xml │ │ └── ic_launcher.xml │ ├── layout/ │ │ ├── about_dialog.xml │ │ ├── adb_dialog.xml │ │ ├── adb_pair_dialog.xml │ │ ├── adb_pairing_tutorial_activity.xml │ │ ├── app_list_empty.xml │ │ ├── app_list_item.xml │ │ ├── appbar.xml │ │ ├── appbar_activity.xml │ │ ├── appbar_fragment_activity.xml │ │ ├── apps_activity.xml │ │ ├── confirmation_dialog.xml │ │ ├── home_activity.xml │ │ ├── home_extra_step_required.xml │ │ ├── home_item_container.xml │ │ ├── home_learn_more.xml │ │ ├── home_manage_apps_item.xml │ │ ├── home_server_status.xml │ │ ├── home_start_adb.xml │ │ ├── home_start_root.xml │ │ ├── home_start_wireless_adb.xml │ │ ├── home_terminal.xml │ │ ├── preference_recyclerview.xml │ │ ├── shell_dialog.xml │ │ ├── starter_activity.xml │ │ └── terminal_tutorial_activity.xml │ ├── menu/ │ │ └── main.xml │ ├── values/ │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── bools.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── strings_untranslatable.xml │ │ ├── styles.xml │ │ ├── themes.xml │ │ ├── themes_overlay.xml │ │ └── values.xml │ ├── values-ang/ │ │ └── strings.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-ars/ │ │ └── strings.xml │ ├── values-az/ │ │ └── strings.xml │ ├── values-b+es+419/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-bn/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-ckb/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-da/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-el/ │ │ └── strings.xml │ ├── values-enm/ │ │ └── strings.xml │ ├── values-eo/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-es-rCL/ │ │ └── strings.xml │ ├── values-et/ │ │ └── strings.xml │ ├── values-fa/ │ │ └── strings.xml │ ├── values-fi/ │ │ └── strings.xml │ ├── values-fil/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-he/ │ │ └── strings.xml │ ├── values-hi/ │ │ └── strings.xml │ ├── values-hr/ │ │ └── strings.xml │ ├── values-hu/ │ │ └── strings.xml │ ├── values-hy/ │ │ └── strings.xml │ ├── values-id/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-ka/ │ │ └── strings.xml │ ├── values-kk/ │ │ └── strings.xml │ ├── values-km/ │ │ └── strings.xml │ ├── values-kn/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-lb/ │ │ └── strings.xml │ ├── values-lv/ │ │ └── strings.xml │ ├── values-mk/ │ │ └── strings.xml │ ├── values-ml/ │ │ └── strings.xml │ ├── values-ms/ │ │ └── strings.xml │ ├── values-my/ │ │ └── strings.xml │ ├── values-night/ │ │ └── styles.xml │ ├── values-nl/ │ │ └── strings.xml │ ├── values-or/ │ │ └── strings.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-sk/ │ │ └── strings.xml │ ├── values-sl/ │ │ └── strings.xml │ ├── values-sr/ │ │ └── strings.xml │ ├── values-sv/ │ │ └── strings.xml │ ├── values-sw600dp/ │ │ ├── bools.xml │ │ ├── dimens.xml │ │ └── values.xml │ ├── values-ta/ │ │ └── strings.xml │ ├── values-te/ │ │ └── strings.xml │ ├── values-th/ │ │ └── strings.xml │ ├── values-tr/ │ │ └── strings.xml │ ├── values-ug/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-ur/ │ │ └── strings.xml │ ├── values-v21/ │ │ └── themes_override.xml │ ├── values-v31/ │ │ └── themes_overlay.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── backup_descriptor.xml │ ├── data_extraction_rules.xml │ └── settings.xml ├── server/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── rikka/ │ └── shizuku/ │ └── server/ │ ├── ApkChangedObservers.kt │ ├── BinderSender.java │ ├── ServerConstants.java │ ├── ShizukuClientManager.java │ ├── ShizukuConfig.java │ ├── ShizukuConfigManager.java │ ├── ShizukuService.java │ ├── ShizukuUserServiceManager.java │ ├── api/ │ │ └── IContentProviderUtils.java │ └── ktx/ │ └── Handler.kt ├── settings.gradle ├── shell/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── rikka/ │ └── shizuku/ │ └── shell/ │ └── ShizukuShellLoader.java ├── signing.gradle └── starter/ ├── .gitignore ├── build.gradle └── src/ └── main/ ├── AndroidManifest.xml └── java/ └── moe/ └── shizuku/ └── starter/ ├── ServiceStarter.java └── util/ └── IContentProviderCompat.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.bat text eol=crlf *.jar binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Report a bug of Shizuku body: - type: checkboxes id: requirements attributes: label: Requirements options: - label: Shizuku is downloaded from official channels (GitHub release or Google Play) - label: Shizuku is not running in a virtual environment or broken ROM (GrapheneOS) - label: (Root users) No Xposed installed / Xposed is not enabled for Shizuku - type: input id: version attributes: label: Latest Shizuku version validations: required: true - type: input id: shizuku_version attributes: label: Version in use validations: required: true - type: dropdown id: mode attributes: label: Mode options: - adb - root - manual validations: required: true - type: input id: android_version attributes: label: Android version validations: required: true - type: input id: device attributes: label: Device validations: required: true - type: textarea id: reproducer attributes: label: What did you do - type: textarea id: logs attributes: label: What happened description: You can upload log and screenshot - type: textarea id: expected-behaviour attributes: label: What do you expect ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: GitHub Community Support url: https://github.com/RikkaApps/Shizuku/discussions about: Please ask and answer questions here. ================================================ FILE: .github/workflows/app.yml ================================================ name: App on: push: paths-ignore: - '.github/ISSUE_TEMPLATE' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: 'recursive' fetch-depth: 0 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '21' - name: Write key if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' run: | echo KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }} > signing.properties echo KEYSTORE_ALIAS=${{ secrets.KEYSTORE_ALIAS }} >> signing.properties echo KEYSTORE_ALIAS_PASSWORD='${{ secrets.KEYSTORE_ALIAS_PASSWORD }}' >> signing.properties echo KEYSTORE_FILE=../key.jks >> signing.properties echo ${{ secrets.KEYSTORE }} | base64 --decode > key.jks - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: dependency-graph: generate-and-submit build-scan-publish: true build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - name: Build with Gradle id: buildWithGradle run: | yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null || true echo 'android.sdk.channel=3' >> gradle.properties echo 'android.native.buildOutput=verbose' >> gradle.properties echo 'org.gradle.caching=true' >> gradle.properties echo 'org.gradle.parallel=true' >> gradle.properties ./gradlew :manager:assemble releaseName=`ls manager/build/outputs/apk/release/shizuku*-v*-release.apk | awk -F '(/|-release.apk)' '{print $6}'` && echo "releaseName=$releaseName" >> $GITHUB_OUTPUT - name: Upload release if: success() uses: actions/upload-artifact@v4 with: name: ${{ steps.buildWithGradle.outputs.releaseName }} path: "manager/build/outputs" compression-level: 9 ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild /.idea /manager/signing.properties /out /signing.properties ================================================ FILE: .gitmodules ================================================ [submodule "api"] path = api url = git@github.com:RikkaApps/Shizuku-API.git branch = master ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Shizuku ## Background When developing apps that requires root, the most common method is to run some commands in the su shell. For example, there is an app that uses the `pm enable/disable` command to enable/disable components. This method has very big disadvantages: 1. **Extremely slow** (Multiple process creation) 2. Needs to process texts (**Super unreliable**) 3. The possibility is limited to available commands 4. Even if ADB has sufficient permissions, the app requires root privileges to run Shizuku uses a completely different way. See detailed description below. ## User guide & Download ## How does Shizuku work? First, we need to talk about how app use system APIs. For example, if the app wants to get installed apps, we all know we should use `PackageManager#getInstalledPackages()`. This is actually an interprocess communication (IPC) process of the app process and system server process, just the Android framework did the inner works for us. Android uses `binder` to do this type of IPC. `Binder` allows the server-side to learn the uid and pid of the client-side, so that the system server can check if the app has the permission to do the operation. Usually, if there is a "manager" (e.g., `PackageManager`) for apps to use, there should be a "service" (e.g., `PackageManagerService`) in the system server process. We can simply think if the app holds the `binder` of the "service", it can communicate with the "service". The app process will receive binders of system services on start. Shizuku guides users to run a process, Shizuku server, with root or ADB first. When the app starts, the `binder` to Shizuku server will also be sent to the app. The most important feature Shizuku provides is something like be a middle man to receive requests from the app, sent them to the system server, and send back the results. You can see the `transactRemote` method in `rikka.shizuku.server.ShizukuService` class, and `moe.shizuku.api.ShizukuBinderWrapper` class for the detail. So, we reached our goal, to use system APIs with higher permission. And to the app, it is almost identical to the use of system APIs directly. ## Developer guide ### API & sample https://github.com/RikkaApps/Shizuku-API ### Migrating from pre-v11 > Existing applications still works, of course. https://github.com/RikkaApps/Shizuku-API#migration-guide-for-existing-applications-use-shizuku-pre-v11 ### Attention 1. ADB permissions are limited ADB has limited permissions and different on various system versions. You can see permissions granted to ADB [here](https://github.com/aosp-mirror/platform_frameworks_base/blob/master/packages/Shell/AndroidManifest.xml). Before calling the API, you can use `ShizukuService#getUid` to check if Shizuku is running user ADB, or use `ShizukuService#checkPermission` to check if the server has sufficient permissions. 2. Hidden API limitation from Android 9 As of Android 9, the usage of the hidden APIs is limited for normal apps. Please use other methods (such as ). 3. Android 8.0 & ADB At present, the way Shizuku service gets the app process is to combine `IActivityManager#registerProcessObserver` and `IActivityManager#registerUidObserver` (26+) to ensure that the app process will be sent when the app starts. However, on API 26, ADB lacks permissions to use `registerUidObserver`, so if you need to use Shizuku in a process that might not be started by an Activity, it is recommended to trigger the send binder by starting a transparent activity. 4. Direct use of `transactRemote` requires attention * The API may be different under different Android versions, please be sure to check it carefully. Also, the `android.app.IActivityManager` has the aidl form in API 26 and later, and `android.app.IActivityManager$Stub` exists only on API 26. * `SystemServiceHelper.getTransactionCode` may not get the correct transaction code, such as `android.content.pm.IPackageManager$Stub.TRANSACTION_getInstalledPackages` does not exist on API 25 and there is `android.content.pm.IPackageManager$Stub.TRANSACTION_getInstalledPackages_47` (this situation has been dealt with, but it is not excluded that there may be other circumstances). This problem is not encountered with the `ShizukuBinderWrapper` method. ## Developing Shizuku itself ### Build - Clone with `git clone --recurse-submodules` - Run gradle task `:manager:assembleDebug` or `:manager:assembleRelease` The `:manager:assembleDebug` task generates a debuggable server. You can attach a debugger to `shizuku_server` to debug the server. Be aware that, in Android Studio, "Run/Debug configurations" - "Always install with package manager" should be checked, so that the server will use the latest code. ## License All code files in this project are licensed under Apache 2.0 Under Apache 2.0 section 6, specifically: * You are **FORBIDDEN** to use `manager/src/main/res/mipmap*/ic_launcher*.png` image files, unless for displaying Shizuku itself. * You are **FORBIDDEN** to use `Shizuku` as app name or use `moe.shizuku.privileged.api` as application id or declare `moe.shizuku.manager.permission.*` permission. ================================================ FILE: build.gradle ================================================ plugins { id("idea") } idea.module { excludeDirs += file('out') } subprojects { plugins.withId("com.android.base") { android { compileSdk = 36 buildToolsVersion = "36.0.0" ndkVersion = "29.0.13113456" defaultConfig { minSdk = 24 targetSdk = 36 } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } } } } apply from: 'api/manifest.gradle' def gitCommitId = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim() def gitCommitCount = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim()) def baseVersionName = "${api_version_major}.6.0" ext { versionCode = gitCommitCount versionName = "${baseVersionName}.r${gitCommitCount}.${gitCommitId}" } ================================================ FILE: common/.gitignore ================================================ /build ================================================ FILE: common/build.gradle ================================================ plugins { id 'com.android.library' } android { namespace 'rikka.shizuku.common' buildFeatures { buildConfig = false } } dependencies { compileOnly libs.hidden.stub } ================================================ FILE: common/src/main/AndroidManifest.xml ================================================ ================================================ FILE: common/src/main/java/moe/shizuku/common/util/BuildUtils.java ================================================ package moe.shizuku.common.util; import android.os.Build; /** * TODO: Replace it with {@link rikka.core.util.BuildUtils}. */ public class BuildUtils { private static final int SDK = Build.VERSION.SDK_INT; private static final int PREVIEW_SDK = SDK >= 23 ? Build.VERSION.PREVIEW_SDK_INT : 0; public static boolean atLeast31() { return SDK >= 31 || SDK == 30 && PREVIEW_SDK > 0; } public static boolean atLeast30() { return SDK >= 30; } public static boolean atLeast29() { return SDK >= 29; } public static boolean atLeast28() { return SDK >= 28; } public static boolean atLeast26() { return SDK >= 26; } public static boolean atLeast24() { return SDK >= 24; } public static boolean atLeast23() { return SDK >= 23; } } ================================================ FILE: common/src/main/java/moe/shizuku/common/util/OsUtils.java ================================================ package moe.shizuku.common.util; import android.os.SELinux; public class OsUtils { private static final int UID = android.system.Os.getuid(); private static final int PID = android.system.Os.getpid(); private static final String SELINUX_CONTEXT; static { String context; try { context = SELinux.getContext(); } catch (Throwable tr) { context = null; } SELINUX_CONTEXT = context; } public static int getUid() { return UID; } public static int getPid() { return PID; } public static String getSELinuxContext() { return SELINUX_CONTEXT; } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true android.nonTransitiveRClass=false android.nonFinalResIds=false ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: manager/.gitignore ================================================ /build /signing.properties /.cxx ================================================ FILE: manager/aapt2-resources.cfg ================================================ color/material_orange_50#no_obfuscate color/material_indigo_50#no_obfuscate color/material_blue_grey_50#no_obfuscate color/material_teal_50#no_obfuscate color/material_cyan_50#no_obfuscate color/material_blue_50#no_obfuscate color/material_blue_grey_50#no_obfuscate color/material_deep_purple_50#no_obfuscate color/material_red_50#no_obfuscate color/material_orange_100#no_obfuscate color/material_indigo_100#no_obfuscate color/material_blue_grey_100#no_obfuscate color/material_teal_100#no_obfuscate color/material_cyan_100#no_obfuscate color/material_blue_100#no_obfuscate color/material_blue_grey_100#no_obfuscate color/material_deep_purple_100#no_obfuscate color/material_red_100#no_obfuscate color/material_orange_600#no_obfuscate color/material_indigo_600#no_obfuscate color/material_blue_grey_600#no_obfuscate color/material_teal_600#no_obfuscate color/material_cyan_600#no_obfuscate color/material_blue_600#no_obfuscate color/material_blue_grey_600#no_obfuscate color/material_deep_purple_600#no_obfuscate color/material_red_600#no_obfuscate ================================================ FILE: manager/build.gradle ================================================ import java.nio.file.Paths import com.android.build.gradle.internal.tasks.CompileArtProfileTask plugins { id('com.android.application') id('org.jetbrains.kotlin.android') id('dev.rikka.tools.refine') id('dev.rikka.tools.autoresconfig') id('dev.rikka.tools.materialthemebuilder') } android { namespace 'moe.shizuku.manager' defaultConfig { applicationId "moe.shizuku.privileged.api" versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName externalNativeBuild { cmake { arguments '-DANDROID_STL=none' } } } buildFeatures { buildConfig true viewBinding true prefab true } signingConfigs { sign } buildTypes { debug { signingConfig signingConfigs.sign } release { signingConfig signingConfigs.sign minifyEnabled true shrinkResources true vcsInfo.include false proguardFiles 'proguard-rules.pro' } } externalNativeBuild { cmake { path 'src/main/jni/CMakeLists.txt' version = "3.31.0+" } } kotlinOptions { jvmTarget = "21" } packagingOptions { jniLibs { useLegacyPackaging true } resources { excludes += ['**'] } } dependenciesInfo { includeInApk false } lint { checkReleaseBuilds false } } autoResConfig { generatedClassFullName = "rikka.shizuku.manager.ShizukuLocales" generateRes = false generatedArrayFirstItem = "SYSTEM" generateLocaleConfig = true } materialThemeBuilder { themes { shizuku { primaryColor = "#3F51B5" lightThemeFormat = "Theme.Material3.Light.%s" lightThemeParent = "Theme.Material3.Light.Rikka" darkThemeFormat = "Theme.Material3.Dark.%s" darkThemeParent = "Theme.Material3.Dark.Rikka" } } generatePalette = true generateTextColors = true } def collapseReleaseResourceNames = task('collapseReleaseResourceNames').doLast { def aapt2 = Paths.get(project.android.sdkDirectory.path, 'build-tools', project.android.buildToolsVersion, 'aapt2') def zip = Paths.get(project.buildDir.path, 'intermediates', 'optimized_processed_res', 'release', 'optimizeReleaseResources', 'resources-release-optimize.ap_') def optimized = new File("${zip}.opt") def cmd = exec { commandLine aapt2, 'optimize', '--collapse-resource-names', '--resources-config-path', 'aapt2-resources.cfg', '-o', optimized, zip ignoreExitValue false } if (cmd.exitValue == 0) { delete(zip) optimized.renameTo("$zip") } } afterEvaluate { tasks.getByName('optimizeReleaseResources').finalizedBy(collapseReleaseResourceNames) tasks.getByName('preReleaseBuild').dependsOn(':shell:assembleRelease') tasks.getByName('preDebugBuild').dependsOn(':shell:assembleDebug') } android.applicationVariants.configureEach { variant -> variant.outputs.configureEach { outputFileName = "shizuku-v${variant.versionName}-${variant.name}.apk" variant.assembleProvider.get().doLast { def outDir = new File(rootDir, "out") def mappingDir = new File(outDir, "mapping").absolutePath def apkDir = new File(outDir, "apk").absolutePath if (variant.getBuildType().isMinifyEnabled()) { copy { from variant.mappingFileProvider.get() into mappingDir rename { String fileName -> "mapping-${variant.versionName}.txt" } } copy { from outputFile into apkDir } } } } } tasks.withType(CompileArtProfileTask.class).configureEach { enabled = false } configurations.configureEach { exclude group: 'androidx.appcompat', module: 'appcompat' exclude group: 'androidx.profileinstaller', module: 'profileinstaller' } dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' implementation project(':server') implementation project(':rish') implementation project(':starter') implementation project(':api') implementation project(':provider') implementation libs.hidden.compat compileOnly libs.hidden.stub implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.core:core-ktx:1.16.0' implementation 'androidx.fragment:fragment-ktx:1.8.7' implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'com.google.android.material:material:1.12.0' implementation 'com.github.topjohnwu.libsu:core:6.0.0' implementation 'dev.rikka.rikkax.appcompat:appcompat:1.6.1' implementation 'dev.rikka.rikkax.compatibility:compatibility:2.0.0' implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1' implementation 'dev.rikka.rikkax.material:material:2.7.2' implementation 'dev.rikka.rikkax.material:material-preference:2.0.0' implementation 'dev.rikka.rikkax.html:html-ktx:1.1.2' implementation 'dev.rikka.rikkax.recyclerview:recyclerview-adapter:1.3.0' implementation 'dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2' implementation 'dev.rikka.rikkax.insets:insets:1.3.0' implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0' implementation 'dev.rikka.rikkax.widget:borderview:1.1.0' implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' implementation 'dev.rikka.rikkax.lifecycle:lifecycle-resource-livedata:1.0.1' implementation 'dev.rikka.rikkax.lifecycle:lifecycle-shared-viewmodel:1.0.1' implementation 'dev.rikka.rikkax.lifecycle:lifecycle-viewmodel-lazy:2.0.0' implementation 'io.github.vvb2060.ndk:boringssl:20250114' implementation 'org.lsposed.libcxx:libcxx:27.0.12077973' implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:6.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.80' implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' } apply from: rootProject.file('signing.gradle') ================================================ FILE: manager/proguard-rules.pro ================================================ -keepclassmembers class * implements android.os.Parcelable { public static final ** CREATOR; } -keepclasseswithmembernames,includedescriptorclasses class * { native ; } -assumenosideeffects class kotlin.jvm.internal.Intrinsics { public static void check*(...); public static void throw*(...); } -assumenosideeffects class java.util.Objects{ ** requireNonNull(...); } -keepnames class moe.shizuku.api.BinderContainer # Missing class android.app.IProcessObserver$Stub # Missing class android.app.IUidObserver$Stub -keepclassmembers class rikka.hidden.compat.adapter.ProcessObserverAdapter { ; } -keepclassmembers class rikka.hidden.compat.adapter.UidObserverAdapter { ; } # Entrance of Shizuku service -keep class rikka.shizuku.server.ShizukuService { public static void main(java.lang.String[]); } # Entrance of user service starter -keep class moe.shizuku.starter.ServiceStarter { public static void main(java.lang.String[]); } # Entrance of shell -keep class moe.shizuku.manager.shell.Shell { public static void main(java.lang.String[], java.lang.String, android.os.IBinder, android.os.Handler); } -assumenosideeffects class android.util.Log { public static *** d(...); } -assumenosideeffects class moe.shizuku.manager.utils.Logger { public *** d(...); } #noinspection ShrinkerUnresolvedReference -assumenosideeffects class rikka.shizuku.server.util.Logger { public *** d(...); } -allowaccessmodification -repackageclasses rikka.shizuku -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile ================================================ FILE: manager/src/main/.gitignore ================================================ /assets/*.dex ================================================ FILE: manager/src/main/AndroidManifest.xml ================================================ ================================================ FILE: manager/src/main/assets/rish ================================================ #!/system/bin/sh BASEDIR=$(dirname "$0") DEX="$BASEDIR"/rish_shizuku.dex if [ ! -f "$DEX" ]; then echo "Cannot find $DEX, please check the tutorial in Shizuku app" exit 1 fi if [ $(getprop ro.build.version.sdk) -ge 34 ]; then if [ -w $DEX ]; then echo "On Android 14+, app_process cannot load writable dex." echo "Attempting to remove the write permission..." chmod 400 $DEX fi if [ -w $DEX ]; then echo "Cannot remove the write permission of $DEX." echo "You can copy to file to terminal app's private directory (/data/data/, so that remove write permission is possible" exit 1 fi fi # Replace "PKG" with the application id of your terminal app [ -z "$RISH_APPLICATION_ID" ] && export RISH_APPLICATION_ID="PKG" /system/bin/app_process -Djava.class.path="$DEX" /system/bin --nice-name=rish rikka.shizuku.shell.ShizukuShellLoader "$@" ================================================ FILE: manager/src/main/java/moe/shizuku/manager/AppConstants.java ================================================ package moe.shizuku.manager; public class AppConstants { public static final String TAG = "ShizukuManager"; public static final String NOTIFICATION_CHANNEL_STATUS = "starter"; public static final String NOTIFICATION_CHANNEL_WORK = "work"; public static final int NOTIFICATION_ID_STATUS = 1; public static final int NOTIFICATION_ID_WORK = 2; private static final String PACKAGE = "moe.shizuku.manager"; public static final String EXTRA = PACKAGE + ".extra"; } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/Helps.java ================================================ package moe.shizuku.manager; import moe.shizuku.manager.utils.MultiLocaleEntity; public class Helps { public static final MultiLocaleEntity ADB = new MultiLocaleEntity(); public static final MultiLocaleEntity ADB_ANDROID11 = new MultiLocaleEntity(); public static final MultiLocaleEntity APPS = new MultiLocaleEntity(); public static final MultiLocaleEntity HOME = new MultiLocaleEntity(); public static final MultiLocaleEntity DOWNLOAD = new MultiLocaleEntity(); public static final MultiLocaleEntity SUI = new MultiLocaleEntity(); public static final MultiLocaleEntity RISH = new MultiLocaleEntity(); public static final MultiLocaleEntity ADB_PERMISSION = new MultiLocaleEntity(); static { ADB.put("zh-CN", "https://shizuku.rikka.app/zh-hans/guide/setup/"); ADB.put("zh-TW", "https://shizuku.rikka.app/zh-hant/guide/setup/"); ADB.put("en", "https://shizuku.rikka.app/guide/setup/"); ADB_ANDROID11.put("zh-CN", "https://shizuku.rikka.app/zh-hans/guide/setup/"); ADB_ANDROID11.put("zh-TW", "https://shizuku.rikka.app/zh-hant/guide/setup/"); ADB_ANDROID11.put("en", "https://shizuku.rikka.app/guide/setup/"); APPS.put("zh-CN", "https://shizuku.rikka.app/zh-hans/apps/"); APPS.put("zh-TW", "https://shizuku.rikka.app/zh-hant/apps/"); APPS.put("en", "https://shizuku.rikka.app/apps/"); HOME.put("zh-CN", "https://shizuku.rikka.app/zh-hans/"); HOME.put("zh-TW", "https://shizuku.rikka.app/zh-hant/"); HOME.put("en", "https://shizuku.rikka.app/"); DOWNLOAD.put("zh-CN", "https://shizuku.rikka.app/zh-hans/download/"); DOWNLOAD.put("zh-TW", "https://shizuku.rikka.app/zh-hant/download/"); DOWNLOAD.put("en", "https://shizuku.rikka.app/download/"); ADB_PERMISSION.put("zh-CN", "https://shizuku.rikka.app/zh-hans/guide/setup/#%E9%80%9A%E8%BF%87%E6%97%A0%E7%BA%BF%E8%B0%83%E8%AF%95%E5%90%AF%E5%8A%A8-%E9%80%9A%E8%BF%87%E8%BF%9E%E6%8E%A5%E7%94%B5%E8%84%91%E5%90%AF%E5%8A%A8-adb-%E6%9D%83%E9%99%90%E5%8F%97%E9%99%90"); ADB_PERMISSION.put("zh-TW", "https://shizuku.rikka.app/zh-hant/guide/setup/#%E9%80%8F%E9%81%8E%E7%84%A1%E7%B7%9A%E9%99%A4%E9%8C%AF%E5%95%9F%E5%8B%95-%E9%80%8F%E9%81%8E%E9%80%A3%E7%B7%9A%E9%9B%BB%E8%85%A6%E5%95%9F%E5%8B%95-adb-%E6%AC%8A%E9%99%90%E5%8F%97%E9%99%90"); ADB_PERMISSION.put("en", "https://shizuku.rikka.app/guide/setup/#start-via-wireless-debugging-start-by-connecting-to-a-computer-the-permission-of-adb-is-limited"); SUI.put("en", "https://github.com/RikkaApps/Sui"); RISH.put("en", "https://github.com/RikkaApps/Shizuku-API/tree/master/rish"); } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/MainActivity.java ================================================ package moe.shizuku.manager; import moe.shizuku.manager.home.HomeActivity; public class MainActivity extends HomeActivity { } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/Manifest.java ================================================ package moe.shizuku.manager; public class Manifest { public static class permission { public static final String API_V23 = "moe.shizuku.manager.permission.API_V23"; } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ShizukuApplication.kt ================================================ package moe.shizuku.manager import android.app.Application import android.content.Context import android.os.Build import androidx.appcompat.app.AppCompatDelegate import com.topjohnwu.superuser.Shell import moe.shizuku.manager.ktx.logd import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.core.util.BuildUtils.atLeast30 import rikka.material.app.LocaleDelegate lateinit var application: ShizukuApplication class ShizukuApplication : Application() { companion object { init { logd("ShizukuApplication", "init") Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR)) if (Build.VERSION.SDK_INT >= 28) { HiddenApiBypass.setHiddenApiExemptions("") } if (atLeast30) { System.loadLibrary("adb") } } } private fun init(context: Context?) { ShizukuSettings.initialize(context) LocaleDelegate.defaultLocale = ShizukuSettings.getLocale() AppCompatDelegate.setDefaultNightMode(ShizukuSettings.getNightMode()) } override fun onCreate() { super.onCreate() application = this init(this) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ShizukuManagerProvider.kt ================================================ package moe.shizuku.manager import android.os.Bundle import androidx.core.os.bundleOf import moe.shizuku.api.BinderContainer import moe.shizuku.manager.utils.Logger.LOGGER import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuApiConstants.USER_SERVICE_ARG_TOKEN import rikka.shizuku.ShizukuProvider import rikka.shizuku.server.ktx.workerHandler import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException class ShizukuManagerProvider : ShizukuProvider() { companion object { private const val EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER" private const val METHOD_SEND_USER_SERVICE = "sendUserService" } override fun onCreate(): Boolean { disableAutomaticSuiInitialization() return super.onCreate() } override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { if (extras == null) return null return if (method == METHOD_SEND_USER_SERVICE) { try { extras.classLoader = BinderContainer::class.java.classLoader val token = extras.getString(USER_SERVICE_ARG_TOKEN) ?: return null val binder = extras.getParcelable(EXTRA_BINDER)?.binder ?: return null val countDownLatch = CountDownLatch(1) var reply: Bundle? = Bundle() val listener = object : Shizuku.OnBinderReceivedListener { override fun onBinderReceived() { try { Shizuku.attachUserService(binder, bundleOf( USER_SERVICE_ARG_TOKEN to token )) reply!!.putParcelable(EXTRA_BINDER, BinderContainer(Shizuku.getBinder())) } catch (e: Throwable) { LOGGER.e(e, "attachUserService $token") reply = null } Shizuku.removeBinderReceivedListener(this) countDownLatch.countDown() } } Shizuku.addBinderReceivedListenerSticky(listener, workerHandler) return try { countDownLatch.await(5, TimeUnit.SECONDS) reply } catch (e: TimeoutException) { LOGGER.e(e, "Binder not received in 5s") null } } catch (e: Throwable) { LOGGER.e(e, "sendUserService") null } } else { super.call(method, arg, extras) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ShizukuSettings.java ================================================ package moe.shizuku.manager; import android.app.ActivityThread; import android.content.Context; import android.content.ContextWrapper; import android.content.SharedPreferences; import android.os.Build; import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import java.lang.annotation.Retention; import java.util.Locale; import moe.shizuku.manager.utils.EmptySharedPreferencesImpl; import moe.shizuku.manager.utils.EnvironmentUtils; import static java.lang.annotation.RetentionPolicy.SOURCE; public class ShizukuSettings { public static final String NAME = "settings"; public static final String NIGHT_MODE = "night_mode"; public static final String LANGUAGE = "language"; public static final String KEEP_START_ON_BOOT = "start_on_boot"; private static SharedPreferences sPreferences; public static SharedPreferences getPreferences() { return sPreferences; } @NonNull private static Context getSettingsStorageContext(@NonNull Context context) { Context storageContext; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { storageContext = context.createDeviceProtectedStorageContext(); } else { storageContext = context; } storageContext = new ContextWrapper(storageContext) { @Override public SharedPreferences getSharedPreferences(String name, int mode) { try { return super.getSharedPreferences(name, mode); } catch (IllegalStateException e) { // SharedPreferences in credential encrypted storage are not available until after user is unlocked return new EmptySharedPreferencesImpl(); } } }; return storageContext; } public static void initialize(Context context) { if (sPreferences == null) { sPreferences = getSettingsStorageContext(context) .getSharedPreferences(NAME, Context.MODE_PRIVATE); } } @IntDef({ LaunchMethod.UNKNOWN, LaunchMethod.ROOT, LaunchMethod.ADB, }) @Retention(SOURCE) public @interface LaunchMethod { int UNKNOWN = -1; int ROOT = 0; int ADB = 1; } @LaunchMethod public static int getLastLaunchMode() { return getPreferences().getInt("mode", LaunchMethod.UNKNOWN); } public static void setLastLaunchMode(@LaunchMethod int method) { getPreferences().edit().putInt("mode", method).apply(); } @AppCompatDelegate.NightMode public static int getNightMode() { int defValue = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; if (EnvironmentUtils.isWatch(ActivityThread.currentActivityThread().getApplication())) { defValue = AppCompatDelegate.MODE_NIGHT_YES; } return getPreferences().getInt(NIGHT_MODE, defValue); } public static Locale getLocale() { String tag = getPreferences().getString(LANGUAGE, null); if (TextUtils.isEmpty(tag) || "SYSTEM".equals(tag)) { return Locale.getDefault(); } return Locale.forLanguageTag(tag); } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbClient.kt ================================================ package moe.shizuku.manager.adb import android.util.Log import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_SIGNATURE import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_TOKEN import moe.shizuku.manager.adb.AdbProtocol.A_AUTH import moe.shizuku.manager.adb.AdbProtocol.A_CLSE import moe.shizuku.manager.adb.AdbProtocol.A_CNXN import moe.shizuku.manager.adb.AdbProtocol.A_MAXDATA import moe.shizuku.manager.adb.AdbProtocol.A_OKAY import moe.shizuku.manager.adb.AdbProtocol.A_OPEN import moe.shizuku.manager.adb.AdbProtocol.A_STLS import moe.shizuku.manager.adb.AdbProtocol.A_STLS_VERSION import moe.shizuku.manager.adb.AdbProtocol.A_VERSION import moe.shizuku.manager.adb.AdbProtocol.A_WRTE import moe.shizuku.manager.ktx.logd import rikka.core.util.BuildUtils import java.io.Closeable import java.io.DataInputStream import java.io.DataOutputStream import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder import javax.net.ssl.SSLSocket private const val TAG = "AdbClient" class AdbClient(private val host: String, private val port: Int, private val key: AdbKey) : Closeable { private lateinit var socket: Socket private lateinit var plainInputStream: DataInputStream private lateinit var plainOutputStream: DataOutputStream private var useTls = false private lateinit var tlsSocket: SSLSocket private lateinit var tlsInputStream: DataInputStream private lateinit var tlsOutputStream: DataOutputStream private val inputStream get() = if (useTls) tlsInputStream else plainInputStream private val outputStream get() = if (useTls) tlsOutputStream else plainOutputStream fun connect() { socket = Socket(host, port) socket.tcpNoDelay = true plainInputStream = DataInputStream(socket.getInputStream()) plainOutputStream = DataOutputStream(socket.getOutputStream()) write(A_CNXN, A_VERSION, A_MAXDATA, "host::") var message = read() if (message.command == A_STLS) { if (!BuildUtils.atLeast29) { error("Connect to adb with TLS is not supported before Android 9") } write(A_STLS, A_STLS_VERSION, 0) val sslContext = key.sslContext tlsSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket tlsSocket.startHandshake() Log.d(TAG, "Handshake succeeded.") tlsInputStream = DataInputStream(tlsSocket.inputStream) tlsOutputStream = DataOutputStream(tlsSocket.outputStream) useTls = true message = read() } else if (message.command == A_AUTH) { if (message.command != A_AUTH && message.arg0 != ADB_AUTH_TOKEN) error("not A_AUTH ADB_AUTH_TOKEN") write(A_AUTH, ADB_AUTH_SIGNATURE, 0, key.sign(message.data)) message = read() if (message.command != A_CNXN) { write(A_AUTH, ADB_AUTH_RSAPUBLICKEY, 0, key.adbPublicKey) message = read() } } if (message.command != A_CNXN) error("not A_CNXN") } fun shellCommand(command: String, listener: ((ByteArray) -> Unit)?) { val localId = 1 write(A_OPEN, localId, 0, "shell:$command") var message = read() when (message.command) { A_OKAY -> { while (true) { message = read() val remoteId = message.arg0 if (message.command == A_WRTE) { if (message.data_length > 0) { listener?.invoke(message.data!!) } write(A_OKAY, localId, remoteId) } else if (message.command == A_CLSE) { write(A_CLSE, localId, remoteId) break } else { error("not A_WRTE or A_CLSE") } } } A_CLSE -> { val remoteId = message.arg0 write(A_CLSE, localId, remoteId) } else -> { error("not A_OKAY or A_CLSE") } } } private fun write(command: Int, arg0: Int, arg1: Int, data: ByteArray? = null) = write(AdbMessage(command, arg0, arg1, data)) private fun write(command: Int, arg0: Int, arg1: Int, data: String) = write(AdbMessage(command, arg0, arg1, data)) private fun write(message: AdbMessage) { outputStream.write(message.toByteArray()) outputStream.flush() Log.d(TAG, "write ${message.toStringShort()}") } private fun read(): AdbMessage { val buffer = ByteBuffer.allocate(AdbMessage.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN) inputStream.readFully(buffer.array(), 0, 24) val command = buffer.int val arg0 = buffer.int val arg1 = buffer.int val dataLength = buffer.int val checksum = buffer.int val magic = buffer.int val data: ByteArray? if (dataLength >= 0) { data = ByteArray(dataLength) inputStream.readFully(data, 0, dataLength) } else { data = null } val message = AdbMessage(command, arg0, arg1, dataLength, checksum, magic, data) message.validateOrThrow() Log.d(TAG, "read ${message.toStringShort()}") return message } override fun close() { try { plainInputStream.close() } catch (e: Throwable) { } try { plainOutputStream.close() } catch (e: Throwable) { } try { socket.close() } catch (e: Exception) { } if (useTls) { try { tlsInputStream.close() } catch (e: Throwable) { } try { tlsOutputStream.close() } catch (e: Throwable) { } try { tlsSocket.close() } catch (e: Exception) { } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbException.kt ================================================ package moe.shizuku.manager.adb @Suppress("NOTHING_TO_INLINE") inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) open class AdbException : Exception { constructor(message: String, cause: Throwable?) : super(message, cause) constructor(message: String) : super(message) constructor(cause: Throwable) : super(cause) constructor() } class AdbInvalidPairingCodeException : AdbException() class AdbKeyException(cause: Throwable) : AdbException(cause) ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbKey.kt ================================================ package moe.shizuku.manager.adb import android.annotation.SuppressLint import android.content.SharedPreferences import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.edit import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import rikka.core.ktx.unsafeLazy import java.io.ByteArrayInputStream import java.math.BigInteger import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder import java.security.* import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAPublicKeySpec import java.util.* import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.spec.GCMParameterSpec import javax.net.ssl.SSLContext import javax.net.ssl.SSLEngine import javax.net.ssl.X509ExtendedKeyManager import javax.net.ssl.X509ExtendedTrustManager private const val TAG = "AdbKey" class AdbKey(private val adbKeyStore: AdbKeyStore, name: String) { companion object { private const val ANDROID_KEYSTORE = "AndroidKeyStore" private const val ENCRYPTION_KEY_ALIAS = "_adbkey_encryption_key_" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val IV_SIZE_IN_BYTES = 12 private const val TAG_SIZE_IN_BYTES = 16 private val PADDING = byteArrayOf( 0x00, 0x01, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14) } private val encryptionKey: Key private val privateKey: RSAPrivateKey private val publicKey: RSAPublicKey private val certificate: X509Certificate init { this.encryptionKey = getOrCreateEncryptionKey() ?: error("Failed to generate encryption key with AndroidKeyManager.") this.privateKey = getOrCreatePrivateKey() this.publicKey = KeyFactory.getInstance("RSA").generatePublic(RSAPublicKeySpec(privateKey.modulus, RSAKeyGenParameterSpec.F4)) as RSAPublicKey val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey) val x509Certificate = X509v3CertificateBuilder(X500Name("CN=00"), BigInteger.ONE, Date(0), Date(2461449600 * 1000), Locale.ROOT, X500Name("CN=00"), SubjectPublicKeyInfo.getInstance(publicKey.encoded) ).build(signer) this.certificate = CertificateFactory.getInstance("X.509") .generateCertificate(ByteArrayInputStream(x509Certificate.encoded)) as X509Certificate Log.d(TAG, privateKey.toString()) } val adbPublicKey: ByteArray by unsafeLazy { publicKey.adbEncoded(name) } private fun getOrCreateEncryptionKey(): Key? { val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) keyStore.load(null) return keyStore.getKey(ENCRYPTION_KEY_ALIAS, null) ?: run { val parameterSpec = KeyGenParameterSpec.Builder(ENCRYPTION_KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) keyGenerator.init(parameterSpec) keyGenerator.generateKey() } } private fun encrypt(plaintext: ByteArray, aad: ByteArray?): ByteArray? { if (plaintext.size > Int.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) { return null } val ciphertext = ByteArray(IV_SIZE_IN_BYTES + plaintext.size + TAG_SIZE_IN_BYTES) val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, encryptionKey) cipher.updateAAD(aad) cipher.doFinal(plaintext, 0, plaintext.size, ciphertext, IV_SIZE_IN_BYTES) System.arraycopy(cipher.iv, 0, ciphertext, 0, IV_SIZE_IN_BYTES) return ciphertext } private fun decrypt(ciphertext: ByteArray, aad: ByteArray?): ByteArray? { if (ciphertext.size < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) { return null } val params = GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES) val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params) cipher.updateAAD(aad) return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.size - IV_SIZE_IN_BYTES) } private fun getOrCreatePrivateKey(): RSAPrivateKey { var privateKey: RSAPrivateKey? = null val aad = ByteArray(16) "adbkey".toByteArray().copyInto(aad) var ciphertext = adbKeyStore.get() if (ciphertext != null) { try { val plaintext = decrypt(ciphertext, aad) val keyFactory = KeyFactory.getInstance("RSA") privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(plaintext)) as RSAPrivateKey } catch (e: Exception) { } } if (privateKey == null) { val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA) keyPairGenerator.initialize(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) val keyPair = keyPairGenerator.generateKeyPair() privateKey = keyPair.private as RSAPrivateKey ciphertext = encrypt(privateKey.encoded, aad) if (ciphertext != null) { adbKeyStore.put(ciphertext) } } return privateKey } fun sign(data: ByteArray?): ByteArray { val cipher = Cipher.getInstance("RSA/ECB/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, privateKey) cipher.update(PADDING) return cipher.doFinal(data) } private val keyManager get() = object : X509ExtendedKeyManager() { private val alias = "key" override fun chooseClientAlias(keyTypes: Array, issuers: Array?, socket: Socket?): String? { Log.d(TAG, "chooseClientAlias: keyType=${keyTypes.contentToString()}, issuers=${issuers?.contentToString()}") for (keyType in keyTypes) { if (keyType == "RSA") return alias } return null } override fun getCertificateChain(alias: String?): Array? { Log.d(TAG, "getCertificateChain: alias=$alias") return if (alias == this.alias) arrayOf(certificate) else null } override fun getPrivateKey(alias: String?): PrivateKey? { Log.d(TAG, "getPrivateKey: alias=$alias") return if (alias == this.alias) privateKey else null } override fun getClientAliases(keyType: String?, issuers: Array?): Array? { return null } override fun getServerAliases(keyType: String, issuers: Array?): Array? { return null } override fun chooseServerAlias(keyType: String, issuers: Array?, socket: Socket?): String? { return null } } private val trustManager get() = @RequiresApi(Build.VERSION_CODES.R) object : X509ExtendedTrustManager() { @SuppressLint("TrustAllX509TrustManager") override fun checkClientTrusted(chain: Array?, authType: String?, socket: Socket?) { } @SuppressLint("TrustAllX509TrustManager") override fun checkClientTrusted(chain: Array?, authType: String?, engine: SSLEngine?) { } @SuppressLint("TrustAllX509TrustManager") override fun checkClientTrusted(chain: Array?, authType: String?) { } @SuppressLint("TrustAllX509TrustManager") override fun checkServerTrusted(chain: Array?, authType: String?, socket: Socket?) { } @SuppressLint("TrustAllX509TrustManager") override fun checkServerTrusted(chain: Array?, authType: String?, engine: SSLEngine?) { } @SuppressLint("TrustAllX509TrustManager") override fun checkServerTrusted(chain: Array?, authType: String?) { } override fun getAcceptedIssuers(): Array { return emptyArray() } } @delegate:RequiresApi(Build.VERSION_CODES.R) val sslContext: SSLContext by unsafeLazy { val sslContext = SSLContext.getInstance("TLSv1.3") sslContext.init(arrayOf(keyManager), arrayOf(trustManager), SecureRandom()) sslContext } } interface AdbKeyStore { fun put(bytes: ByteArray) fun get(): ByteArray? } class PreferenceAdbKeyStore(private val preference: SharedPreferences) : AdbKeyStore { private val preferenceKey = "adbkey" override fun put(bytes: ByteArray) { preference.edit { putString(preferenceKey, String(Base64.encode(bytes, Base64.NO_WRAP))) } } override fun get(): ByteArray? { if (!preference.contains(preferenceKey)) return null return Base64.decode(preference.getString(preferenceKey, null), Base64.NO_WRAP) } } const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 const val RSAPublicKey_Size = 524 private fun BigInteger.toAdbEncoded(): IntArray { // little-endian integer with padding zeros in the end val endcoded = IntArray(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) val r32 = BigInteger.ZERO.setBit(32) var tmp = this.add(BigInteger.ZERO) for (i in 0 until ANDROID_PUBKEY_MODULUS_SIZE_WORDS) { val out = tmp.divideAndRemainder(r32) tmp = out[0] endcoded[i] = out[1].toInt() } return endcoded } private fun RSAPublicKey.adbEncoded(name: String): ByteArray { // https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:system/core/libcrypto_utils/android_pubkey.c /* typedef struct RSAPublicKey { uint32_t modulus_size_words; // ANDROID_PUBKEY_MODULUS_SIZE uint32_t n0inv; // n0inv = -1 / N[0] mod 2^32 uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // rr = (2^(rsa_size)) ^ 2 mod N uint32_t exponent; } RSAPublicKey; */ val r32 = BigInteger.ZERO.setBit(32) val n0inv = modulus.remainder(r32).modInverse(r32).negate() val r = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8) val rr = r.modPow(BigInteger.valueOf(2), modulus) val buffer = ByteBuffer.allocate(RSAPublicKey_Size).order(ByteOrder.LITTLE_ENDIAN) buffer.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) buffer.putInt(n0inv.toInt()) modulus.toAdbEncoded().forEach { buffer.putInt(it) } rr.toAdbEncoded().forEach { buffer.putInt(it) } buffer.putInt(publicExponent.toInt()) val base64Bytes = Base64.encode(buffer.array(), Base64.NO_WRAP) val nameBytes = " $name\u0000".toByteArray() val bytes = ByteArray(base64Bytes.size + nameBytes.size) base64Bytes.copyInto(bytes) nameBytes.copyInto(bytes, base64Bytes.size) return bytes } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbMdns.kt ================================================ package moe.shizuku.manager.adb import android.content.Context import android.net.nsd.NsdManager import android.net.nsd.NsdServiceInfo import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.lifecycle.Observer import java.io.IOException import java.net.InetSocketAddress import java.net.NetworkInterface import java.net.ServerSocket @RequiresApi(Build.VERSION_CODES.R) class AdbMdns( context: Context, private val serviceType: String, private val observer: Observer ) { private var registered = false private var running = false private var serviceName: String? = null private val listener = DiscoveryListener(this) private val nsdManager: NsdManager = context.getSystemService(NsdManager::class.java) fun start() { if (running) return running = true if (!registered) { nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener) } } fun stop() { if (!running) return running = false if (registered) { nsdManager.stopServiceDiscovery(listener) } } private fun onDiscoveryStart() { registered = true } private fun onDiscoveryStop() { registered = false } private fun onServiceFound(info: NsdServiceInfo) { nsdManager.resolveService(info, ResolveListener(this)) } private fun onServiceLost(info: NsdServiceInfo) { if (info.serviceName == serviceName) observer.onChanged(-1) } private fun onServiceResolved(resolvedService: NsdServiceInfo) { if (running && NetworkInterface.getNetworkInterfaces() .asSequence() .any { networkInterface -> networkInterface.inetAddresses .asSequence() .any { resolvedService.host.hostAddress == it.hostAddress } } && isPortAvailable(resolvedService.port) ) { serviceName = resolvedService.serviceName observer.onChanged(resolvedService.port) } } private fun isPortAvailable(port: Int) = try { ServerSocket().use { it.bind(InetSocketAddress("127.0.0.1", port), 1) false } } catch (e: IOException) { true } internal class DiscoveryListener(private val adbMdns: AdbMdns) : NsdManager.DiscoveryListener { override fun onDiscoveryStarted(serviceType: String) { Log.v(TAG, "onDiscoveryStarted: $serviceType") adbMdns.onDiscoveryStart() } override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { Log.v(TAG, "onStartDiscoveryFailed: $serviceType, $errorCode") } override fun onDiscoveryStopped(serviceType: String) { Log.v(TAG, "onDiscoveryStopped: $serviceType") adbMdns.onDiscoveryStop() } override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { Log.v(TAG, "onStopDiscoveryFailed: $serviceType, $errorCode") } override fun onServiceFound(serviceInfo: NsdServiceInfo) { Log.v(TAG, "onServiceFound: ${serviceInfo.serviceName}") adbMdns.onServiceFound(serviceInfo) } override fun onServiceLost(serviceInfo: NsdServiceInfo) { Log.v(TAG, "onServiceLost: ${serviceInfo.serviceName}") adbMdns.onServiceLost(serviceInfo) } } internal class ResolveListener(private val adbMdns: AdbMdns) : NsdManager.ResolveListener { override fun onResolveFailed(nsdServiceInfo: NsdServiceInfo, i: Int) {} override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) { adbMdns.onServiceResolved(nsdServiceInfo) } } companion object { const val TLS_CONNECT = "_adb-tls-connect._tcp" const val TLS_PAIRING = "_adb-tls-pairing._tcp" const val TAG = "AdbMdns" } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbMessage.kt ================================================ package moe.shizuku.manager.adb import moe.shizuku.manager.adb.AdbProtocol.A_AUTH import moe.shizuku.manager.adb.AdbProtocol.A_CLSE import moe.shizuku.manager.adb.AdbProtocol.A_CNXN import moe.shizuku.manager.adb.AdbProtocol.A_OKAY import moe.shizuku.manager.adb.AdbProtocol.A_OPEN import moe.shizuku.manager.adb.AdbProtocol.A_STLS import moe.shizuku.manager.adb.AdbProtocol.A_SYNC import moe.shizuku.manager.adb.AdbProtocol.A_WRTE import java.nio.ByteBuffer import java.nio.ByteOrder class AdbMessage( val command: Int, val arg0: Int, val arg1: Int, val data_length: Int, val data_crc32: Int, val magic: Int, val data: ByteArray? ) { constructor(command: Int, arg0: Int, arg1: Int, data: String) : this( command, arg0, arg1, "$data\u0000".toByteArray()) constructor(command: Int, arg0: Int, arg1: Int, data: ByteArray?) : this( command, arg0, arg1, data?.size ?: 0, crc32(data), (command.toLong() xor 0xFFFFFFFF).toInt(), data) fun validate(): Boolean { if (command != magic xor -0x1) return false if (data_length != 0 && crc32(data) != data_crc32) return false return true } fun validateOrThrow() { if (!validate()) throw IllegalArgumentException("bad message ${this.toStringShort()}") } fun toByteArray(): ByteArray { val length = HEADER_LENGTH + (data?.size ?: 0) return ByteBuffer.allocate(length).apply { order(ByteOrder.LITTLE_ENDIAN) putInt(command) putInt(arg0) putInt(arg1) putInt(data_length) putInt(data_crc32) putInt(magic) if (data != null) { put(data) } }.array() } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as AdbMessage if (command != other.command) return false if (arg0 != other.arg0) return false if (arg1 != other.arg1) return false if (data_length != other.data_length) return false if (data_crc32 != other.data_crc32) return false if (magic != other.magic) return false if (data != null) { if (other.data == null) return false if (!data.contentEquals(other.data)) return false } else if (other.data != null) return false return true } override fun hashCode(): Int { var result = command result = 31 * result + arg0 result = 31 * result + arg1 result = 31 * result + data_length result = 31 * result + data_crc32 result = 31 * result + magic result = 31 * result + (data?.contentHashCode() ?: 0) return result } override fun toString(): String { return "AdbMessage(${toStringShort()})" } fun toStringShort(): String { val commandString = when (command) { A_SYNC -> "A_SYNC" A_CNXN -> "A_CNXN" A_AUTH -> "A_AUTH" A_OPEN -> "A_OPEN" A_OKAY -> "A_OKAY" A_CLSE -> "A_CLSE" A_WRTE -> "A_WRTE" A_STLS -> "A_STLS" else -> command.toString() } return "command=$commandString, arg0=$arg0, arg1=$arg1, data_length=$data_length, data_crc32=$data_crc32, magic=$magic, data=${data?.contentToString()}" } companion object { const val HEADER_LENGTH = 24 private fun crc32(data: ByteArray?): Int { if (data == null) return 0 var res = 0 for (b in data) { if (b >= 0) res += b else res += b + 256 } return res } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbPairingClient.kt ================================================ package moe.shizuku.manager.adb import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import com.android.org.conscrypt.Conscrypt import java.io.Closeable import java.io.DataInputStream import java.io.DataOutputStream import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder import javax.net.ssl.SSLSocket private const val TAG = "AdbPairClient" private const val kCurrentKeyHeaderVersion = 1.toByte() private const val kMinSupportedKeyHeaderVersion = 1.toByte() private const val kMaxSupportedKeyHeaderVersion = 1.toByte() private const val kMaxPeerInfoSize = 8192 private const val kMaxPayloadSize = kMaxPeerInfoSize * 2 private const val kExportedKeyLabel = "adb-label\u0000" private const val kExportedKeySize = 64 private const val kPairingPacketHeaderSize = 6 private class PeerInfo( val type: Byte, data: ByteArray) { val data = ByteArray(kMaxPeerInfoSize - 1) init { data.copyInto(this.data, 0, 0, data.size.coerceAtMost(kMaxPeerInfoSize - 1)) } enum class Type(val value: Byte) { ADB_RSA_PUB_KEY(0.toByte()), ADB_DEVICE_GUID(0.toByte()), } fun writeTo(buffer: ByteBuffer) { buffer.run { put(type) put(data) } Log.d(TAG, "write PeerInfo ${toStringShort()}") } override fun toString(): String { return "PeerInfo(${toStringShort()})" } fun toStringShort(): String { return "type=$type, data=${data.contentToString()}" } companion object { fun readFrom(buffer: ByteBuffer): PeerInfo { val type = buffer.get() val data = ByteArray(kMaxPeerInfoSize - 1) buffer.get(data) return PeerInfo(type, data) } } } private class PairingPacketHeader( val version: Byte, val type: Byte, val payload: Int) { enum class Type(val value: Byte) { SPAKE2_MSG(0.toByte()), PEER_INFO(1.toByte()) } fun writeTo(buffer: ByteBuffer) { buffer.run { put(version) put(type) putInt(payload) } Log.d(TAG, "write PairingPacketHeader ${toStringShort()}") } override fun toString(): String { return "PairingPacketHeader(${toStringShort()})" } fun toStringShort(): String { return "version=${version.toInt()}, type=${type.toInt()}, payload=$payload" } companion object { fun readFrom(buffer: ByteBuffer): PairingPacketHeader? { val version = buffer.get() val type = buffer.get() val payload = buffer.int if (version < kMinSupportedKeyHeaderVersion || version > kMaxSupportedKeyHeaderVersion) { Log.e(TAG, "PairingPacketHeader version mismatch (us=$kCurrentKeyHeaderVersion them=${version})") return null } if (type != Type.SPAKE2_MSG.value && type != Type.PEER_INFO.value) { Log.e(TAG, "Unknown PairingPacket type=${type}") return null } if (payload <= 0 || payload > kMaxPayloadSize) { Log.e(TAG, "header payload not within a safe payload size (size=${payload})") return null } val header = PairingPacketHeader(version, type, payload) Log.d(TAG, "read PairingPacketHeader ${header.toStringShort()}") return header } } } private class PairingContext private constructor(private val nativePtr: Long) { val msg: ByteArray init { msg = nativeMsg(nativePtr) } fun initCipher(theirMsg: ByteArray) = nativeInitCipher(nativePtr, theirMsg) fun encrypt(`in`: ByteArray) = nativeEncrypt(nativePtr, `in`) fun decrypt(`in`: ByteArray) = nativeDecrypt(nativePtr, `in`) fun destroy() = nativeDestroy(nativePtr) private external fun nativeMsg(nativePtr: Long): ByteArray private external fun nativeInitCipher(nativePtr: Long, theirMsg: ByteArray): Boolean private external fun nativeEncrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? private external fun nativeDecrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? private external fun nativeDestroy(nativePtr: Long) companion object { fun create(password: ByteArray): PairingContext? { val nativePtr = nativeConstructor(true, password) return if (nativePtr != 0L) PairingContext(nativePtr) else null } @JvmStatic private external fun nativeConstructor(isClient: Boolean, password: ByteArray): Long } } @RequiresApi(Build.VERSION_CODES.R) class AdbPairingClient(private val host: String, private val port: Int, private val pairCode: String, private val key: AdbKey) : Closeable { private enum class State { Ready, ExchangingMsgs, ExchangingPeerInfo, Stopped } private lateinit var socket: Socket private lateinit var inputStream: DataInputStream private lateinit var outputStream: DataOutputStream private val peerInfo: PeerInfo = PeerInfo(PeerInfo.Type.ADB_RSA_PUB_KEY.value, key.adbPublicKey) private lateinit var pairingContext: PairingContext private var state: State = State.Ready fun start(): Boolean { setupTlsConnection() state = State.ExchangingMsgs if (!doExchangeMsgs()) { state = State.Stopped return false } state = State.ExchangingPeerInfo if (!doExchangePeerInfo()) { state = State.Stopped return false } state = State.Stopped return true } private fun setupTlsConnection() { socket = Socket(host, port) socket.tcpNoDelay = true val sslContext = key.sslContext val sslSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket sslSocket.startHandshake() Log.d(TAG, "Handshake succeeded.") inputStream = DataInputStream(sslSocket.inputStream) outputStream = DataOutputStream(sslSocket.outputStream) val pairCodeBytes = pairCode.toByteArray() val keyMaterial = Conscrypt.exportKeyingMaterial(sslSocket, kExportedKeyLabel, null, kExportedKeySize) val passwordBytes = ByteArray(pairCode.length + keyMaterial.size) pairCodeBytes.copyInto(passwordBytes) keyMaterial.copyInto(passwordBytes, pairCodeBytes.size) val pairingContext = PairingContext.create(passwordBytes) checkNotNull(pairingContext) { "Unable to create PairingContext." } this.pairingContext = pairingContext } private fun createHeader(type: PairingPacketHeader.Type, payloadSize: Int): PairingPacketHeader { return PairingPacketHeader(kCurrentKeyHeaderVersion, type.value, payloadSize) } private fun readHeader(): PairingPacketHeader? { val bytes = ByteArray(kPairingPacketHeaderSize) inputStream.readFully(bytes) val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) return PairingPacketHeader.readFrom(buffer) } private fun writeHeader(header: PairingPacketHeader, payload: ByteArray) { val buffer = ByteBuffer.allocate(kPairingPacketHeaderSize).order(ByteOrder.BIG_ENDIAN) header.writeTo(buffer) outputStream.write(buffer.array()) outputStream.write(payload) Log.d(TAG, "write payload, size=${payload.size}") } private fun doExchangeMsgs(): Boolean { val msg = pairingContext.msg val size = msg.size val ourHeader = createHeader(PairingPacketHeader.Type.SPAKE2_MSG, size) writeHeader(ourHeader, msg) val theirHeader = readHeader() ?: return false if (theirHeader.type != PairingPacketHeader.Type.SPAKE2_MSG.value) return false val theirMessage = ByteArray(theirHeader.payload) inputStream.readFully(theirMessage) if (!pairingContext.initCipher(theirMessage)) return false return true } private fun doExchangePeerInfo(): Boolean { val buf = ByteBuffer.allocate(kMaxPeerInfoSize).order(ByteOrder.BIG_ENDIAN) peerInfo.writeTo(buf) val outbuf = pairingContext.encrypt(buf.array()) ?: return false val ourHeader = createHeader(PairingPacketHeader.Type.PEER_INFO, outbuf.size) writeHeader(ourHeader, outbuf) val theirHeader = readHeader() ?: return false if (theirHeader.type != PairingPacketHeader.Type.PEER_INFO.value) return false val theirMessage = ByteArray(theirHeader.payload) inputStream.readFully(theirMessage) val decrypted = pairingContext.decrypt(theirMessage) ?: throw AdbInvalidPairingCodeException() if (decrypted.size != kMaxPeerInfoSize) { Log.e(TAG, "Got size=${decrypted.size} PeerInfo.size=$kMaxPeerInfoSize") return false } val theirPeerInfo = PeerInfo.readFrom(ByteBuffer.wrap(decrypted)) Log.d(TAG, theirPeerInfo.toString()) return true } override fun close() { try { inputStream.close() } catch (e: Throwable) { } try { outputStream.close() } catch (e: Throwable) { } try { socket.close() } catch (e: Exception) { } if (state != State.Ready) { pairingContext.destroy() } } companion object { init { System.loadLibrary("adb") } @JvmStatic external fun available(): Boolean } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbPairingService.kt ================================================ package moe.shizuku.manager.adb import android.annotation.TargetApi import android.app.* import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import android.util.Log import androidx.lifecycle.Observer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import moe.shizuku.manager.R import moe.shizuku.manager.ShizukuSettings import rikka.core.ktx.unsafeLazy import java.net.ConnectException @TargetApi(Build.VERSION_CODES.R) class AdbPairingService : Service() { companion object { const val notificationChannel = "adb_pairing" private const val tag = "AdbPairingService" private const val notificationId = 1 private const val replyRequestId = 1 private const val stopRequestId = 2 private const val retryRequestId = 3 private const val startAction = "start" private const val stopAction = "stop" private const val replyAction = "reply" private const val remoteInputResultKey = "paring_code" private const val portKey = "paring_code" fun startIntent(context: Context): Intent { return Intent(context, AdbPairingService::class.java).setAction(startAction) } private fun stopIntent(context: Context): Intent { return Intent(context, AdbPairingService::class.java).setAction(stopAction) } private fun replyIntent(context: Context, port: Int): Intent { return Intent(context, AdbPairingService::class.java).setAction(replyAction).putExtra(portKey, port) } } private var adbMdns: AdbMdns? = null private val observer = Observer { port -> Log.i(tag, "Pairing service port: $port") if (port <= 0) return@Observer // Since the service could be killed before user finishing input, // we need to put the port into Intent val notification = createInputNotification(port) getSystemService(NotificationManager::class.java).notify(notificationId, notification) } private var started = false override fun onCreate() { super.onCreate() getSystemService(NotificationManager::class.java).createNotificationChannel( NotificationChannel( notificationChannel, getString(R.string.notification_channel_adb_pairing), NotificationManager.IMPORTANCE_HIGH ).apply { setSound(null, null) setShowBadge(false) setAllowBubbles(false) }) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = when (intent?.action) { startAction -> { onStart() } replyAction -> { val code = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(remoteInputResultKey) ?: "" val port = intent.getIntExtra(portKey, -1) if (port != -1) { onInput(code.toString(), port) } else { onStart() } } stopAction -> { stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() null } else -> { return START_NOT_STICKY } } if (notification != null) { try { startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) } catch (e: Throwable) { Log.e(tag, "startForeground failed", e) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { getSystemService(NotificationManager::class.java).notify(notificationId, notification) } } } return START_REDELIVER_INTENT } private fun startSearch() { if (started) return started = true adbMdns = AdbMdns(this, AdbMdns.TLS_PAIRING, observer).apply { start() } } private fun stopSearch() { if (!started) return started = false adbMdns?.stop() } override fun onDestroy() { super.onDestroy() stopSearch() } private fun onStart(): Notification { startSearch() return searchingNotification } private fun onInput(code: String, port: Int): Notification { GlobalScope.launch(Dispatchers.IO) { val host = "127.0.0.1" val key = try { AdbKey(PreferenceAdbKeyStore(ShizukuSettings.getPreferences()), "shizuku") } catch (e: Throwable) { e.printStackTrace() return@launch } AdbPairingClient(host, port, code, key).runCatching { start() }.onFailure { handleResult(false, it) }.onSuccess { handleResult(it, null) } } return workingNotification } private fun handleResult(success: Boolean, exception: Throwable?) { stopForeground(STOP_FOREGROUND_REMOVE) val title: String val text: String? if (success) { Log.i(tag, "Pair succeed") title = getString(R.string.notification_adb_pairing_succeed_title) text = getString(R.string.notification_adb_pairing_succeed_text) stopSearch() } else { title = getString(R.string.notification_adb_pairing_failed_title) text = when (exception) { is ConnectException -> { getString(R.string.cannot_connect_port) } is AdbInvalidPairingCodeException -> { getString(R.string.paring_code_is_wrong) } is AdbKeyException -> { getString(R.string.adb_error_key_store) } else -> { exception?.let { Log.getStackTraceString(it) } } } if (exception != null) { Log.w(tag, "Pair failed", exception) } else { Log.w(tag, "Pair failed") } } getSystemService(NotificationManager::class.java).notify( notificationId, Notification.Builder(this, notificationChannel) .setColor(getColor(R.color.notification)) .setSmallIcon(R.drawable.ic_system_icon) .setContentTitle(title) .setContentText(text) /*.apply { if (!success) { addAction(retryNotificationAction) } }*/ .build() ) stopSelf() } private val stopNotificationAction by unsafeLazy { val pendingIntent = PendingIntent.getService( this, stopRequestId, stopIntent(this), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0 ) Notification.Action.Builder( null, getString(R.string.notification_adb_pairing_stop_searching), pendingIntent ) .build() } private val retryNotificationAction by unsafeLazy { val pendingIntent = PendingIntent.getService( this, retryRequestId, startIntent(this), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0 ) Notification.Action.Builder( null, getString(R.string.notification_adb_pairing_retry), pendingIntent ) .build() } private val replyNotificationAction by unsafeLazy { val remoteInput = RemoteInput.Builder(remoteInputResultKey).run { setLabel(getString(R.string.dialog_adb_pairing_paring_code)) build() } val pendingIntent = PendingIntent.getForegroundService( this, replyRequestId, replyIntent(this, -1), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT ) Notification.Action.Builder( null, getString(R.string.notification_adb_pairing_input_paring_code), pendingIntent ) .addRemoteInput(remoteInput) .build() } private fun replyNotificationAction(port: Int): Notification.Action { // Ensure pending intent is created val action = replyNotificationAction PendingIntent.getForegroundService( this, replyRequestId, replyIntent(this, port), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT ) return action } private val searchingNotification by unsafeLazy { Notification.Builder(this, notificationChannel) .setColor(getColor(R.color.notification)) .setSmallIcon(R.drawable.ic_system_icon) .setContentTitle(getString(R.string.notification_adb_pairing_searching_for_service_title)) .addAction(stopNotificationAction) .build() } private fun createInputNotification(port: Int): Notification { return Notification.Builder(this, notificationChannel) .setColor(getColor(R.color.notification)) .setContentTitle(getString(R.string.notification_adb_pairing_service_found_title)) .setSmallIcon(R.drawable.ic_system_icon) .addAction(replyNotificationAction(port)) .build() } private val workingNotification by unsafeLazy { Notification.Builder(this, notificationChannel) .setColor(getColor(R.color.notification)) .setContentTitle(getString(R.string.notification_adb_pairing_working_title)) .setSmallIcon(R.drawable.ic_system_icon) .build() } override fun onBind(intent: Intent?): IBinder? { return null } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbPairingTutorialActivity.kt ================================================ package moe.shizuku.manager.adb import android.app.AppOpsManager import android.app.ForegroundServiceStartNotAllowedException import android.app.NotificationManager import android.content.ActivityNotFoundException import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.view.isGone import androidx.core.view.isVisible import moe.shizuku.manager.AppConstants import moe.shizuku.manager.app.AppBarActivity import moe.shizuku.manager.databinding.AdbPairingTutorialActivityBinding import rikka.compatibility.DeviceCompatibility @RequiresApi(Build.VERSION_CODES.R) class AdbPairingTutorialActivity : AppBarActivity() { private lateinit var binding: AdbPairingTutorialActivityBinding private var notificationEnabled: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val context = this binding = AdbPairingTutorialActivityBinding.inflate(layoutInflater) setContentView(binding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) notificationEnabled = isNotificationEnabled() if (notificationEnabled) { startPairingService() } binding.apply { syncNotificationEnabled() if (DeviceCompatibility.isMiui()) { miui.isVisible = true } developerOptions.setOnClickListener { val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(":settings:fragment_args_key", "toggle_adb_wireless") try { context.startActivity(intent) } catch (e: ActivityNotFoundException) { } } notificationOptions.setOnClickListener { val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) try { context.startActivity(intent) } catch (e: ActivityNotFoundException) { } } } } private fun syncNotificationEnabled() { binding.apply { step1.isVisible = notificationEnabled step2.isVisible = notificationEnabled step3.isVisible = notificationEnabled network.isVisible = notificationEnabled notification.isVisible = notificationEnabled notificationDisabled.isGone = notificationEnabled } } private fun isNotificationEnabled(): Boolean { val context = this val nm = context.getSystemService(NotificationManager::class.java) val channel = nm.getNotificationChannel(AdbPairingService.notificationChannel) return nm.areNotificationsEnabled() && (channel == null || channel.importance != NotificationManager.IMPORTANCE_NONE) } override fun onResume() { super.onResume() val newNotificationEnabled = isNotificationEnabled() if (newNotificationEnabled != notificationEnabled) { notificationEnabled = newNotificationEnabled syncNotificationEnabled() if (newNotificationEnabled) { startPairingService() } } } private fun startPairingService() { val intent = AdbPairingService.startIntent(this) try { startForegroundService(intent) } catch (e: Throwable) { Log.e(AppConstants.TAG, "startForegroundService", e) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException ) { val mode = getSystemService(AppOpsManager::class.java) .noteOpNoThrow("android:start_foreground", android.os.Process.myUid(), packageName, null, null) if (mode == AppOpsManager.MODE_ERRORED) { Toast.makeText(this, "OP_START_FOREGROUND is denied. What are you doing?", Toast.LENGTH_LONG).show() } startService(intent) } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/adb/AdbProtocol.kt ================================================ package moe.shizuku.manager.adb object AdbProtocol { const val A_SYNC = 0x434e5953 const val A_CNXN = 0x4e584e43 const val A_AUTH = 0x48545541 const val A_OPEN = 0x4e45504f const val A_OKAY = 0x59414b4f const val A_CLSE = 0x45534c43 const val A_WRTE = 0x45545257 const val A_STLS = 0x534C5453 const val A_VERSION = 0x01000000 const val A_MAXDATA = 4096 const val A_STLS_VERSION = 0x01000000 const val ADB_AUTH_TOKEN = 1 const val ADB_AUTH_SIGNATURE = 2 const val ADB_AUTH_RSAPUBLICKEY = 3 } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/app/AppActivity.kt ================================================ package moe.shizuku.manager.app import android.content.res.Resources import android.content.res.Resources.Theme import android.graphics.Color import android.os.Build import androidx.annotation.RequiresApi import moe.shizuku.manager.R import rikka.core.res.isNight import rikka.core.res.resolveColor import rikka.material.app.MaterialActivity abstract class AppActivity : MaterialActivity() { override fun computeUserThemeKey(): String { return ThemeHelper.getTheme(this) + ThemeHelper.isUsingSystemColor() } override fun onApplyUserThemeResource(theme: Theme, isDecorView: Boolean) { if (ThemeHelper.isUsingSystemColor()) { if (resources.configuration.isNight()) theme.applyStyle(R.style.ThemeOverlay_DynamicColors_Dark, true) else theme.applyStyle(R.style.ThemeOverlay_DynamicColors_Light, true) } theme.applyStyle(ThemeHelper.getThemeStyleRes(this), true) } @RequiresApi(Build.VERSION_CODES.M) override fun onApplyTranslucentSystemBars() { super.onApplyTranslucentSystemBars() val window = window val theme = theme if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { window?.decorView?.post { if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 >= Resources.getSystem().displayMetrics.density * 40) { window.navigationBarColor = theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { window.isNavigationBarContrastEnforced = false } } else { window.navigationBarColor = Color.TRANSPARENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { window.isNavigationBarContrastEnforced = true } } } } } override fun onSupportNavigateUp(): Boolean { if (!super.onSupportNavigateUp()) { finish() } return true } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/app/AppBarActivity.kt ================================================ package moe.shizuku.manager.app import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar import com.google.android.material.appbar.AppBarLayout import moe.shizuku.manager.R import rikka.core.ktx.unsafeLazy abstract class AppBarActivity : AppActivity() { private val rootView: ViewGroup by unsafeLazy { findViewById(R.id.root) } private val toolbarContainer: AppBarLayout by unsafeLazy { findViewById(R.id.toolbar_container) } private val toolbar: Toolbar by unsafeLazy { findViewById(R.id.toolbar) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) super.setContentView(getLayoutId()) setSupportActionBar(toolbar) } @LayoutRes open fun getLayoutId(): Int { return R.layout.appbar_activity } override fun setContentView(layoutResID: Int) { layoutInflater.inflate(layoutResID, rootView, true) rootView.bringChildToFront(toolbarContainer) } override fun setContentView(view: View?) { setContentView(view, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) } override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) { rootView.addView(view, 0, params) } @RequiresApi(Build.VERSION_CODES.M) override fun onApplyTranslucentSystemBars() { super.onApplyTranslucentSystemBars() window?.statusBarColor = Color.TRANSPARENT } } abstract class AppBarFragmentActivity : AppBarActivity() { override fun getLayoutId(): Int { return R.layout.appbar_fragment_activity } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/app/ThemeHelper.java ================================================ package moe.shizuku.manager.app; import android.content.Context; import android.os.Build; import androidx.annotation.StyleRes; import moe.shizuku.manager.R; import moe.shizuku.manager.ShizukuSettings; import moe.shizuku.manager.utils.EnvironmentUtils; import rikka.core.util.ResourceUtils; public class ThemeHelper { private static final String THEME_DEFAULT = "DEFAULT"; private static final String THEME_BLACK = "BLACK"; public static final String KEY_LIGHT_THEME = "light_theme"; public static final String KEY_BLACK_NIGHT_THEME = "black_night_theme"; public static final String KEY_USE_SYSTEM_COLOR = "use_system_color"; public static boolean isBlackNightTheme(Context context) { return ShizukuSettings.getPreferences().getBoolean(KEY_BLACK_NIGHT_THEME, EnvironmentUtils.isWatch(context)); } public static boolean isUsingSystemColor() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ShizukuSettings.getPreferences().getBoolean(KEY_USE_SYSTEM_COLOR, true); } public static String getTheme(Context context) { if (isBlackNightTheme(context) && ResourceUtils.isNightMode(context.getResources().getConfiguration())) return THEME_BLACK; return ShizukuSettings.getPreferences().getString(KEY_LIGHT_THEME, THEME_DEFAULT); } @StyleRes public static int getThemeStyleRes(Context context) { switch (getTheme(context)) { case THEME_BLACK: return R.style.ThemeOverlay_Black; case THEME_DEFAULT: default: return R.style.ThemeOverlay; } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/authorization/AuthorizationManager.kt ================================================ package moe.shizuku.manager.authorization import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Parcel import moe.shizuku.manager.BuildConfig import moe.shizuku.manager.Manifest import moe.shizuku.manager.utils.Logger.LOGGER import moe.shizuku.manager.utils.ShizukuSystemApis import rikka.shizuku.server.ServerConstants import rikka.parcelablelist.ParcelableListSlice import rikka.shizuku.Shizuku import java.util.* object AuthorizationManager { private const val FLAG_ALLOWED = 1 shl 1 private const val FLAG_DENIED = 1 shl 2 private const val MASK_PERMISSION = FLAG_ALLOWED or FLAG_DENIED private fun getApplications(userId: Int): List { val data = Parcel.obtain() val reply = Parcel.obtain() return try { data.writeInterfaceToken("moe.shizuku.server.IShizukuService") data.writeInt(userId) try { Shizuku.getBinder()!!.transact(ServerConstants.BINDER_TRANSACTION_getApplications, data, reply, 0) } catch (e: Throwable) { throw RuntimeException(e) } reply.readException() @Suppress("UNCHECKED_CAST") (ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice).list!! } finally { reply.recycle() data.recycle() } } fun getPackages(): List { val packages: MutableList = ArrayList() if (Shizuku.isPreV11() || (Shizuku.getVersion() == 11 && Shizuku.getServerPatchVersion() < 3)) { val allPackages: MutableList = ArrayList() for (user in ShizukuSystemApis.getUsers(useCache = false)) { try { allPackages.addAll(ShizukuSystemApis.getInstalledPackages((PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS).toLong(), user.id)) } catch (e: Throwable) { LOGGER.w(e, "getInstalledPackages") } } for (pi in allPackages) { if (BuildConfig.APPLICATION_ID == pi.packageName) continue if (pi.applicationInfo?.metaData?.getBoolean("moe.shizuku.client.V3_SUPPORT") != true) continue if (pi.requestedPermissions?.contains(Manifest.permission.API_V23) != true) continue packages.add(pi) } } else { packages.addAll(getApplications(-1)) } return packages } fun granted(packageName: String, uid: Int): Boolean { return if (Shizuku.isPreV11()) { ShizukuSystemApis.checkPermission(Manifest.permission.API_V23, packageName, uid / 100000) == PackageManager.PERMISSION_GRANTED } else { (Shizuku.getFlagsForUid(uid, MASK_PERMISSION) and FLAG_ALLOWED) == FLAG_ALLOWED } } fun grant(packageName: String, uid: Int) { if (Shizuku.isPreV11()) { ShizukuSystemApis.grantRuntimePermission(packageName, Manifest.permission.API_V23, uid / 100000) } else { Shizuku.updateFlagsForUid(uid, MASK_PERMISSION, FLAG_ALLOWED) } } fun revoke(packageName: String, uid: Int) { if (Shizuku.isPreV11()) { ShizukuSystemApis.revokeRuntimePermission(packageName, Manifest.permission.API_V23, uid / 100000) } else { Shizuku.updateFlagsForUid(uid, MASK_PERMISSION, 0) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/authorization/RequestPermissionActivity.kt ================================================ package moe.shizuku.manager.authorization import android.app.Dialog import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Bundle import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.app.AppActivity import moe.shizuku.manager.databinding.ConfirmationDialogBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.utils.Logger.LOGGER import rikka.core.res.resolveColor import rikka.html.text.HtmlCompat import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuApiConstants.REQUEST_PERMISSION_REPLY_ALLOWED import rikka.shizuku.ShizukuApiConstants.REQUEST_PERMISSION_REPLY_IS_ONETIME import rikka.shizuku.server.ktx.workerHandler import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException class RequestPermissionActivity : AppActivity() { private lateinit var dialog: Dialog private fun setResult(requestUid: Int, requestPid: Int, requestCode: Int, allowed: Boolean, onetime: Boolean) { val data = Bundle() data.putBoolean(REQUEST_PERMISSION_REPLY_ALLOWED, allowed) data.putBoolean(REQUEST_PERMISSION_REPLY_IS_ONETIME, onetime) try { Shizuku.dispatchPermissionConfirmationResult(requestUid, requestPid, requestCode, data) } catch (e: Throwable) { LOGGER.e("dispatchPermissionConfirmationResult") } } private fun checkSelfPermission(): Boolean { val permission = Shizuku.checkRemotePermission("android.permission.GRANT_RUNTIME_PERMISSIONS") == PackageManager.PERMISSION_GRANTED if (permission) return true val icon = getDrawable(R.drawable.ic_system_icon) icon?.setTint(theme.resolveColor(android.R.attr.colorAccent)) val dialog = MaterialAlertDialogBuilder(this) .setIcon(icon) .setTitle("Shizuku: ${getString(R.string.app_management_dialog_adb_is_limited_title)}") .setMessage(getString(R.string.app_management_dialog_adb_is_limited_message, Helps.ADB.get()).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE)) .setPositiveButton(android.R.string.ok, null) .setOnDismissListener { finish() } .create() dialog.setOnShowListener { (it as AlertDialog).findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() } try { dialog.show() } catch (ignored: Throwable) { } return false } private fun waitForBinder(): Boolean { val countDownLatch = CountDownLatch(1) val listener = object : Shizuku.OnBinderReceivedListener { override fun onBinderReceived() { countDownLatch.countDown() Shizuku.removeBinderReceivedListener(this) } } Shizuku.addBinderReceivedListenerSticky(listener, workerHandler) return try { countDownLatch.await(5, TimeUnit.SECONDS) true } catch (e: TimeoutException) { LOGGER.e(e, "Binder not received in 5s") false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!waitForBinder()) { finish() return } val uid = intent.getIntExtra("uid", -1) val pid = intent.getIntExtra("pid", -1) val requestCode = intent.getIntExtra("requestCode", -1) val ai = intent.getParcelableExtra("applicationInfo") if (uid == -1 || pid == -1 || ai == null) { finish() return } if (!checkSelfPermission()) { setResult(uid, pid, requestCode, allowed = false, onetime = true) return } val label = try { ai.loadLabel(packageManager) } catch (e: Exception) { ai.packageName } val binding = ConfirmationDialogBinding.inflate(layoutInflater).apply { button1.setOnClickListener { setResult(uid, pid, requestCode, allowed = true, onetime = false) dialog.dismiss() } button3.setOnClickListener { setResult(uid, pid, requestCode, allowed = false, onetime = true) dialog.dismiss() } title.text = HtmlCompat.fromHtml(getString(R.string.permission_warning_template, label, getString(R.string.permission_group_description))) } dialog = MaterialAlertDialogBuilder(this) .setView(binding.root) .setCancelable(false) .setOnDismissListener { finish() } .create() dialog.setCanceledOnTouchOutside(false) dialog.show() } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/AdbDialogFragment.kt ================================================ package moe.shizuku.manager.home import android.Manifest.permission.WRITE_SECURE_SETTINGS import android.app.Dialog import android.content.ActivityNotFoundException import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.MutableLiveData import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.R import moe.shizuku.manager.adb.AdbMdns import moe.shizuku.manager.databinding.AdbDialogBinding import moe.shizuku.manager.starter.StarterActivity import moe.shizuku.manager.utils.EnvironmentUtils @RequiresApi(Build.VERSION_CODES.R) class AdbDialogFragment : DialogFragment() { private lateinit var binding: AdbDialogBinding private lateinit var adbMdns: AdbMdns private val port = MutableLiveData() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireContext() binding = AdbDialogBinding.inflate(layoutInflater) adbMdns = AdbMdns(context, AdbMdns.TLS_CONNECT) { port.postValue(it) } val port = EnvironmentUtils.getAdbTcpPort() val builder = MaterialAlertDialogBuilder(context).apply { setTitle(R.string.dialog_adb_discovery) setView(binding.root) setNegativeButton(android.R.string.cancel, null) setPositiveButton(R.string.development_settings, null) if (port != -1) { setNeutralButton("$port", null) } } val dialog = builder.create() dialog.setCanceledOnTouchOutside(false) dialog.setOnShowListener { onDialogShow(dialog) } return dialog } override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) adbMdns.stop() } private fun onDialogShow(dialog: AlertDialog) { adbMdns.start() val context = dialog.context if (context.checkSelfPermission(WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED) { val cr = context.contentResolver Settings.Global.putInt(cr, "adb_wifi_enabled", 1) Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1) Settings.Global.putLong(cr, "adb_allowed_connection_time", 0L) } dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(":settings:fragment_args_key", "toggle_adb_wireless") try { it.context.startActivity(intent) } catch (_: ActivityNotFoundException) { } } dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { startAndDismiss(EnvironmentUtils.getAdbTcpPort()) } port.observe(this) { if (it > 65535 || it < 1) return@observe startAndDismiss(it) } } private fun startAndDismiss(port: Int) { val host = "127.0.0.1" val intent = Intent(context, StarterActivity::class.java).apply { putExtra(StarterActivity.EXTRA_IS_ROOT, false) putExtra(StarterActivity.EXTRA_HOST, host) putExtra(StarterActivity.EXTRA_PORT, port) } requireContext().startActivity(intent) dismissAllowingStateLoss() } fun show(fragmentManager: FragmentManager) { if (fragmentManager.isStateSaved) return show(fragmentManager, javaClass.simpleName) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/AdbPairDialogFragment.kt ================================================ package moe.shizuku.manager.home import android.annotation.SuppressLint import android.app.Dialog import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Build.VERSION_CODES import android.os.Bundle import android.provider.Settings import android.view.Gravity import android.view.LayoutInflater import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import moe.shizuku.manager.R import moe.shizuku.manager.ShizukuSettings import moe.shizuku.manager.adb.* import moe.shizuku.manager.databinding.AdbPairDialogBinding import rikka.lifecycle.viewModels import java.net.ConnectException @RequiresApi(VERSION_CODES.R) class AdbPairDialogFragment : DialogFragment() { private lateinit var binding: AdbPairDialogBinding private val viewModel by viewModels { ViewModel(requireContext()) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireContext() binding = AdbPairDialogBinding.inflate(LayoutInflater.from(context)) val builder = MaterialAlertDialogBuilder(context).apply { setTitle(R.string.dialog_adb_pairing_title) setView(binding.root) setNegativeButton(android.R.string.cancel, null) setPositiveButton(android.R.string.ok, null) setNeutralButton(R.string.development_settings, null) } val dialog = builder.create() dialog.setCanceledOnTouchOutside(false) dialog.setOnShowListener { onDialogShow(dialog) } return dialog } private fun onDialogShow(dialog: AlertDialog) { binding.pairingCode.editText!!.doAfterTextChanged { binding.pairingCode.error = null } binding.pairingCode.error = null dialog.getButton(AlertDialog.BUTTON_POSITIVE).isVisible = false dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(":settings:fragment_args_key", "toggle_adb_wireless") try { it.context.startActivity(intent) } catch (e: ActivityNotFoundException) { } } dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { val context = it.context val port = try { binding.port.editText!!.text.toString().toInt() } catch (e: Exception) { -1 } if (port > 65535 || port < 1) { binding.port.isVisible = true binding.port.error = context.getString(R.string.dialog_adb_invalid_port) return@setOnClickListener } val password = binding.pairingCode.editText!!.text.toString() viewModel.run(port, password) } viewModel.port.observe(this) { if (it == -1) { dialog.setTitle(R.string.dialog_adb_pairing_discovery) binding.text1.isVisible = true binding.pairingCode.isVisible = false binding.port.editText!!.setText(it.toString()) dialog.getButton(AlertDialog.BUTTON_POSITIVE).isVisible = false dialog.getButton(AlertDialog.BUTTON_NEUTRAL).isVisible = true } else { dialog.setTitle(R.string.dialog_adb_pairing_title) binding.text1.isVisible = false binding.pairingCode.isVisible = true binding.port.editText!!.setText(it.toString()) dialog.getButton(AlertDialog.BUTTON_POSITIVE).isVisible = true dialog.getButton(AlertDialog.BUTTON_NEUTRAL).isVisible = false } } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val context = requireContext() val inMultiScreenOrDisplay = (requireActivity().isInMultiWindowMode || (requireActivity().window?.decorView?.display?.displayId ?: -1) > 0) binding.text1.isVisible = inMultiScreenOrDisplay binding.text2.isVisible = !inMultiScreenOrDisplay if (inMultiScreenOrDisplay) { dialog?.setTitle(R.string.dialog_adb_pairing_discovery) } else { dialog?.setTitle(R.string.dialog_adb_pairing_title) } viewModel.result.observe(this) { if (it == null) { dismissAllowingStateLoss() } else { when (it) { is ConnectException -> { binding.port.error = context.getString(R.string.cannot_connect_port) } is AdbInvalidPairingCodeException -> { binding.pairingCode.error = context.getString(R.string.paring_code_is_wrong) } is AdbKeyException -> { Toast.makeText(context, context.getString(R.string.adb_error_key_store), Toast.LENGTH_LONG) .apply { setGravity(Gravity.CENTER, 0, 0) }.show() } } } } } fun show(fragmentManager: FragmentManager) { if (fragmentManager.isStateSaved) return show(fragmentManager, javaClass.simpleName) } override fun getDialog(): AlertDialog? { return super.getDialog() as AlertDialog? } } @SuppressLint("NewApi") private class ViewModel(context: Context) : androidx.lifecycle.ViewModel() { private val _result = MutableLiveData() val result = _result as LiveData private val _port = MutableLiveData() val port = _port as LiveData private val adbMdns: AdbMdns = AdbMdns(context, AdbMdns.TLS_PAIRING) { _port.postValue(it) } init { adbMdns.start() } fun run(port: Int, password: String) { GlobalScope.launch(Dispatchers.IO) { val host = "127.0.0.1" val key = try { AdbKey(PreferenceAdbKeyStore(ShizukuSettings.getPreferences()), "shizuku") } catch (e: Throwable) { e.printStackTrace() _result.postValue(AdbKeyException(e)) return@launch } AdbPairingClient(host, port, password, key).runCatching { start() }.onFailure { _result.postValue(it) it.printStackTrace() }.onSuccess { if (it) { _result.postValue(null) } } } } override fun onCleared() { super.onCleared() adbMdns.stop() } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/AdbPermissionLimitedViewHolder.kt ================================================ package moe.shizuku.manager.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import moe.shizuku.manager.Helps import moe.shizuku.manager.databinding.HomeExtraStepRequiredBinding import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.utils.CustomTabsHelper import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator class AdbPermissionLimitedViewHolder(binding: HomeExtraStepRequiredBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeExtraStepRequiredBinding.inflate(inflater, outer.root, true) AdbPermissionLimitedViewHolder(inner, outer.root) } } init { binding.button1.setOnClickListener { v: View -> CustomTabsHelper.launchUrlOrCopy(v.context, Helps.ADB_PERMISSION.get()) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/HomeActivity.kt ================================================ package moe.shizuku.manager.home import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.os.Process import android.text.method.LinkMovementMethod import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.R import moe.shizuku.manager.ShizukuSettings import moe.shizuku.manager.app.AppBarActivity import moe.shizuku.manager.databinding.AboutDialogBinding import moe.shizuku.manager.databinding.HomeActivityBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.management.appsViewModel import moe.shizuku.manager.settings.SettingsActivity import moe.shizuku.manager.utils.AppIconCache import rikka.core.ktx.unsafeLazy import rikka.lifecycle.Status import rikka.lifecycle.viewModels import rikka.recyclerview.addEdgeSpacing import rikka.recyclerview.addItemSpacing import rikka.recyclerview.fixEdgeEffect import rikka.shizuku.Shizuku abstract class HomeActivity : AppBarActivity() { private val binderReceivedListener = Shizuku.OnBinderReceivedListener { checkServerStatus() appsModel.load() } private val binderDeadListener = Shizuku.OnBinderDeadListener { checkServerStatus() } private val homeModel by viewModels { HomeViewModel() } private val appsModel by appsViewModel() private val adapter by unsafeLazy { HomeAdapter(homeModel, appsModel) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = HomeActivityBinding.inflate(layoutInflater) setContentView(binding.root) homeModel.serviceStatus.observe(this) { if (it.status == Status.SUCCESS) { val status = homeModel.serviceStatus.value?.data ?: return@observe adapter.updateData() ShizukuSettings.setLastLaunchMode(if (status.uid == 0) ShizukuSettings.LaunchMethod.ROOT else ShizukuSettings.LaunchMethod.ADB) } } appsModel.grantedCount.observe(this) { if (it.status == Status.SUCCESS) { adapter.updateData() } } val recyclerView = binding.list recyclerView.adapter = adapter recyclerView.fixEdgeEffect() recyclerView.addItemSpacing(top = 4f, bottom = 4f, unit = TypedValue.COMPLEX_UNIT_DIP) recyclerView.addEdgeSpacing(top = 4f, bottom = 4f, left = 16f, right = 16f, unit = TypedValue.COMPLEX_UNIT_DIP) Shizuku.addBinderReceivedListenerSticky(binderReceivedListener) Shizuku.addBinderDeadListener(binderDeadListener) } override fun onResume() { super.onResume() checkServerStatus() } private fun checkServerStatus() { homeModel.reload() } override fun onDestroy() { super.onDestroy() Shizuku.removeBinderReceivedListener(binderReceivedListener) Shizuku.removeBinderDeadListener(binderDeadListener) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_about -> { val binding = AboutDialogBinding.inflate(LayoutInflater.from(this), null, false) binding.sourceCode.movementMethod = LinkMovementMethod.getInstance() binding.sourceCode.text = getString( R.string.about_view_source_code, "GitHub" ).toHtml() binding.icon.setImageBitmap( AppIconCache.getOrLoadBitmap( this, applicationInfo, Process.myUid() / 100000, resources.getDimensionPixelOffset(R.dimen.default_app_icon_size) ) ) binding.versionName.text = packageManager.getPackageInfo(packageName, 0).versionName MaterialAlertDialogBuilder(this) .setView(binding.root) .show() true } R.id.action_stop -> { if (!Shizuku.pingBinder()) { return true } MaterialAlertDialogBuilder(this) .setMessage(R.string.dialog_stop_message) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> try { Shizuku.exit() } catch (e: Throwable) { } } .setNegativeButton(android.R.string.cancel, null) .show() true } R.id.action_settings -> { startActivity(Intent(this, SettingsActivity::class.java)) true } else -> super.onOptionsItemSelected(item) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/HomeAdapter.kt ================================================ package moe.shizuku.manager.home import android.os.Build import moe.shizuku.manager.management.AppsViewModel import moe.shizuku.manager.utils.EnvironmentUtils import moe.shizuku.manager.utils.UserHandleCompat import rikka.recyclerview.IdBasedRecyclerViewAdapter import rikka.recyclerview.IndexCreatorPool import rikka.shizuku.Shizuku class HomeAdapter(private val homeModel: HomeViewModel, private val appsModel: AppsViewModel) : IdBasedRecyclerViewAdapter(ArrayList()) { init { updateData() setHasStableIds(true) } companion object { private const val ID_STATUS = 0L private const val ID_APPS = 1L private const val ID_TERMINAL = 2L private const val ID_START_ROOT = 3L private const val ID_START_WADB = 4L private const val ID_START_ADB = 5L private const val ID_LEARN_MORE = 6L private const val ID_ADB_PERMISSION_LIMITED = 7L } override fun onCreateCreatorPool(): IndexCreatorPool { return IndexCreatorPool() } fun updateData() { val status = homeModel.serviceStatus.value?.data ?: return val grantedCount = appsModel.grantedCount.value?.data ?: 0 val adbPermission = status.permission val running = status.isRunning val isPrimaryUser = UserHandleCompat.myUserId() == 0 clear() addItem(ServerStatusViewHolder.CREATOR, status, ID_STATUS) if (adbPermission) { addItem(ManageAppsViewHolder.CREATOR, status to grantedCount, ID_APPS) addItem(TerminalViewHolder.CREATOR, status, ID_TERMINAL) } if (running && !adbPermission) { addItem(AdbPermissionLimitedViewHolder.CREATOR, status, ID_ADB_PERMISSION_LIMITED) } if (isPrimaryUser) { val root = EnvironmentUtils.isRooted() val rootRestart = running && status.uid == 0 if (root) { addItem(StartRootViewHolder.CREATOR, rootRestart, ID_START_ROOT) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || EnvironmentUtils.getAdbTcpPort() > 0) { addItem(StartWirelessAdbViewHolder.CREATOR, null, ID_START_WADB) } addItem(StartAdbViewHolder.CREATOR, null, ID_START_ADB) if (!root) { addItem(StartRootViewHolder.CREATOR, rootRestart, ID_START_ROOT) } } addItem(LearnMoreViewHolder.CREATOR, null, ID_LEARN_MORE) notifyDataSetChanged() } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/HomeViewModel.kt ================================================ package moe.shizuku.manager.home import android.content.pm.PackageManager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import moe.shizuku.manager.BuildConfig import moe.shizuku.manager.Manifest import moe.shizuku.manager.model.ServiceStatus import moe.shizuku.manager.utils.Logger.LOGGER import moe.shizuku.manager.utils.ShizukuSystemApis import rikka.lifecycle.Resource import rikka.shizuku.Shizuku class HomeViewModel : ViewModel() { private val _serviceStatus = MutableLiveData>() val serviceStatus = _serviceStatus as LiveData> private fun load(): ServiceStatus { if (!Shizuku.pingBinder()) { return ServiceStatus() } val uid = Shizuku.getUid() val apiVersion = Shizuku.getVersion() val patchVersion = Shizuku.getServerPatchVersion().let { if (it < 0) 0 else it } val seContext = if (apiVersion >= 6) { try { Shizuku.getSELinuxContext() } catch (tr: Throwable) { LOGGER.w(tr, "getSELinuxContext") null } } else null val permissionTest = Shizuku.checkRemotePermission("android.permission.GRANT_RUNTIME_PERMISSIONS") == PackageManager.PERMISSION_GRANTED // Before a526d6bb, server will not exit on uninstall, manager installed later will get not permission // Run a random remote transaction here, report no permission as not running ShizukuSystemApis.checkPermission(Manifest.permission.API_V23, BuildConfig.APPLICATION_ID, 0) return ServiceStatus(uid, apiVersion, patchVersion, seContext, permissionTest) } fun reload() { viewModelScope.launch(Dispatchers.IO) { try { val status = load() _serviceStatus.postValue(Resource.success(status)) } catch (e: CancellationException) { } catch (e: Throwable) { _serviceStatus.postValue(Resource.error(e, ServiceStatus())) } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/LearnMoreViewHolder.kt ================================================ package moe.shizuku.manager.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import moe.shizuku.manager.Helps import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeLearnMoreBinding import moe.shizuku.manager.utils.CustomTabsHelper import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator class LearnMoreViewHolder(binding: HomeLearnMoreBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeLearnMoreBinding.inflate(inflater, outer.root, true) LearnMoreViewHolder(inner, outer.root) } } init { root.setOnClickListener { v: View -> CustomTabsHelper.launchUrlOrCopy(v.context, Helps.HOME.get()) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/ManageAppsViewHolder.kt ================================================ package moe.shizuku.manager.home import android.content.Intent import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeManageAppsItemBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.management.ApplicationManagementActivity import moe.shizuku.manager.model.ServiceStatus import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator class ManageAppsViewHolder(private val binding: HomeManageAppsItemBinding, root: View) : BaseViewHolder>(root), View.OnClickListener { companion object { val CREATOR = Creator> { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeManageAppsItemBinding.inflate(inflater, outer.root, true) ManageAppsViewHolder(inner, outer.root) } } init { root.setOnClickListener(this) } private inline val title get() = binding.text1 private inline val summary get() = binding.text2 override fun onBind() { val context = itemView.context if (!data.first.isRunning) { itemView.isEnabled = false title.setText(R.string.home_app_management_title) summary.text = context.getString( R.string.home_status_service_not_running, context.getString(R.string.app_name) ) } else { itemView.isEnabled = true title.text = context.resources.getQuantityString( R.plurals.home_app_management_authorized_apps_count, data.second, data.second ) summary.text = context.getString(R.string.home_app_management_view_authorized_apps) } } override fun onClick(v: View) { v.context.startActivity(Intent(v.context, ApplicationManagementActivity::class.java)) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/ServerStatusViewHolder.kt ================================================ package moe.shizuku.manager.home import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import moe.shizuku.manager.R import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeServerStatusBinding import moe.shizuku.manager.model.ServiceStatus import rikka.html.text.HtmlCompat import rikka.html.text.toHtml import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuApiConstants import rikka.shizuku.server.ServerConstants class ServerStatusViewHolder(private val binding: HomeServerStatusBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeServerStatusBinding.inflate(inflater, outer.root, true) ServerStatusViewHolder(inner, outer.root) } } private inline val textView get() = binding.text1 private inline val summaryView get() = binding.text2 private inline val iconView get() = binding.icon override fun onBind() { val context = itemView.context val status = data val ok = status.isRunning val isRoot = status.uid == 0 val apiVersion = status.apiVersion val patchVersion = status.patchVersion if (ok) { iconView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_server_ok_24dp)) } else { iconView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_server_error_24dp)) } val user = if (isRoot) "root" else "adb" val title = if (ok) { context.getString(R.string.home_status_service_is_running, context.getString(R.string.app_name)) } else { context.getString(R.string.home_status_service_not_running, context.getString(R.string.app_name)) } val summary = if (ok) { if (apiVersion != Shizuku.getLatestServiceVersion() || status.patchVersion != ShizukuApiConstants.SERVER_PATCH_VERSION) { context.getString( R.string.home_status_service_version_update, user, "${apiVersion}.${patchVersion}", "${Shizuku.getLatestServiceVersion()}.${ShizukuApiConstants.SERVER_PATCH_VERSION}" ) } else { context.getString(R.string.home_status_service_version, user, "${apiVersion}.${patchVersion}") } } else { "" } textView.text = title.toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) summaryView.text = summary.toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) if (TextUtils.isEmpty(summaryView.text)) { summaryView.visibility = View.GONE } else { summaryView.visibility = View.VISIBLE } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/StartAdbViewHolder.kt ================================================ package moe.shizuku.manager.home import android.content.Intent import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeStartAdbBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.starter.Starter import rikka.core.util.ClipboardUtils import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator class StartAdbViewHolder(binding: HomeStartAdbBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeStartAdbBinding.inflate(inflater, outer.root, true) StartAdbViewHolder(inner, outer.root) } } init { binding.button1.setOnClickListener { v: View -> val context = v.context MaterialAlertDialogBuilder(context) .setTitle(R.string.home_adb_button_view_command) .setMessage( HtmlCompat.fromHtml( context.getString( R.string.home_adb_dialog_view_command_message, Starter.adbCommand ) ) ) .setPositiveButton(R.string.home_adb_dialog_view_command_copy_button) { _, _ -> if (ClipboardUtils.put(context, Starter.adbCommand)) { Toast.makeText( context, context.getString(R.string.toast_copied_to_clipboard, Starter.adbCommand), Toast.LENGTH_SHORT ).show() } } .setNegativeButton(android.R.string.cancel, null) .setNeutralButton(R.string.home_adb_dialog_view_command_button_send) { _, _ -> var intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" intent.putExtra(Intent.EXTRA_TEXT, Starter.adbCommand) intent = Intent.createChooser( intent, context.getString(R.string.home_adb_dialog_view_command_button_send) ) context.startActivity(intent) } .show() } binding.text1.movementMethod = LinkMovementMethod.getInstance() binding.text1.text = context.getString(R.string.home_adb_description, Helps.ADB.get()) .toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/StartRootViewHolder.kt ================================================ package moe.shizuku.manager.home import android.content.Intent import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeStartRootBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.starter.StarterActivity import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator import rikka.shizuku.Shizuku class StartRootViewHolder(private val binding: HomeStartRootBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeStartRootBinding.inflate(inflater, outer.root, true) StartRootViewHolder(inner, outer.root) } } private inline val start get() = binding.button1 private inline val restart get() = binding.button2 private var alertDialog: AlertDialog? = null init { val listener = View.OnClickListener { v: View -> onStartClicked(v) } start.setOnClickListener(listener) restart.setOnClickListener(listener) binding.text1.movementMethod = LinkMovementMethod.getInstance() } private fun onStartClicked(v: View) { val context = v.context val intent = Intent(context, StarterActivity::class.java).apply { putExtra(StarterActivity.EXTRA_IS_ROOT, true) } context.startActivity(intent) } override fun onBind() { start.isEnabled = true restart.isEnabled = true if (data!!) { start.visibility = View.GONE restart.visibility = View.VISIBLE } else { start.visibility = View.VISIBLE restart.visibility = View.GONE } val sb = StringBuilder() .append( context.getString( R.string.home_root_description, "Don\'t kill my app!" ) ) if (Shizuku.pingBinder()) { sb.append("

").append( context.getString( R.string.home_root_description_sui, "Sui", "Sui" ) ) } binding.text1.text = sb.toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) } override fun onRecycle() { super.onRecycle() alertDialog = null } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/StartWirelessAdbViewHolder.kt ================================================ package moe.shizuku.manager.home import android.content.Context import android.content.Intent import android.os.Build import android.os.SystemProperties import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.adb.AdbPairingTutorialActivity import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeStartWirelessAdbBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.starter.StarterActivity import moe.shizuku.manager.utils.CustomTabsHelper import moe.shizuku.manager.utils.EnvironmentUtils import rikka.core.content.asActivity import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator import java.net.Inet4Address class StartWirelessAdbViewHolder(binding: HomeStartWirelessAdbBinding, root: View) : BaseViewHolder(root) { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeStartWirelessAdbBinding.inflate(inflater, outer.root, true) StartWirelessAdbViewHolder(inner, outer.root) } } init { binding.button1.setOnClickListener { v: View -> onAdbClicked(v.context) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { binding.button3.setOnClickListener { v: View -> CustomTabsHelper.launchUrlOrCopy(v.context, Helps.ADB_ANDROID11.get()) } binding.button2.setOnClickListener { v: View -> onPairClicked(v.context) } binding.text1.movementMethod = LinkMovementMethod.getInstance() binding.text1.text = context.getString(R.string.home_wireless_adb_description) .toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) } else { binding.text1.text = context.getString(R.string.home_wireless_adb_description_pre_11) .toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) binding.button2.isVisible = false binding.button3.isVisible = false } } override fun onBind(payloads: MutableList) { super.onBind(payloads) } private fun onAdbClicked(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { AdbDialogFragment().show(context.asActivity().supportFragmentManager) return } val port = EnvironmentUtils.getAdbTcpPort() if (port > 0) { val host = "127.0.0.1" val intent = Intent(context, StarterActivity::class.java).apply { putExtra(StarterActivity.EXTRA_IS_ROOT, false) putExtra(StarterActivity.EXTRA_HOST, host) putExtra(StarterActivity.EXTRA_PORT, port) } context.startActivity(intent) } else { WadbNotEnabledDialogFragment().show(context.asActivity().supportFragmentManager) } } @RequiresApi(Build.VERSION_CODES.R) private fun onPairClicked(context: Context) { if ((context.display?.displayId ?: -1) > 0) { // Running in a multi-display environment (e.g., Windows Subsystem for Android), // pairing dialog can be displayed simultaneously with Shizuku. // Input from notification is harder to use under this situation. AdbPairDialogFragment().show(context.asActivity().supportFragmentManager) } else { context.startActivity(Intent(context, AdbPairingTutorialActivity::class.java)) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/TerminalViewHolder.kt ================================================ package moe.shizuku.manager.home import android.content.Intent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import moe.shizuku.manager.R import moe.shizuku.manager.databinding.HomeItemContainerBinding import moe.shizuku.manager.databinding.HomeTerminalBinding import moe.shizuku.manager.model.ServiceStatus import moe.shizuku.manager.shell.ShellTutorialActivity import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator class TerminalViewHolder(private val binding: HomeTerminalBinding, private val root: View) : BaseViewHolder(root), View.OnClickListener { companion object { val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> val outer = HomeItemContainerBinding.inflate(inflater, parent, false) val inner = HomeTerminalBinding.inflate(inflater, outer.root, true) TerminalViewHolder(inner, outer.root) } } init { root.setOnClickListener(this) } private inline val summary get() = binding.text2 override fun onBind() { val context = itemView.context if (!data.isRunning) { root.isEnabled = false summary.text = context.getString(R.string.home_status_service_not_running, context.getString(R.string.app_name)) } else { root.isEnabled = true summary.text = context.getString(R.string.home_terminal_description) } } override fun onClick(v: View) { v.context.startActivity(Intent(v.context, ShellTutorialActivity::class.java)) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/home/WadbNotEnabledDialogFragment.kt ================================================ package moe.shizuku.manager.home import android.app.Dialog import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.R class WadbNotEnabledDialogFragment :DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireContext() return MaterialAlertDialogBuilder(context) .setMessage(R.string.dialog_wireless_adb_not_enabled) .setPositiveButton(android.R.string.ok, null) .create() } fun show(fragmentManager: FragmentManager) { if (fragmentManager.isStateSaved) return show(fragmentManager, javaClass.simpleName) } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ktx/Context.kt ================================================ package moe.shizuku.manager.ktx import android.content.Context import android.os.Build import android.os.UserManager import moe.shizuku.manager.ShizukuApplication val Context.application: ShizukuApplication get() { return applicationContext as ShizukuApplication } fun Context.createDeviceProtectedStorageContextCompat(): Context { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { createDeviceProtectedStorageContext() } else { this } } fun Context.createDeviceProtectedStorageContextCompatWhenLocked(): Context { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && getSystemService(UserManager::class.java)?.isUserUnlocked != true) { createDeviceProtectedStorageContext() } else { this } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ktx/Log.kt ================================================ @file:Suppress("NOTHING_TO_INLINE") package moe.shizuku.manager.ktx import android.util.Log inline val T.TAG: String get() = T::class.java.simpleName.let { if (it.isBlank()) throw IllegalStateException("tag is empty") if (it.length > 23) it.substring(0, 23) else it } inline fun T.logv(message: String, throwable: Throwable? = null) = logv(TAG, message, throwable) inline fun T.logi(message: String, throwable: Throwable? = null) = logi(TAG, message, throwable) inline fun T.logw(message: String, throwable: Throwable? = null) = logw(TAG, message, throwable) inline fun T.logd(message: String, throwable: Throwable? = null) = logd(TAG, message, throwable) inline fun T.loge(message: String, throwable: Throwable? = null) = loge(TAG, message, throwable) inline fun T.logv(tag: String, message: String, throwable: Throwable? = null) = Log.v(tag, message, throwable) inline fun T.logi(tag: String, message: String, throwable: Throwable? = null) = Log.i(tag, message, throwable) inline fun T.logw(tag: String, message: String, throwable: Throwable? = null) = Log.w(tag, message, throwable) inline fun T.logd(tag: String, message: String, throwable: Throwable? = null) = Log.d(tag, message, throwable) inline fun T.loge(tag: String, message: String, throwable: Throwable? = null) = Log.e(tag, message, throwable) ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ktx/PackageManager.kt ================================================ package moe.shizuku.manager.ktx import android.content.ComponentName import android.content.pm.PackageManager fun PackageManager.setComponentEnabled(componentName: ComponentName, enabled: Boolean) { val oldState = getComponentEnabledSetting(componentName) val newState = if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED if (newState != oldState) { val flags = PackageManager.DONT_KILL_APP setComponentEnabledSetting(componentName, newState, flags) } } fun PackageManager.isComponentEnabled(componentName: ComponentName, defaultValue: Boolean = true): Boolean { return when (getComponentEnabledSetting(componentName)) { PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> defaultValue else -> false } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ktx/RecyclerView.kt ================================================ package moe.shizuku.manager.ktx import android.graphics.Canvas import android.widget.EdgeEffect import androidx.recyclerview.widget.RecyclerView class FixedAlwaysClipToPaddingEdgeEffectFactory( private val paddingLeft: Int, private val paddingTop: Int, private val paddingRight: Int, private val paddingBottom: Int ) : RecyclerView.EdgeEffectFactory() { override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { return object : EdgeEffect(view.context) { private var ensureSize = false private fun ensureSize() { if (ensureSize) return ensureSize = true when (direction) { DIRECTION_LEFT -> { setSize(view.measuredHeight - paddingTop - paddingBottom, view.measuredWidth - paddingLeft - paddingRight) } DIRECTION_TOP -> { setSize(view.measuredWidth - paddingLeft - paddingRight, view.measuredHeight - paddingTop - paddingBottom) } DIRECTION_RIGHT -> { setSize(view.measuredHeight - paddingTop - paddingBottom, view.measuredWidth - paddingLeft - paddingRight) } DIRECTION_BOTTOM -> { setSize(view.measuredWidth - paddingLeft - paddingRight, view.measuredHeight - paddingTop - paddingBottom) } } } override fun draw(c: Canvas): Boolean { ensureSize() val restore = c.save() when (direction) { DIRECTION_LEFT -> { c.translate(paddingBottom.toFloat(), 0f) } DIRECTION_TOP -> { c.translate(paddingLeft.toFloat(), paddingTop.toFloat()) } DIRECTION_RIGHT -> { c.translate(-paddingTop.toFloat(), 0f) } DIRECTION_BOTTOM -> { c.translate(paddingRight.toFloat(), paddingBottom.toFloat()) } } val res = super.draw(c) c.restoreToCount(restore) return res } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/ktx/String.kt ================================================ package moe.shizuku.manager.ktx import android.text.Spanned import rikka.html.text.HtmlCompat fun CharSequence.toHtml(flags: Int = 0): Spanned { return HtmlCompat.fromHtml(this.toString(), flags) } fun CharSequence.toHtml(tagHandler: HtmlCompat.TagHandler): Spanned { return HtmlCompat.fromHtml(this.toString(), null, tagHandler) } fun CharSequence.toHtml(flags: Int, tagHandler: HtmlCompat.TagHandler): Spanned { return HtmlCompat.fromHtml(this.toString(), flags, null, tagHandler) } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/legacy/LegacyIsNotSupportedActivity.kt ================================================ package moe.shizuku.manager.legacy import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import com.google.android.material.dialog.MaterialAlertDialogBuilder import moe.shizuku.manager.MainActivity import moe.shizuku.manager.R import moe.shizuku.manager.app.AppActivity import moe.shizuku.manager.ktx.toHtml import rikka.html.text.HtmlCompat class LegacyIsNotSupportedActivity : AppActivity() { companion object { /** * Activity result: user denied request (only API pre-23). */ private inline val RESULT_CANCELED get() = Activity.RESULT_CANCELED /** * Activity result: error, such as manager app itself not authorized. */ private const val RESULT_ERROR = 1 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val callingComponent = callingActivity if (callingComponent == null) { setResult(RESULT_CANCELED) finish() return } val ai = try { packageManager.getApplicationInfo(callingComponent.packageName, PackageManager.GET_META_DATA) } catch (e: Throwable) { finish() return } val label = try { ai.loadLabel(packageManager) } catch (e: Exception) { ai.packageName } val v3Support = ai.metaData?.getBoolean("moe.shizuku.client.V3_SUPPORT") == true if (v3Support) { MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.dialog_requesting_legacy_title, label)) .setMessage(getString(R.string.dialog_requesting_legacy_message, label).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE)) .setPositiveButton(android.R.string.ok, null) .setNeutralButton(R.string.dialog_requesting_legacy_button_open_shizuku) { _, _ -> startActivity(Intent(this, MainActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } .setOnDismissListener { setResult(RESULT_ERROR) finish() } .setCancelable(false) .show() } else { MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.dialog_legacy_not_support_title, label)) .setMessage(getString(R.string.dialog_legacy_not_support_message, label).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE)) .setPositiveButton(android.R.string.ok, null) .setOnDismissListener { setResult(RESULT_ERROR) finish() } .setCancelable(false) .show() } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/legacy/ShellRequestHandlerActivity.kt ================================================ package moe.shizuku.manager.legacy import android.os.Bundle import android.widget.Toast import moe.shizuku.manager.app.AppActivity import moe.shizuku.manager.shell.ShellBinderRequestHandler class ShellRequestHandlerActivity : AppActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ShellBinderRequestHandler.handleRequest(this, intent) finish() } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/management/AppViewHolder.kt ================================================ package moe.shizuku.manager.management import android.content.pm.PackageInfo import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Job import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.authorization.AuthorizationManager import moe.shizuku.manager.databinding.AppListItemBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.utils.AppIconCache import moe.shizuku.manager.utils.ShizukuSystemApis import moe.shizuku.manager.utils.UserHandleCompat import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator import rikka.shizuku.Shizuku class AppViewHolder(private val binding: AppListItemBinding) : BaseViewHolder(binding.root), View.OnClickListener { companion object { @JvmField val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> AppViewHolder(AppListItemBinding.inflate(inflater, parent, false)) } } private val icon get() = binding.icon private val name get() = binding.title private val pkg get() = binding.summary private val switchWidget get() = binding.switchWidget private val root get() = binding.requiresRoot init { itemView.filterTouchesWhenObscured = true itemView.setOnClickListener(this) } private inline val packageName get() = data.packageName private inline val ai get() = data.applicationInfo!! private inline val uid get() = ai.uid private var loadIconJob: Job? = null override fun onClick(v: View) { val context = v.context try { if (AuthorizationManager.granted(packageName, uid)) { AuthorizationManager.revoke(packageName, uid) } else { AuthorizationManager.grant(packageName, uid) } } catch (e: SecurityException) { val uid = try { Shizuku.getUid() } catch (ex: Throwable) { return } if (uid != 0) { val dialog = MaterialAlertDialogBuilder(context) .setTitle(R.string.app_management_dialog_adb_is_limited_title) .setMessage(context.getString(R.string.app_management_dialog_adb_is_limited_message, Helps.ADB.get()).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE)) .setPositiveButton(android.R.string.ok, null) .create() dialog.setOnShowListener { (it as AlertDialog).findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() } try { dialog.show() } catch (ignored: Throwable) { } } } adapter.notifyItemChanged(adapterPosition, Any()) } override fun onBind() { val pm = itemView.context.packageManager val userId = UserHandleCompat.getUserId(uid) icon.setImageDrawable(ai.loadIcon(pm)) name.text = if (userId != UserHandleCompat.myUserId()) { val userInfo = ShizukuSystemApis.getUserInfo(userId) "${ai.loadLabel(pm)} - ${userInfo.name} ($userId)" } else { ai.loadLabel(pm) } pkg.text = ai.packageName switchWidget.isChecked = AuthorizationManager.granted(packageName, uid) root.visibility = if (ai.metaData != null && ai.metaData.getBoolean("moe.shizuku.client.V3_REQUIRES_ROOT")) View.VISIBLE else View.GONE loadIconJob = AppIconCache.loadIconBitmapAsync(context, ai, ai.uid / 100000, icon) } override fun onBind(payloads: List) { switchWidget.isChecked = AuthorizationManager.granted(packageName, uid) } override fun onRecycle() { if (loadIconJob?.isActive == true) { loadIconJob?.cancel() } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/management/ApplicationManagementActivity.kt ================================================ package moe.shizuku.manager.management import android.os.Bundle import android.util.TypedValue import android.view.MenuItem import android.widget.Toast import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.app.AppBarActivity import moe.shizuku.manager.databinding.AppsActivityBinding import moe.shizuku.manager.utils.CustomTabsHelper import rikka.lifecycle.Status import rikka.recyclerview.addEdgeSpacing import rikka.recyclerview.fixEdgeEffect import rikka.shizuku.Shizuku import java.util.* class ApplicationManagementActivity : AppBarActivity() { private val viewModel by appsViewModel() private val adapter = AppsAdapter() private val binderDeadListener = Shizuku.OnBinderDeadListener { if (!isFinishing) { finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!Shizuku.pingBinder()) { finish() return } val binding = AppsActivityBinding.inflate(layoutInflater) setContentView(binding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) viewModel.packages.observe(this) { when (it.status) { Status.SUCCESS -> { adapter.updateData(it.data) } Status.ERROR -> { finish() val tr = it.error Toast.makeText(this, Objects.toString(tr, "unknown"), Toast.LENGTH_SHORT).show() tr.printStackTrace() } Status.LOADING -> { } } } if (viewModel.packages.value == null) { viewModel.load() } val recyclerView = binding.list recyclerView.adapter = adapter recyclerView.fixEdgeEffect() recyclerView.addEdgeSpacing(top = 8f, bottom = 8f, unit = TypedValue.COMPLEX_UNIT_DIP) adapter.registerAdapterDataObserver(object : AdapterDataObserver() { override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { viewModel.load(true) } }) Shizuku.addBinderDeadListener(binderDeadListener) } override fun onDestroy() { super.onDestroy() Shizuku.removeBinderDeadListener(binderDeadListener) } override fun onResume() { super.onResume() adapter.notifyDataSetChanged() } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/management/AppsAdapter.java ================================================ package moe.shizuku.manager.management; import android.content.pm.PackageInfo; import java.util.List; import rikka.recyclerview.BaseRecyclerViewAdapter; import rikka.recyclerview.ClassCreatorPool; public class AppsAdapter extends BaseRecyclerViewAdapter { public AppsAdapter() { super(); getCreatorPool().putRule(PackageInfo.class, AppViewHolder.CREATOR); getCreatorPool().putRule(Object.class, EmptyViewHolder.CREATOR); setHasStableIds(true); } @Override public long getItemId(int position) { return getItemAt(position).hashCode(); } @Override public ClassCreatorPool onCreateCreatorPool() { return new ClassCreatorPool(); } public void updateData(List data) { getItems().clear(); if (data.isEmpty()) { getItems().add(new Object()); } else { getItems().addAll(data); } notifyDataSetChanged(); } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/management/AppsViewModel.kt ================================================ package moe.shizuku.manager.management import android.content.Context import android.content.pm.PackageInfo import androidx.activity.ComponentActivity import androidx.annotation.MainThread import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import moe.shizuku.manager.authorization.AuthorizationManager import rikka.lifecycle.Resource import rikka.lifecycle.activityViewModels import rikka.lifecycle.viewModels @MainThread fun ComponentActivity.appsViewModel() = viewModels { AppsViewModel(this) } @MainThread fun Fragment.appsViewModel() = activityViewModels { AppsViewModel(requireContext()) } class AppsViewModel(context: Context) : ViewModel() { private val _packages = MutableLiveData>>() val packages = _packages as LiveData>> private val _grantedCount = MutableLiveData>() val grantedCount = _grantedCount as LiveData> fun load(onlyCount: Boolean = false) { viewModelScope.launch(Dispatchers.IO) { try { val list: MutableList = ArrayList() var count = 0 for (pi in AuthorizationManager.getPackages()) { list.add(pi) if (AuthorizationManager.granted(pi.packageName, pi.applicationInfo!!.uid)) count++ } if (!onlyCount) _packages.postValue(Resource.success(list)) _grantedCount.postValue(Resource.success(count)) } catch (e: CancellationException) { } catch (e: Throwable) { _packages.postValue(Resource.error(e, null)) _grantedCount.postValue(Resource.error(e, 0)) } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/management/EmptyViewHolder.kt ================================================ package moe.shizuku.manager.management import android.content.pm.PackageInfo import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Job import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.authorization.AuthorizationManager import moe.shizuku.manager.databinding.AppListEmptyBinding import moe.shizuku.manager.databinding.AppListItemBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.utils.AppIconCache import moe.shizuku.manager.utils.ShizukuSystemApis import moe.shizuku.manager.utils.UserHandleCompat import rikka.html.text.HtmlCompat import rikka.recyclerview.BaseViewHolder import rikka.recyclerview.BaseViewHolder.Creator import rikka.shizuku.Shizuku class EmptyViewHolder(private val binding: AppListEmptyBinding) : BaseViewHolder(binding.root) { companion object { @JvmField val CREATOR = Creator { inflater: LayoutInflater, parent: ViewGroup? -> EmptyViewHolder(AppListEmptyBinding.inflate(inflater, parent, false)) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/model/ServiceStatus.kt ================================================ package moe.shizuku.manager.model import rikka.shizuku.Shizuku data class ServiceStatus( val uid: Int = -1, val apiVersion: Int = -1, val patchVersion: Int = -1, val seContext: String? = null, val permission: Boolean = false ) { val isRunning: Boolean get() = uid != -1 && Shizuku.pingBinder() } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/receiver/BootCompleteReceiver.kt ================================================ package moe.shizuku.manager.receiver import android.Manifest.permission.WRITE_SECURE_SETTINGS import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi import com.topjohnwu.superuser.Shell import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import moe.shizuku.manager.AppConstants import moe.shizuku.manager.ShizukuSettings import moe.shizuku.manager.ShizukuSettings.LaunchMethod import moe.shizuku.manager.adb.AdbClient import moe.shizuku.manager.adb.AdbKey import moe.shizuku.manager.adb.AdbMdns import moe.shizuku.manager.adb.PreferenceAdbKeyStore import moe.shizuku.manager.starter.Starter import moe.shizuku.manager.utils.UserHandleCompat import rikka.shizuku.Shizuku import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit class BootCompleteReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_LOCKED_BOOT_COMPLETED != intent.action && Intent.ACTION_BOOT_COMPLETED != intent.action) { return } if (UserHandleCompat.myUserId() > 0 || Shizuku.pingBinder()) return if (ShizukuSettings.getLastLaunchMode() == LaunchMethod.ROOT) { rootStart(context) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU // https://r.android.com/2128832 && context.checkSelfPermission(WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED && ShizukuSettings.getLastLaunchMode() == LaunchMethod.ADB) { adbStart(context) } else { Log.w(AppConstants.TAG, "No support start on boot") } } private fun rootStart(context: Context) { if (!Shell.getShell().isRoot) { //NotificationHelper.notify(context, AppConstants.NOTIFICATION_ID_STATUS, AppConstants.NOTIFICATION_CHANNEL_STATUS, R.string.notification_service_start_no_root) Shell.getCachedShell()?.close() return } Shell.cmd(Starter.internalCommand).exec() } @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun adbStart(context: Context) { val cr = context.contentResolver Settings.Global.putInt(cr, "adb_wifi_enabled", 1) Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1) Settings.Global.putLong(cr, "adb_allowed_connection_time", 0L) val pending = goAsync() CoroutineScope(Dispatchers.IO).launch { val latch = CountDownLatch(1) val adbMdns = AdbMdns(context, AdbMdns.TLS_CONNECT) { port -> if (port <= 0) return@AdbMdns try { val keystore = PreferenceAdbKeyStore(ShizukuSettings.getPreferences()) val key = AdbKey(keystore, "shizuku") val client = AdbClient("127.0.0.1", port, key) client.connect() client.shellCommand(Starter.internalCommand, null) client.close() } catch (_: Exception) { } latch.countDown() } if (Settings.Global.getInt(cr, "adb_wifi_enabled", 0) == 1) { adbMdns.start() latch.await(3, TimeUnit.SECONDS) adbMdns.stop() } pending.finish() } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/receiver/ShizukuReceiver.kt ================================================ package moe.shizuku.manager.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import moe.shizuku.manager.shell.ShellBinderRequestHandler class ShizukuReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if ("rikka.shizuku.intent.action.REQUEST_BINDER" == intent.action) { ShellBinderRequestHandler.handleRequest(context, intent) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/settings/IntegerSimpleMenuPreference.java ================================================ package moe.shizuku.manager.settings; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.ContextThemeWrapper; import android.view.View; import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; import androidx.annotation.StyleableRes; import androidx.core.content.res.TypedArrayUtils; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import moe.shizuku.manager.R; import rikka.preference.simplemenu.SimpleMenuPopupWindow; /** * a {@link rikka.preference.SimpleMenuPreference} to implement night mode in user interface settings. * a {@link rikka.preference.SimpleMenuPreference} which use integer values array as entryValues. * * @author Haruue Icymoon haruue@caoyue.com.cn */ @SuppressLint("RestrictedApi") public class IntegerSimpleMenuPreference extends Preference { private final SimpleMenuPopupWindow mPopupWindow; private View mAnchor; private View mItemView; private CharSequence[] mEntries; private int[] mEntryValues; private int mValue; private String mSummary; private boolean mValueSet; @SuppressLint("RestrictedApi") public IntegerSimpleMenuPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.ListPreference, defStyleAttr, defStyleRes); mEntries = TypedArrayUtils.getTextArray(a, R.styleable.ListPreference_entries, R.styleable.ListPreference_android_entries); mEntryValues = getIntArray(a, R.styleable.ListPreference_entryValues, R.styleable.ListPreference_android_entryValues); a.recycle(); /* Retrieve the Preference summary attribute since it's private * in the Preference class. */ a = context.obtainStyledAttributes(attrs, R.styleable.Preference, defStyleAttr, defStyleRes); mSummary = TypedArrayUtils.getString(a, R.styleable.Preference_summary, R.styleable.Preference_android_summary); a.recycle(); a = context.obtainStyledAttributes( attrs, R.styleable.SimpleMenuPreference, defStyleAttr, defStyleRes); int popupStyle = a.getResourceId(R.styleable.SimpleMenuPreference_android_popupMenuStyle, R.style.Widget_Preference_SimpleMenuPreference_PopupMenu); int popupTheme = a.getResourceId(R.styleable.SimpleMenuPreference_android_popupTheme,R.style.ThemeOverlay_Preference_SimpleMenuPreference_PopupMenu); Context popupContext; if (popupTheme != 0) { popupContext = new ContextThemeWrapper(context, popupTheme); } else { popupContext = context; } mPopupWindow = new SimpleMenuPopupWindow(popupContext, attrs, R.styleable.SimpleMenuPreference_android_popupMenuStyle, popupStyle); mPopupWindow.setOnItemClickListener(i -> { int value = getEntryValues()[i]; if (callChangeListener(value)) { setValue(value); } }); a.recycle(); } public IntegerSimpleMenuPreference(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, R.style.Preference_SimpleMenuPreference); } public IntegerSimpleMenuPreference(Context context, AttributeSet attrs) { this(context, attrs, R.attr.simpleMenuPreferenceStyle); } public IntegerSimpleMenuPreference(Context context) { this(context, null); } @SuppressLint("RestrictedApi") private static int[] getIntArray(TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex) { int resourceId = TypedArrayUtils.getResourceId(a, index, fallbackIndex, 0); return a.getResources().getIntArray(resourceId); } @Override protected void onClick() { if (getEntries() == null || getEntries().length == 0) { return; } if (mPopupWindow == null) { return; } mPopupWindow.setEntries(getEntries()); mPopupWindow.setSelectedIndex(findIndexOfValue(getValue())); View container = (View) mItemView // itemView .getParent(); // -> list (RecyclerView) mPopupWindow.show(mItemView, container, (int) mAnchor.getX()); } /** * Sets the human-readable entries to be shown in the list. This will be * shown in subsequent dialogs. *

* Each entry must have a corresponding index in * {@link #setEntryValues(int[])}. * * @param entries The entries. * @see #setEntryValues(int[]) */ public void setEntries(CharSequence[] entries) { mEntries = entries; mPopupWindow.requestMeasure(); } /** * @param entriesResId The entries array as a resource. * @see #setEntries(CharSequence[]) */ public void setEntries(@ArrayRes int entriesResId) { setEntries(getContext().getResources().getTextArray(entriesResId)); } /** * The list of entries to be shown in the list in subsequent dialogs. * * @return The list as an array. */ public CharSequence[] getEntries() { return mEntries; } /** * The array to find the value to save for a preference when an entry from * entries is selected. If a user clicks on the second item in entries, the * second item in this array will be saved to the preference. * * @param entryValues The array to be used as values to save for the preference. */ public void setEntryValues(int[] entryValues) { mEntryValues = entryValues; } /** * @param entryValuesResId The entry values array as a resource. * @see #setEntryValues(int[]) */ public void setEntryValues(@ArrayRes int entryValuesResId) { setEntryValues(getContext().getResources().getIntArray(entryValuesResId)); } /** * Returns the array of values to be saved for the preference. * * @return The array of values. */ public int[] getEntryValues() { return mEntryValues; } /** * Sets the value of the key. This should be one of the entries in * {@link #getEntryValues()}. * * @param value The value to set for the key. */ public void setValue(int value) { // Always persist/notify the first time. final boolean changed = mValue != value; if (changed || !mValueSet) { mValue = value; mValueSet = true; persistInt(value); if (changed) { notifyChanged(); } } } /** * Returns the summary of this ListPreference. If the summary * has a {@linkplain java.lang.String#format String formatting} * marker in it (i.e. "%s" or "%1$s"), then the current entry * value will be substituted in its place. * * @return the summary with appropriate string substitution */ @Override public CharSequence getSummary() { final CharSequence entry = getEntry(); if (mSummary == null) { return super.getSummary(); } else { return String.format(mSummary, entry == null ? "" : entry); } } /** * Sets the summary for this Preference with a CharSequence. * If the summary has a * {@linkplain java.lang.String#format String formatting} * marker in it (i.e. "%s" or "%1$s"), then the current entry * value will be substituted in its place when it's retrieved. * * @param summary The summary for the preference. */ @Override public void setSummary(CharSequence summary) { super.setSummary(summary); if (summary == null && mSummary != null) { mSummary = null; } else if (summary != null && !summary.equals(mSummary)) { mSummary = summary.toString(); } } /** * Sets the value to the given index from the entry values. * * @param index The index of the value to set. */ public void setValueIndex(int index) { if (mEntryValues != null) { setValue(mEntryValues[index]); } } /** * Returns the value of the key. This should be one of the entries in * {@link #getEntryValues()}. * * @return The value of the key. */ public int getValue() { return mValue; } /** * Returns the entry corresponding to the current value. * * @return The entry corresponding to the current value, or null. */ public CharSequence getEntry() { int index = getValueIndex(); return index >= 0 && mEntries != null ? mEntries[index] : null; } /** * Returns the index of the given value (in the entry values array). * * @param value The value whose index should be returned. * @return The index of the value, or -1 if not found. */ public int findIndexOfValue(int value) { if (mEntryValues != null) { for (int i = mEntryValues.length - 1; i >= 0; i--) { int entryValue = mEntryValues[i]; if (entryValue == value) { return i; } } } return -1; } private int getValueIndex() { return findIndexOfValue(mValue); } @Override protected Object onGetDefaultValue(TypedArray a, int index) { return a.getInt(index, 1); } @Override protected void onSetInitialValue(Object defaultValue) { if (defaultValue == null) { defaultValue = 0; } setValue(getPersistedInt((Integer) defaultValue)); } @Override protected Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); if (isPersistent()) { // No need to save instance state since it's persistent return superState; } final SavedState myState = new SavedState(superState); myState.value = getValue(); return myState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state == null || !state.getClass().equals(SavedState.class)) { // Didn't save state for us in onSaveInstanceState super.onRestoreInstanceState(state); return; } SavedState myState = (SavedState) state; super.onRestoreInstanceState(myState.getSuperState()); setValue(myState.value); } private static class SavedState extends BaseSavedState { int value; public SavedState(Parcel source) { super(source); value = source.readInt(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(value); } public SavedState(Parcelable superState) { super(superState); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); mItemView = holder.itemView; mAnchor = holder.itemView.findViewById(android.R.id.empty); if (mAnchor == null) { throw new IllegalStateException("SimpleMenuPreference item layout must contain" + "a view id is android.R.id.empty to support iconSpaceReserved"); } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/settings/SettingsActivity.kt ================================================ package moe.shizuku.manager.settings import android.content.res.Resources import android.os.Bundle import moe.shizuku.manager.R import moe.shizuku.manager.app.AppBarFragmentActivity class SettingsActivity : AppBarFragmentActivity() { override fun onApplyUserThemeResource(theme: Resources.Theme, isDecorView: Boolean) { super.onApplyUserThemeResource(theme, isDecorView) theme.applyStyle(R.style.ThemeOverlay_Rikka_Material3_Preference, true) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.setDisplayHomeAsUpEnabled(true) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, SettingsFragment()) .commit() } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/settings/SettingsFragment.kt ================================================ package moe.shizuku.manager.settings import android.content.ComponentName import android.content.Context import android.os.Build import android.os.Bundle import android.text.TextUtils import android.util.TypedValue import android.view.LayoutInflater import android.view.ViewGroup import android.widget.FrameLayout import androidx.appcompat.app.AppCompatDelegate import androidx.preference.* import androidx.recyclerview.widget.RecyclerView import moe.shizuku.manager.R import moe.shizuku.manager.ShizukuSettings import moe.shizuku.manager.ShizukuSettings.KEEP_START_ON_BOOT import moe.shizuku.manager.app.ThemeHelper import moe.shizuku.manager.app.ThemeHelper.KEY_BLACK_NIGHT_THEME import moe.shizuku.manager.app.ThemeHelper.KEY_USE_SYSTEM_COLOR import moe.shizuku.manager.ktx.isComponentEnabled import moe.shizuku.manager.ktx.setComponentEnabled import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.receiver.BootCompleteReceiver import moe.shizuku.manager.utils.CustomTabsHelper import rikka.core.util.ResourceUtils import rikka.material.app.LocaleDelegate import rikka.recyclerview.addEdgeSpacing import rikka.recyclerview.fixEdgeEffect import rikka.shizuku.manager.ShizukuLocales import rikka.widget.borderview.BorderRecyclerView import java.util.* import moe.shizuku.manager.ShizukuSettings.LANGUAGE as KEY_LANGUAGE import moe.shizuku.manager.ShizukuSettings.NIGHT_MODE as KEY_NIGHT_MODE class SettingsFragment : PreferenceFragmentCompat() { private lateinit var languagePreference: ListPreference private lateinit var nightModePreference: IntegerSimpleMenuPreference private lateinit var blackNightThemePreference: TwoStatePreference private lateinit var startOnBootPreference: TwoStatePreference private lateinit var startupPreference: PreferenceCategory private lateinit var translationPreference: Preference private lateinit var translationContributorsPreference: Preference private lateinit var useSystemColorPreference: TwoStatePreference override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { val context = requireContext() preferenceManager.setStorageDeviceProtected() preferenceManager.sharedPreferencesName = ShizukuSettings.NAME preferenceManager.sharedPreferencesMode = Context.MODE_PRIVATE setPreferencesFromResource(R.xml.settings, null) languagePreference = findPreference(KEY_LANGUAGE)!! nightModePreference = findPreference(KEY_NIGHT_MODE)!! blackNightThemePreference = findPreference(KEY_BLACK_NIGHT_THEME)!! startOnBootPreference = findPreference(KEEP_START_ON_BOOT)!! startupPreference = findPreference("startup")!! translationPreference = findPreference("translation")!! translationContributorsPreference = findPreference("translation_contributors")!! useSystemColorPreference = findPreference(KEY_USE_SYSTEM_COLOR)!! val componentName = ComponentName(context.packageName, BootCompleteReceiver::class.java.name) startOnBootPreference.isChecked = context.packageManager.isComponentEnabled(componentName) startOnBootPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> if (newValue is Boolean) { context.packageManager.setComponentEnabled(componentName, newValue) context.packageManager.isComponentEnabled(componentName) == newValue } else false } languagePreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> if (newValue is String) { val locale: Locale = if ("SYSTEM" == newValue) { LocaleDelegate.systemLocale } else { Locale.forLanguageTag(newValue) } LocaleDelegate.defaultLocale = locale activity?.recreate() } true } setupLocalePreference() nightModePreference.value = ShizukuSettings.getNightMode() nightModePreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, value: Any? -> if (value is Int) { if (ShizukuSettings.getNightMode() != value) { AppCompatDelegate.setDefaultNightMode(value) activity?.recreate() } } true } if (ShizukuSettings.getNightMode() != AppCompatDelegate.MODE_NIGHT_NO) { blackNightThemePreference.isChecked = ThemeHelper.isBlackNightTheme(context) blackNightThemePreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? -> if (ResourceUtils.isNightMode(context.resources.configuration)) { activity?.recreate() } true } } else { blackNightThemePreference.isVisible = false } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { useSystemColorPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, value: Any? -> if (value is Boolean) { if (ThemeHelper.isUsingSystemColor() != value) { activity?.recreate() } } true } } else { useSystemColorPreference.isVisible = false } translationPreference.summary = context.getString(R.string.settings_translation_summary, context.getString(R.string.app_name)) translationPreference.setOnPreferenceClickListener { CustomTabsHelper.launchUrlOrCopy(context, context.getString(R.string.translation_url)) true } val contributors = context.getString(R.string.translation_contributors).toHtml().toString() if (contributors.isNotBlank()) { translationContributorsPreference.summary = contributors } else { translationContributorsPreference.isVisible = false } } override fun onCreateRecyclerView( inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle? ): RecyclerView { val recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState) as BorderRecyclerView recyclerView.fixEdgeEffect() recyclerView.addEdgeSpacing(bottom = 8f, unit = TypedValue.COMPLEX_UNIT_DIP) val lp = recyclerView.layoutParams if (lp is FrameLayout.LayoutParams) { lp.rightMargin = recyclerView.context.resources.getDimension(R.dimen.rd_activity_horizontal_margin).toInt() lp.leftMargin = lp.rightMargin } return recyclerView } private fun setupLocalePreference() { val localeTags = ShizukuLocales.LOCALES val displayLocaleTags = ShizukuLocales.DISPLAY_LOCALES languagePreference.entries = displayLocaleTags languagePreference.entryValues = localeTags val currentLocaleTag = languagePreference.value val currentLocaleIndex = localeTags.indexOf(currentLocaleTag) val currentLocale = ShizukuSettings.getLocale() val localizedLocales = mutableListOf() for ((index, displayLocale) in displayLocaleTags.withIndex()) { if (index == 0) { localizedLocales.add(getString(R.string.follow_system)) continue } val locale = Locale.forLanguageTag(displayLocale.toString()) val localeName = if (!TextUtils.isEmpty(locale.script)) locale.getDisplayScript(locale) else locale.getDisplayName(locale) val localizedLocaleName = if (!TextUtils.isEmpty(locale.script)) locale.getDisplayScript(currentLocale) else locale.getDisplayName(currentLocale) localizedLocales.add( if (index != currentLocaleIndex) { "$localeName
$localizedLocaleName".toHtml() } else { localizedLocaleName } ) } languagePreference.entries = localizedLocales.toTypedArray() languagePreference.summary = when { TextUtils.isEmpty(currentLocaleTag) || "SYSTEM" == currentLocaleTag -> { getString(R.string.follow_system) } currentLocaleIndex != -1 -> { val localizedLocale = localizedLocales[currentLocaleIndex] val newLineIndex = localizedLocale.indexOf('\n') if (newLineIndex == -1) { localizedLocale.toString() } else { localizedLocale.subSequence(0, newLineIndex).toString() } } else -> { "" } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/shell/Shell.java ================================================ package moe.shizuku.manager.shell; import android.content.pm.PackageManager; import android.os.Handler; import android.os.IBinder; import rikka.rish.Rish; import rikka.rish.RishConfig; import rikka.shizuku.Shizuku; import rikka.shizuku.ShizukuApiConstants; public class Shell extends Rish { @Override public void requestPermission(Runnable onGrantedRunnable) { if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { onGrantedRunnable.run(); } else if (Shizuku.shouldShowRequestPermissionRationale()) { System.err.println("Permission denied"); System.err.flush(); System.exit(1); } else { Shizuku.addRequestPermissionResultListener(new Shizuku.OnRequestPermissionResultListener() { @Override public void onRequestPermissionResult(int requestCode, int grantResult) { Shizuku.removeRequestPermissionResultListener(this); if (grantResult == PackageManager.PERMISSION_GRANTED) { onGrantedRunnable.run(); } else { System.err.println("Permission denied"); System.err.flush(); System.exit(1); } } }); Shizuku.requestPermission(0); } } public static void main(String[] args, String packageName, IBinder binder, Handler handler) { RishConfig.init(binder, ShizukuApiConstants.BINDER_DESCRIPTOR, 30000); Shizuku.onBinderReceived(binder, packageName); Shizuku.addBinderReceivedListenerSticky(() -> { int version = Shizuku.getVersion(); if (version < 12) { System.err.println("Rish requires server 12 (running " + version + ")"); System.err.flush(); System.exit(1); } new Shell().start(args); }); } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/shell/ShellBinderRequestHandler.kt ================================================ package moe.shizuku.manager.shell import android.content.Context import android.content.Intent import android.os.IBinder import android.os.Parcel import moe.shizuku.manager.utils.Logger.LOGGER import rikka.shizuku.Shizuku object ShellBinderRequestHandler { fun handleRequest(context: Context, intent: Intent): Boolean { if (intent.action != "rikka.shizuku.intent.action.REQUEST_BINDER") { return false } val binder = intent.getBundleExtra("data")?.getBinder("binder") ?: return false val shizukuBinder = Shizuku.getBinder() if (shizukuBinder == null) { LOGGER.w("Binder not received or Shizuku service not running") } val data = Parcel.obtain() return try { data.writeStrongBinder(shizukuBinder) data.writeString(context.applicationInfo.sourceDir) binder.transact(1, data, null, IBinder.FLAG_ONEWAY) true } catch (e: Throwable) { e.printStackTrace() false } finally { data.recycle() } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/shell/ShellTutorialActivity.kt ================================================ package moe.shizuku.manager.shell import android.net.Uri import android.os.Bundle import android.provider.DocumentsContract import android.view.View import androidx.activity.result.contract.ActivityResultContracts import moe.shizuku.manager.Helps import moe.shizuku.manager.R import moe.shizuku.manager.app.AppBarActivity import moe.shizuku.manager.databinding.TerminalTutorialActivityBinding import moe.shizuku.manager.ktx.toHtml import moe.shizuku.manager.utils.CustomTabsHelper import rikka.html.text.HtmlCompat import rikka.insets.* import kotlin.math.roundToInt class ShellTutorialActivity : AppBarActivity() { companion object { private val SH_NAME = "rish" private val DEX_NAME = "rish_shizuku.dex" } private val openDocumentsTree = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { tree: Uri? -> if (tree == null) return@registerForActivityResult val cr = contentResolver val doc = DocumentsContract.buildDocumentUriUsingTree(tree, DocumentsContract.getTreeDocumentId(tree)) val child = DocumentsContract.buildChildDocumentsUriUsingTree(tree, DocumentsContract.getTreeDocumentId(tree)) cr.query( child, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME), null, null, null )?.use { while (it.moveToNext()) { val id = it.getString(0) val name = it.getString(1) if (name == SH_NAME || name == DEX_NAME) { DocumentsContract.deleteDocument(cr, DocumentsContract.buildDocumentUriUsingTree(tree, id)) } } } fun writeToDocument(name: String) { DocumentsContract.createDocument(contentResolver, doc, "application/octet-stream", name)?.runCatching { cr.openOutputStream(this)?.let { assets.open(name).copyTo(it) } } } writeToDocument(SH_NAME) writeToDocument(DEX_NAME) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = TerminalTutorialActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.content.apply { setInitialPadding( initialPaddingLeft, initialPaddingTop + (resources.displayMetrics.density * 8).roundToInt(), initialPaddingRight, initialPaddingBottom ) } supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.apply { val shName = "$SH_NAME" val dexName = "$DEX_NAME" summary.text = getString(R.string.rish_description, shName).toHtml(HtmlCompat.FROM_HTML_OPTION_TRIM_WHITESPACE) text1.text = getString(R.string.terminal_tutorial_1, shName, dexName).toHtml() text2.text = getString(R.string.terminal_tutorial_2, shName).toHtml() summary2.text = getString( R.string.terminal_tutorial_2_description, "Termux", "PKG", "com.termux", "com.termux", ).toHtml() text3.text = getString( R.string.terminal_tutorial_3, "sh $SH_NAME", ).toHtml() summary3.text = getString( R.string.terminal_tutorial_3_description, shName, "PATH" ).toHtml() button1.setOnClickListener { openDocumentsTree.launch(null) } button2.setOnClickListener { v: View -> CustomTabsHelper.launchUrlOrCopy(v.context, Helps.RISH.get()) } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/starter/Starter.kt ================================================ package moe.shizuku.manager.starter import moe.shizuku.manager.application import java.io.File object Starter { private val starterFile = File(application.applicationInfo.nativeLibraryDir, "libshizuku.so") val userCommand: String = starterFile.absolutePath val adbCommand = "adb shell $userCommand" val internalCommand = "$userCommand --apk=${application.applicationInfo.sourceDir}" } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/starter/StarterActivity.kt ================================================ package moe.shizuku.manager.starter import android.content.Context import android.os.Bundle import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import moe.shizuku.manager.AppConstants.EXTRA import moe.shizuku.manager.R import moe.shizuku.manager.ShizukuSettings import moe.shizuku.manager.adb.AdbClient import moe.shizuku.manager.adb.AdbKey import moe.shizuku.manager.adb.AdbKeyException import moe.shizuku.manager.adb.PreferenceAdbKeyStore import moe.shizuku.manager.app.AppBarActivity import moe.shizuku.manager.databinding.StarterActivityBinding import rikka.lifecycle.Resource import rikka.lifecycle.Status import rikka.lifecycle.viewModels import rikka.shizuku.Shizuku import java.net.ConnectException import javax.net.ssl.SSLProtocolException private class NotRootedException : Exception() class StarterActivity : AppBarActivity() { private val viewModel by viewModels { ViewModel( this, intent.getBooleanExtra(EXTRA_IS_ROOT, true), intent.getStringExtra(EXTRA_HOST), intent.getIntExtra(EXTRA_PORT, 0) ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_24) val binding = StarterActivityBinding.inflate(layoutInflater) setContentView(binding.root) viewModel.output.observe(this) { val output = it.data!!.trim() if (output.endsWith("info: shizuku_starter exit with 0")) { viewModel.appendOutput("") viewModel.appendOutput("Waiting for service...") Shizuku.addBinderReceivedListener(object : Shizuku.OnBinderReceivedListener { override fun onBinderReceived() { Shizuku.removeBinderReceivedListener(this) viewModel.appendOutput("Service started, this window will be automatically closed in 3 seconds") window?.decorView?.postDelayed({ if (!isFinishing) finish() }, 3000) } }) } else if (it.status == Status.ERROR) { var message = 0 when (it.error) { is AdbKeyException -> { message = R.string.adb_error_key_store } is NotRootedException -> { message = R.string.start_with_root_failed } is ConnectException -> { message = R.string.cannot_connect_port } is SSLProtocolException -> { message = R.string.adb_pair_required } } if (message != 0) { MaterialAlertDialogBuilder(this) .setMessage(message) .setPositiveButton(android.R.string.ok, null) .show() } } binding.text1.text = output } } companion object { const val EXTRA_IS_ROOT = "$EXTRA.IS_ROOT" const val EXTRA_HOST = "$EXTRA.HOST" const val EXTRA_PORT = "$EXTRA.PORT" } } private class ViewModel(context: Context, root: Boolean, host: String?, port: Int) : androidx.lifecycle.ViewModel() { private val sb = StringBuilder() private val _output = MutableLiveData>() val output = _output as LiveData> init { try { if (root) { startRoot() } else { startAdb(host!!, port) } } catch (e: Throwable) { postResult(e) } } fun appendOutput(line: String) { sb.appendLine(line) postResult() } private fun postResult(throwable: Throwable? = null) { if (throwable == null) _output.postValue(Resource.success(sb)) else _output.postValue(Resource.error(throwable, sb)) } private fun startRoot() { sb.append("Starting with root...").append('\n').append('\n') postResult() GlobalScope.launch(Dispatchers.IO) { if (!Shell.getShell().isRoot) { Shell.getCachedShell()?.close() sb.append('\n').append("Can't open root shell, try again...").append('\n') postResult() if (!Shell.getShell().isRoot) { sb.append('\n').append("Still not :(").append('\n') postResult(NotRootedException()) return@launch } } Shell.cmd(Starter.internalCommand).to(object : CallbackList() { override fun onAddElement(s: String?) { sb.append(s).append('\n') postResult() } }).submit { if (it.code != 0) { sb.append('\n').append("Send this to developer may help solve the problem.") postResult() } } } } private fun startAdb(host: String, port: Int) { sb.append("Starting with wireless adb in port $port...").append('\n').append('\n') postResult() GlobalScope.launch(Dispatchers.IO) { val key = try { AdbKey(PreferenceAdbKeyStore(ShizukuSettings.getPreferences()), "shizuku") } catch (e: Throwable) { e.printStackTrace() sb.append('\n').append(Log.getStackTraceString(e)) postResult(AdbKeyException(e)) return@launch } AdbClient(host, port, key).runCatching { connect() shellCommand(Starter.internalCommand) { sb.append(String(it)) postResult() } close() }.onFailure { it.printStackTrace() sb.append('\n').append(Log.getStackTraceString(it)) postResult(it) } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/AppIconCache.kt ================================================ package moe.shizuku.manager.utils import android.annotation.SuppressLint import android.content.Context import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.graphics.drawable.AdaptiveIconDrawable import android.os.Build import android.widget.ImageView import androidx.collection.LruCache import kotlinx.coroutines.* import me.zhanghai.android.appiconloader.AppIconLoader import moe.shizuku.manager.R import rikka.core.util.BuildUtils import java.util.concurrent.Executor import java.util.concurrent.Executors import kotlin.coroutines.CoroutineContext object AppIconCache : CoroutineScope { private class AppIconLruCache constructor(maxSize: Int) : LruCache, Bitmap>(maxSize) { override fun sizeOf(key: Triple, bitmap: Bitmap): Int { return bitmap.byteCount / 1024 } } override val coroutineContext: CoroutineContext get() = Dispatchers.Main private val lruCache: LruCache, Bitmap> private val dispatcher: CoroutineDispatcher private var appIconLoaders = mutableMapOf() init { // Initialize app icon lru cache val maxMemory = Runtime.getRuntime().maxMemory() / 1024 val availableCacheSize = (maxMemory / 4).toInt() lruCache = AppIconLruCache(availableCacheSize) // Initialize load icon scheduler val availableProcessorsCount = try { Runtime.getRuntime().availableProcessors() } catch (ignored: Exception) { 1 } val threadCount = 1.coerceAtLeast(availableProcessorsCount / 2) val loadIconExecutor: Executor = Executors.newFixedThreadPool(threadCount) dispatcher = loadIconExecutor.asCoroutineDispatcher() } fun dispatcher(): CoroutineDispatcher { return dispatcher } private fun get(packageName: String, userId: Int, size: Int): Bitmap? { return lruCache[Triple(packageName, userId, size)] } private fun put(packageName: String, userId: Int, size: Int, bitmap: Bitmap) { if (get(packageName, userId, size) == null) { lruCache.put(Triple(packageName, userId, size), bitmap) } } private fun remove(packageName: String, userId: Int, size: Int) { lruCache.remove(Triple(packageName, userId, size)) } @SuppressLint("NewApi") fun getOrLoadBitmap(context: Context, info: ApplicationInfo, userId: Int, size: Int): Bitmap? { val cachedBitmap = get(info.packageName, userId, size) if (cachedBitmap != null) { return cachedBitmap } var loader = appIconLoaders[size] if (loader == null) { val shrinkNonAdaptiveIcons = BuildUtils.atLeast30 && context.applicationInfo.loadIcon(context.packageManager) is AdaptiveIconDrawable loader = AppIconLoader(size, shrinkNonAdaptiveIcons, context) appIconLoaders[size] = loader } val bitmap = loader.loadIcon(info, false) put(info.packageName, userId, size, bitmap) return bitmap } @JvmStatic fun loadIconBitmapAsync(context: Context, info: ApplicationInfo, userId: Int, view: ImageView): Job { return launch { val size = view.measuredWidth.let { if (it > 0) it else context.resources.getDimensionPixelSize(R.dimen.default_app_icon_size) } val cachedBitmap = get(info.packageName, userId, size) if (cachedBitmap != null) { view.setImageBitmap(cachedBitmap) return@launch } val bitmap = try { withContext(dispatcher) { getOrLoadBitmap(context, info, userId, size) } } catch (e: CancellationException) { // do nothing if canceled return@launch } catch (e: Throwable) { null } if (bitmap != null) { view.setImageBitmap(bitmap) } else { if (Build.VERSION.SDK_INT >= 26) { view.setImageResource(R.drawable.ic_default_app_icon) } else { view.setImageDrawable(null) } } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/CustomTabsHelper.java ================================================ package moe.shizuku.manager.utils; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import androidx.browser.customtabs.CustomTabsIntent; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import moe.shizuku.manager.R; import rikka.core.util.ClipboardUtils; import rikka.html.text.HtmlCompat; /** * Created by fytho on 2017/12/15. */ public class CustomTabsHelper { public interface OnCreateIntentBuilderListener { void onCreateHelpIntentBuilder(Context context, CustomTabsIntent.Builder builder); } private static OnCreateIntentBuilderListener sOnCreateIntentBuilderListener; public static void setOnCreateIntentBuilderListener(OnCreateIntentBuilderListener onCreateIntentBuilderListener) { sOnCreateIntentBuilderListener = onCreateIntentBuilderListener; } public static CustomTabsIntent.Builder createBuilder() { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); builder.setShowTitle(true); return builder; } public static boolean launchHelp(Context context, Uri uri) { CustomTabsIntent.Builder builder = createBuilder(); if (sOnCreateIntentBuilderListener != null) { sOnCreateIntentBuilderListener.onCreateHelpIntentBuilder(context, builder); } Uri.Builder uriBuilder = uri.buildUpon(); if ((context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) > 0) { uriBuilder.appendQueryParameter("night", "1"); } return launchUrl(context, builder.build(), uriBuilder.build()); } public static boolean launchUrl(Context context, Uri uri) { return launchUrl(context, createBuilder().build(), uri); } private static boolean launchUrl(Context context, CustomTabsIntent customTabsIntent, Uri uri) { try { customTabsIntent.launchUrl(context, uri); return true; } catch (ActivityNotFoundException e) { return false; } } public static void launchUrlOrCopy(Context context, String url) { Uri uri = Uri.parse(url); if (!launchHelp(context, uri)) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); try { context.startActivity(intent); } catch (Throwable tr) { try { ClipboardUtils.put(context, url); new MaterialAlertDialogBuilder(context) .setTitle(R.string.dialog_cannot_open_browser_title) .setMessage(HtmlCompat.fromHtml(context.getString(R.string.toast_copied_to_clipboard_with_text, url))) .setPositiveButton(android.R.string.ok, null) .show(); } catch (Throwable ignored) { } } } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/EmptySharedPreferencesImpl.java ================================================ package moe.shizuku.manager.utils; import android.content.SharedPreferences; import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Set; public class EmptySharedPreferencesImpl implements SharedPreferences { @Override public Map getAll() { return new HashMap<>(); } @Nullable @Override public String getString(String key, @Nullable String defValue) { return defValue; } @Nullable @Override public Set getStringSet(String key, @Nullable Set defValues) { return defValues; } @Override public int getInt(String key, int defValue) { return defValue; } @Override public long getLong(String key, long defValue) { return defValue; } @Override public float getFloat(String key, float defValue) { return defValue; } @Override public boolean getBoolean(String key, boolean defValue) { return defValue; } @Override public boolean contains(String key) { return false; } @Override public Editor edit() { return new EditorImpl(); } @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { } @Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { } private static class EditorImpl implements Editor { @Override public Editor putString(String key, @Nullable String value) { return this; } @Override public Editor putStringSet(String key, @Nullable Set values) { return this; } @Override public Editor putInt(String key, int value) { return this; } @Override public Editor putLong(String key, long value) { return this; } @Override public Editor putFloat(String key, float value) { return this; } @Override public Editor putBoolean(String key, boolean value) { return this; } @Override public Editor remove(String key) { return this; } @Override public Editor clear() { return this; } @Override public boolean commit() { return true; } @Override public void apply() { } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/EnvironmentUtils.kt ================================================ package moe.shizuku.manager.utils import android.app.UiModeManager import android.content.Context import android.content.res.Configuration import android.os.SystemProperties import java.io.File object EnvironmentUtils { @JvmStatic fun isWatch(context: Context): Boolean { return (context.getSystemService(UiModeManager::class.java).currentModeType == Configuration.UI_MODE_TYPE_WATCH) } fun isRooted(): Boolean { return System.getenv("PATH")?.split(File.pathSeparatorChar)?.find { File("$it/su").exists() } != null } fun getAdbTcpPort(): Int { var port = SystemProperties.getInt("service.adb.tcp.port", -1) if (port == -1) port = SystemProperties.getInt("persist.adb.tcp.port", -1) return port } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/Logger.java ================================================ package moe.shizuku.manager.utils; import android.util.Log; import java.util.Locale; public class Logger { public static final Logger LOGGER = new Logger("ShizukuManager"); private String TAG; public Logger(String TAG) { this.TAG = TAG; } public boolean isLoggable(String tag, int level) { return true; } public void v(String msg) { if (isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, msg); } } public void v(String fmt, Object... args) { if (isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, String.format(Locale.ENGLISH, fmt, args)); } } public void v(String msg, Throwable tr) { if (isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, msg, tr); } } public void d(String msg) { if (isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, msg); } } public void d(String fmt, Object... args) { if (isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, String.format(Locale.ENGLISH, fmt, args)); } } public void d(String msg, Throwable tr) { if (isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, msg, tr); } } public void i(String msg) { if (isLoggable(TAG, Log.INFO)) { Log.i(TAG, msg); } } public void i(String fmt, Object... args) { if (isLoggable(TAG, Log.INFO)) { Log.i(TAG, String.format(Locale.ENGLISH, fmt, args)); } } public void i(String msg, Throwable tr) { if (isLoggable(TAG, Log.INFO)) { Log.i(TAG, msg, tr); } } public void w(String msg) { if (isLoggable(TAG, Log.WARN)) { Log.w(TAG, msg); } } public void w(String fmt, Object... args) { if (isLoggable(TAG, Log.WARN)) { Log.w(TAG, String.format(Locale.ENGLISH, fmt, args)); } } public void w(Throwable tr, String fmt, Object... args) { if (isLoggable(TAG, Log.WARN)) { Log.w(TAG, String.format(Locale.ENGLISH, fmt, args), tr); } } public void w(String msg, Throwable tr) { if (isLoggable(TAG, Log.WARN)) { Log.w(TAG, msg, tr); } } public void e(String msg) { if (isLoggable(TAG, Log.ERROR)) { Log.e(TAG, msg); } } public void e(String fmt, Object... args) { if (isLoggable(TAG, Log.ERROR)) { Log.e(TAG, String.format(Locale.ENGLISH, fmt, args)); } } public void e(String msg, Throwable tr) { if (isLoggable(TAG, Log.ERROR)) { Log.e(TAG, msg, tr); } } public void e(Throwable tr, String fmt, Object... args) { if (isLoggable(TAG, Log.ERROR)) { Log.e(TAG, String.format(Locale.ENGLISH, fmt, args), tr); } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/MultiLocaleEntity.java ================================================ package moe.shizuku.manager.utils; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Locale; import moe.shizuku.manager.ShizukuSettings; public class MultiLocaleEntity extends LinkedHashMap { public abstract static class LocaleProvider { public abstract Locale get(); } public static final LocaleProvider DEFAULT_LOCAL_PROVIDER = new LocaleProvider() { @Override public Locale get() { return ShizukuSettings.getLocale(); } }; private static LocaleProvider sLocaleProvider = DEFAULT_LOCAL_PROVIDER; public static void setLocaleProvider(@NonNull LocaleProvider localeProvider) { sLocaleProvider = localeProvider; } public String get() { return get(sLocaleProvider.get()); } public String get(@NonNull Locale locale) { if (size() > 0) { String language = locale.getLanguage(); String region = locale.getCountry(); // fully match locale = new Locale(language, region); for (String l : keySet()) { if (locale.toString().equals(l.replace('-', '_'))) { return get(l); } } // match language only keys locale = new Locale(language); for (String l : keySet()) { if (locale.toString().equals(l)) { return get(l); } } // match a language_region with only language for (String l : keySet()) { if (l.startsWith(locale.toString())) { return get(l); } } if (containsKey("en")) { return get("en"); } if (containsKey("default")) { return get("default"); } for (String key : keySet()) { if (!"overwrite_default".equals(key)) return get(key); } } return null; } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/ShizukuSystemApis.kt ================================================ package moe.shizuku.manager.utils import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ParceledListSlice import android.os.RemoteException import rikka.hidden.compat.PackageManagerApis import rikka.hidden.compat.PermissionManagerApis import rikka.hidden.compat.UserManagerApis import rikka.hidden.compat.util.SystemServiceBinder import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper object ShizukuSystemApis { init { SystemServiceBinder.setOnGetBinderListener { return@setOnGetBinderListener ShizukuBinderWrapper(it) } } private val users = arrayListOf() private fun getUsers(): List { return if (!Shizuku.pingBinder()) { arrayListOf(UserInfoCompat(UserHandleCompat.myUserId(), "Owner")) } else try { val list = UserManagerApis.getUsers(true, true, true) val users: MutableList = ArrayList() for (ui in list) { users.add(UserInfoCompat(ui.id, ui.name)) } return users } catch (tr: Throwable) { arrayListOf(UserInfoCompat(UserHandleCompat.myUserId(), "Owner")) } } fun getUsers(useCache: Boolean = true): List { synchronized(users) { if (!useCache || users.isEmpty()) { users.clear() users.addAll(getUsers()) } return users } } fun getUserInfo(userId: Int): UserInfoCompat { return getUsers(useCache = true).firstOrNull { it.id == userId } ?: UserInfoCompat( UserHandleCompat.myUserId(), "Unknown" ) } fun getInstalledPackages(flags: Long, userId: Int): List { return if (!Shizuku.pingBinder()) { ArrayList() } else try { val listSlice: ParceledListSlice? = PackageManagerApis.getInstalledPackages( flags, userId ) return if (listSlice != null) { listSlice.list } else ArrayList() } catch (tr: RemoteException) { throw RuntimeException(tr.message, tr) } } fun checkPermission(permName: String, pkgName: String, userId: Int): Int { return if (!Shizuku.pingBinder()) { PackageManager.PERMISSION_DENIED } else try { PermissionManagerApis.checkPermission(permName, pkgName, userId) } catch (tr: RemoteException) { throw RuntimeException(tr.message, tr) } } fun grantRuntimePermission(packageName: String, permissionName: String, userId: Int) { if (!Shizuku.pingBinder()) { return } try { PermissionManagerApis.grantRuntimePermission(packageName, permissionName, userId) } catch (tr: RemoteException) { throw RuntimeException(tr.message, tr) } } fun revokeRuntimePermission(packageName: String, permissionName: String, userId: Int) { if (!Shizuku.pingBinder()) { return } try { PermissionManagerApis.revokeRuntimePermission(packageName, permissionName, userId) } catch (tr: RemoteException) { throw RuntimeException(tr.message, tr) } } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/UserHandleCompat.java ================================================ package moe.shizuku.manager.utils; import android.system.Os; public class UserHandleCompat { private static final int MY_USER_ID = getUserId(Os.getuid()); public static final int PER_USER_RANGE = 100000; public static int getUserId(int uid) { return uid / PER_USER_RANGE; } public static int getAppId(int uid) { return uid % PER_USER_RANGE; } public static int myUserId() { return MY_USER_ID; } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/utils/UserInfoCompat.java ================================================ package moe.shizuku.manager.utils; public class UserInfoCompat { public final int id; public final String name; public UserInfoCompat(int id, String name) { this.id = id; this.name = name; } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/widget/CheckedImageView.java ================================================ package moe.shizuku.manager.widget; import android.content.Context; import android.util.AttributeSet; import android.widget.Checkable; import android.widget.ImageView; import androidx.annotation.Nullable; public class CheckedImageView extends ImageView implements Checkable { private boolean mChecked; private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked }; public CheckedImageView(Context context) { super(context); } public CheckedImageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CheckedImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CheckedImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void setChecked(boolean checked) { if (mChecked != checked) { mChecked = checked; refreshDrawableState(); } } @Override public boolean isChecked() { return mChecked; } @Override public void toggle() { setChecked(!mChecked); } @Override public int[] onCreateDrawableState(int extraSpace) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } return drawableState; } } ================================================ FILE: manager/src/main/java/moe/shizuku/manager/widget/VerticalPaddingDecoration.java ================================================ package moe.shizuku.manager.widget; import android.content.Context; import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public class VerticalPaddingDecoration extends RecyclerView.ItemDecoration { private boolean mAllowTop, mAllowBottom; private int mPadding; public VerticalPaddingDecoration(Context context) { this(context, 8); } public VerticalPaddingDecoration(Context context, int paddingDp) { this.mPadding = Math.round(paddingDp * context.getResources().getDisplayMetrics().density); this.mAllowTop = true; this.mAllowBottom = true; } public void setAllowTop(boolean allowTop) { mAllowTop = allowTop; } public void setAllowBottom(boolean allowBottom) { mAllowBottom = allowBottom; } @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { if (parent.getAdapter() == null) { return; } int position = parent.getChildAdapterPosition(view); int count = parent.getAdapter().getItemCount(); if (position == 0 && mAllowTop) { outRect.top = mPadding; } else if (position == count - 1 && mAllowBottom) { outRect.bottom = mPadding; } } } ================================================ FILE: manager/src/main/jni/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.31) project("shizuku") set(CMAKE_CXX_STANDARD 17) add_compile_options(-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") message("Builing Release...") add_compile_options(-Os -flto -fvisibility=hidden -fvisibility-inlines-hidden) add_link_options(-flto -Wl,--exclude-libs,ALL -Wl,--gc-sections -Wl,--strip-all) else () message("Builing Debug...") add_definitions(-DDEBUG) endif () find_package(boringssl REQUIRED CONFIG) find_package(cxx REQUIRED CONFIG) add_executable(libshizuku.so starter.cpp misc.cpp selinux.cpp cgroup.cpp) target_link_libraries(libshizuku.so log cxx::cxx) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command(TARGET libshizuku.so POST_BUILD COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libshizuku.so") endif () add_library(adb SHARED adb_pairing.cpp misc.cpp) target_link_libraries(adb log boringssl::crypto_static cxx::cxx) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command(TARGET adb POST_BUILD COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libadb.so") endif () ================================================ FILE: manager/src/main/jni/adb_pairing.cpp ================================================ #include #include #include #include #include #include #include #include #include #include "adb_pairing.h" #define LOG_TAG "AdbPairClient" #include "logging.h" // --------------------------------------------------------- static constexpr spake2_role_t kClientRole = spake2_role_alice; static constexpr spake2_role_t kServerRole = spake2_role_bob; static const uint8_t kClientName[] = "adb pair client"; static const uint8_t kServerName[] = "adb pair server"; static constexpr size_t kHkdfKeyLength = 16; struct PairingContextNative { SPAKE2_CTX *spake2_ctx; uint8_t key[SPAKE2_MAX_MSG_SIZE]; size_t key_size; EVP_AEAD_CTX *aes_ctx; uint64_t dec_sequence; uint64_t enc_sequence; }; static jlong PairingContext_Constructor(JNIEnv *env, jclass clazz, jboolean isClient, jbyteArray jPassword) { spake2_role_t spake_role; const uint8_t *my_name; const uint8_t *their_name; size_t my_len; size_t their_len; if (isClient) { spake_role = kClientRole; my_name = kClientName; my_len = sizeof(kClientName); their_name = kServerName; their_len = sizeof(kServerName); } else { spake_role = kServerRole; my_name = kServerName; my_len = sizeof(kServerName); their_name = kClientName; their_len = sizeof(kClientName); } auto spake2_ctx = SPAKE2_CTX_new(spake_role, my_name, my_len, their_name, their_len); if (spake2_ctx == nullptr) { LOGE("Unable to create a SPAKE2 context."); return 0; } auto pswd_size = env->GetArrayLength(jPassword); auto pswd = env->GetByteArrayElements(jPassword, nullptr); size_t key_size = 0; uint8_t key[SPAKE2_MAX_MSG_SIZE]; int status = SPAKE2_generate_msg(spake2_ctx, key, &key_size, SPAKE2_MAX_MSG_SIZE, (uint8_t *) pswd, pswd_size); if (status != 1 || key_size == 0) { LOGE("Unable to generate the SPAKE2 public key."); env->ReleaseByteArrayElements(jPassword, pswd, 0); SPAKE2_CTX_free(spake2_ctx); return 0; } env->ReleaseByteArrayElements(jPassword, pswd, 0); auto ctx = (PairingContextNative *) malloc(sizeof(PairingContextNative)); memset(ctx, 0, sizeof(PairingContextNative)); ctx->spake2_ctx = spake2_ctx; memcpy(ctx->key, key, SPAKE2_MAX_MSG_SIZE); ctx->key_size = key_size; return (jlong) ctx; } static jbyteArray PairingContext_Msg(JNIEnv *env, jobject obj, jlong ptr) { auto ctx = (PairingContextNative *) ptr; jbyteArray our_msg = env->NewByteArray(ctx->key_size); env->SetByteArrayRegion(our_msg, 0, ctx->key_size, (jbyte *) ctx->key); return our_msg; } static jboolean PairingContext_InitCipher(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jTheirMsg) { auto res = JNI_TRUE; auto ctx = (PairingContextNative *) ptr; auto spake2_ctx = ctx->spake2_ctx; auto their_msg_size = env->GetArrayLength(jTheirMsg); if (their_msg_size > SPAKE2_MAX_MSG_SIZE) { LOGE("their_msg size [%d] greater then max size [%d].", their_msg_size, SPAKE2_MAX_MSG_SIZE); return JNI_FALSE; } auto their_msg = env->GetByteArrayElements(jTheirMsg, nullptr); size_t key_material_len = 0; uint8_t key_material[SPAKE2_MAX_KEY_SIZE]; int status = SPAKE2_process_msg(spake2_ctx, key_material, &key_material_len, sizeof(key_material), (uint8_t *) their_msg, their_msg_size); env->ReleaseByteArrayElements(jTheirMsg, their_msg, 0); if (status != 1) { LOGE("Unable to process their public key"); return JNI_FALSE; } // -------- uint8_t key[kHkdfKeyLength]; uint8_t info[] = "adb pairing_auth aes-128-gcm key"; status = HKDF(key, sizeof(key), EVP_sha256(), key_material, key_material_len, nullptr, 0, info, sizeof(info) - 1); if (status != 1) { LOGE("HKDF"); return JNI_FALSE; } ctx->aes_ctx = EVP_AEAD_CTX_new(EVP_aead_aes_128_gcm(), key, sizeof(key), EVP_AEAD_DEFAULT_TAG_LENGTH); if (!ctx->aes_ctx) { LOGE("EVP_AEAD_CTX_new"); return JNI_FALSE; } return res; } static jbyteArray PairingContext_Encrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { auto ctx = (PairingContextNative *) ptr; auto aes_ctx = ctx->aes_ctx; auto in = env->GetByteArrayElements(jIn, nullptr); auto in_size = env->GetArrayLength(jIn); auto out_size = (size_t) in_size + EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(ctx->aes_ctx)); uint8_t out[out_size]; auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); uint8_t nonce[nonce_size]; memset(nonce, 0, nonce_size); memcpy(nonce, &ctx->enc_sequence, sizeof(ctx->enc_sequence)); size_t written_sz; int status = EVP_AEAD_CTX_seal(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); env->ReleaseByteArrayElements(jIn, in, 0); if (!status) { LOGE("Failed to encrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); return nullptr; } ++ctx->enc_sequence; jbyteArray jOut = env->NewByteArray(written_sz); env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); return jOut; } static jbyteArray PairingContext_Decrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { auto ctx = (PairingContextNative *) ptr; auto aes_ctx = ctx->aes_ctx; auto in = env->GetByteArrayElements(jIn, nullptr); auto in_size = env->GetArrayLength(jIn); auto out_size = (size_t) in_size; uint8_t out[out_size]; auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); uint8_t nonce[nonce_size]; memset(nonce, 0, nonce_size); memcpy(nonce, &ctx->dec_sequence, sizeof(ctx->dec_sequence)); size_t written_sz; int status = EVP_AEAD_CTX_open(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); env->ReleaseByteArrayElements(jIn, in, 0); if (!status) { LOGE("Failed to decrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); return nullptr; } ++ctx->dec_sequence; jbyteArray jOut = env->NewByteArray(written_sz); env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); return jOut; } static void PairingContext_Destroy(JNIEnv *env, jobject obj, jlong ptr) { auto ctx = (PairingContextNative *) ptr; SPAKE2_CTX_free(ctx->spake2_ctx); if (ctx->aes_ctx) EVP_AEAD_CTX_free(ctx->aes_ctx); free(ctx); } // --------------------------------------------------------- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) return -1; JNINativeMethod methods_PairingContext[] = { {"nativeConstructor", "(Z[B)J", (void *) PairingContext_Constructor}, {"nativeMsg", "(J)[B", (void *) PairingContext_Msg}, {"nativeInitCipher", "(J[B)Z", (void *) PairingContext_InitCipher}, {"nativeEncrypt", "(J[B)[B", (void *) PairingContext_Encrypt}, {"nativeDecrypt", "(J[B)[B", (void *) PairingContext_Decrypt}, {"nativeDestroy", "(J)V", (void *) PairingContext_Destroy}, }; env->RegisterNatives(env->FindClass("moe/shizuku/manager/adb/PairingContext"), methods_PairingContext, sizeof(methods_PairingContext) / sizeof(JNINativeMethod)); return JNI_VERSION_1_6; } ================================================ FILE: manager/src/main/jni/adb_pairing.h ================================================ #ifndef ADB_H #define ADB_H #endif // ADB_H ================================================ FILE: manager/src/main/jni/android.cpp ================================================ #include #include #include #include #include namespace android { int GetApiLevel() { static int apiLevel = 0; if (apiLevel > 0) return apiLevel; char buf[PROP_VALUE_MAX + 1]; if (__system_property_get("ro.build.version.sdk", buf) > 0) apiLevel = atoi(buf); return apiLevel; } int GetPreviewApiLevel() { static int previewApiLevel = 0; if (previewApiLevel > 0) return previewApiLevel; char buf[PROP_VALUE_MAX + 1]; if (__system_property_get("ro.build.version.preview_sdk", buf) > 0) previewApiLevel = atoi(buf); return previewApiLevel; } } ================================================ FILE: manager/src/main/jni/android.h ================================================ #pragma once namespace android { int GetApiLevel(); int GetPreviewApiLevel(); } ================================================ FILE: manager/src/main/jni/cgroup.cpp ================================================ #include #include #include #include namespace cgroup { bool switch_cgroup(const char *cgroup, int pid) { char buf[1024]; snprintf(buf, sizeof(buf), "%s/cgroup.procs", cgroup); if (access(buf, F_OK) != 0) return false; int fd = open(buf, O_WRONLY | O_APPEND | O_CLOEXEC); if (fd == -1) return false; snprintf(buf, sizeof(buf), "%d\n", pid); ssize_t c = write(fd, buf, strlen(buf)); close(fd); return c == strlen(buf); } } ================================================ FILE: manager/src/main/jni/cgroup.h ================================================ #ifndef CGROUP_H #define CGROUP_H namespace cgroup { bool switch_cgroup(const char *cgroup, int pid); } #endif // CGROUP_H ================================================ FILE: manager/src/main/jni/helper.cpp ================================================ #include #include #include #include #include #include #include #include #include "selinux.h" #define LOG_TAG "ShizukuServer" #include "logging.h" static jint setcontext(JNIEnv *env, jobject thiz, jstring jName) { const char *name = env->GetStringUTFChars(jName, nullptr); if (!se::setcon) return -1; int res = se::setcon(name); if (res == -1) PLOGE("setcon %s", name); env->ReleaseStringUTFChars(jName, name); return res; } static JNINativeMethod gMethods[] = { {"setSELinuxContext", "(Ljava/lang/String;)I", (void *) setcontext}, }; static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) { jclass clazz; clazz = env->FindClass(className); if (clazz == nullptr) return JNI_FALSE; if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) return JNI_FALSE; return JNI_TRUE; } static int registerNatives(JNIEnv *env) { if (!registerNativeMethods(env, "moe/shizuku/server/utils/NativeHelper", gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) return JNI_FALSE; return JNI_TRUE; } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; jint result; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) return -1; assert(env != nullptr); se::init(); if (!registerNatives(env)) { LOGE("registerNatives NativeHelper"); return -1; } result = JNI_VERSION_1_6; return result; } ================================================ FILE: manager/src/main/jni/logging.h ================================================ #ifndef _LOGGING_H #define _LOGGING_H #include #include "android/log.h" #ifndef LOG_TAG #define LOG_TAG "Shizuku" #endif #ifndef NO_LOG #ifndef NO_DEBUG_LOG #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) #endif #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) #else #define LOGD(...) #define LOGV(...) #define LOGI(...) #define LOGW(...) #define LOGE(...) #define PLOGE(fmt, args...) #endif #endif // _LOGGING_H ================================================ FILE: manager/src/main/jni/misc.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include "misc.h" ssize_t fdgets(char *buf, const size_t size, int fd) { buf[0] = '\0'; ssize_t ret; do { ret = read(fd, buf, size - 1); } while (ret < 0 && errno == EINTR); if (ret < 0) return -1; buf[ret] = '\0'; return ret; } int get_proc_name(int pid, char *name, size_t size) { int fd; char buf[PATH_MAX]; snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); if ((fd = open(buf, O_RDONLY)) == -1) return 1; fdgets(name, size, fd); close(fd); return 0; } int is_num(const char *s) { size_t len = strlen(s); for (size_t i = 0; i < len; ++i) if (s[i] < '0' || s[i] > '9') return 0; return 1; } int copyfileat(int src_path_fd, const char *src_path, int dst_path_fd, const char *dst_path) { int src_fd; int dst_fd; struct stat stat_buf{}; int64_t size_remaining; size_t count; ssize_t result; if ((src_fd = openat(src_path_fd, src_path, O_RDONLY)) == -1) return -1; if (fstat(src_fd, &stat_buf) == -1) return -1; dst_fd = openat(dst_path_fd, dst_path, O_WRONLY | O_CREAT | O_TRUNC, stat_buf.st_mode); if (dst_fd == -1) { close(src_fd); return -1; } size_remaining = stat_buf.st_size; for (;;) { if (size_remaining > 0x7ffff000) count = 0x7ffff000; else count = static_cast(size_remaining); result = sendfile(dst_fd, src_fd, nullptr, count); if (result == -1) { close(src_fd); close(dst_fd); unlink(dst_path); return -1; } size_remaining -= result; if (size_remaining == 0) { close(src_fd); close(dst_fd); return 0; } } } int copyfile(const char *src_path, const char *dst_path) { return copyfileat(0, src_path, 0, dst_path); } uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size) { uintptr_t _start = start; while (true) { if (_start + size >= end) return 0; if (memcmp((const void *) _start, value, size) == 0) return _start; _start += 1; } } int switch_mnt_ns(int pid) { char mnt[32]; snprintf(mnt, sizeof(mnt), "/proc/%d/ns/mnt", pid); if (access(mnt, R_OK) == -1) return -1; int fd = open(mnt, O_RDONLY); if (fd < 0) return -1; int res = setns(fd, 0); close(fd); return res; } void foreach_proc(foreach_proc_function *func) { DIR *dir; struct dirent *entry; if (!(dir = opendir("/proc"))) return; while ((entry = readdir(dir))) { if (entry->d_type != DT_DIR) continue; if (!is_num(entry->d_name)) continue; pid_t pid = atoi(entry->d_name); func(pid); } closedir(dir); } char *trim(char *str) { size_t len = 0; char *frontp = str; char *endp = nullptr; if (str == nullptr) { return nullptr; } if (str[0] == '\0') { return str; } len = strlen(str); endp = str + len; /* Move the front and back pointers to address the first non-whitespace * characters from each end. */ while (isspace((unsigned char) *frontp)) { ++frontp; } if (endp != frontp) { while (isspace((unsigned char) *(--endp)) && endp != frontp) {} } if (str + len - 1 != endp) *(endp + 1) = '\0'; else if (frontp != str && endp == frontp) *str = '\0'; /* Shift the string so that it starts at str so that if it's dynamically * allocated, we can still free it on the returned pointer. Note the reuse * of endp to mean the front of the string buffer now. */ endp = str; if (frontp != str) { while (*frontp) { *endp++ = *frontp++; } *endp = '\0'; } return str; } ================================================ FILE: manager/src/main/jni/misc.h ================================================ #ifndef MISC_H #define MISC_H int copyfile(const char *src_path, const char *dst_path); uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size); int switch_mnt_ns(int pid); int get_proc_name(int pid, char *name, size_t _size); using foreach_proc_function = void(pid_t); void foreach_proc(foreach_proc_function *func); char *trim(char *str); #endif // MISC_H ================================================ FILE: manager/src/main/jni/selinux.cpp ================================================ #include #include #include #include #include #include #include #include "selinux.h" namespace se { static int __getcon(char **context) { int fd = open("/proc/self/attr/current", O_RDONLY | O_CLOEXEC); if (fd < 0) return fd; char *buf; size_t size; int errno_hold; ssize_t ret; size = sysconf(_SC_PAGE_SIZE); buf = (char *) malloc(size); if (!buf) { ret = -1; goto out; } memset(buf, 0, size); do { ret = read(fd, buf, size - 1); } while (ret < 0 && errno == EINTR); if (ret < 0) goto out2; if (ret == 0) { *context = nullptr; goto out2; } *context = strdup(buf); if (!(*context)) { ret = -1; goto out2; } ret = 0; out2: free(buf); out: errno_hold = errno; close(fd); errno = errno_hold; return 0; } static int __setcon(const char *ctx) { int fd = open("/proc/self/attr/current", O_WRONLY | O_CLOEXEC); if (fd < 0) return fd; size_t len = strlen(ctx) + 1; ssize_t rc = write(fd, ctx, len); close(fd); return rc != len; } static int __setfilecon(const char *path, const char *ctx) { int rc = syscall(__NR_setxattr, path, "security.selinux"/*XATTR_NAME_SELINUX*/, ctx, strlen(ctx) + 1, 0); if (rc) { errno = -rc; return -1; } return 0; } static int __selinux_check_access(const char *scon, const char *tcon, const char *tclass, const char *perm, void *auditdata) { return 0; } static void __freecon(char *con) { free(con); } getcon_t *getcon = __getcon; setcon_t *setcon = __setcon; setfilecon_t *setfilecon = __setfilecon; selinux_check_access_t *selinux_check_access = __selinux_check_access; freecon_t *freecon = __freecon; void init() { if (access("/system/lib/libselinux.so", F_OK) != 0 && access("/system/lib64/libselinux.so", F_OK) != 0) return; void *handle = dlopen("libselinux.so", RTLD_LAZY | RTLD_LOCAL); if (handle == nullptr) return; getcon = (getcon_t *) dlsym(handle, "getcon"); setcon = (setcon_t *) dlsym(handle, "setcon"); setfilecon = (setfilecon_t *) dlsym(handle, "setfilecon"); selinux_check_access = (selinux_check_access_t *) dlsym(handle, "selinux_check_access"); freecon = (freecon_t *) (dlsym(handle, "freecon")); } } ================================================ FILE: manager/src/main/jni/selinux.h ================================================ #ifndef SELINUX_H #define SELINUX_H namespace se { void init(); using getcon_t = int(char **); using setcon_t = int(const char *); using setfilecon_t = int(const char *, const char *); using selinux_check_access_t = int(const char *, const char *, const char *, const char *, void *); using freecon_t = void(char *); extern getcon_t *getcon; extern setcon_t *setcon; extern setfilecon_t *setfilecon; extern selinux_check_access_t *selinux_check_access; extern freecon_t *freecon; } #endif // SELINUX_H ================================================ FILE: manager/src/main/jni/starter.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "android.h" #include "misc.h" #include "selinux.h" #include "cgroup.h" #include "logging.h" #ifdef DEBUG #define JAVA_DEBUGGABLE #endif #define perrorf(...) fprintf(stderr, __VA_ARGS__) #define EXIT_FATAL_SET_CLASSPATH 3 #define EXIT_FATAL_FORK 4 #define EXIT_FATAL_APP_PROCESS 5 #define EXIT_FATAL_UID 6 #define EXIT_FATAL_PM_PATH 7 #define EXIT_FATAL_KILL 9 #define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10 #define PACKAGE_NAME "moe.shizuku.privileged.api" #define SERVER_NAME "shizuku_server" #define SERVER_CLASS_PATH "rikka.shizuku.server.ShizukuService" #if defined(__arm__) #define ABI "arm" #elif defined(__i386__) #define ABI "x86" #elif defined(__x86_64__) #define ABI "x86_64" #elif defined(__aarch64__) #define ABI "arm64" #endif static void run_server(const char *dex_path, const char *main_class, const char *process_name) { if (setenv("CLASSPATH", dex_path, true)) { LOGE("can't set CLASSPATH\n"); exit(EXIT_FATAL_SET_CLASSPATH); } #define ARG(v) char **v = nullptr; \ char buf_##v[PATH_MAX]; \ size_t v_size = 0; \ uintptr_t v_current = 0; #define ARG_PUSH(v, arg) v_size += sizeof(char *); \ if (v == nullptr) { \ v = (char **) malloc(v_size); \ } else { \ v = (char **) realloc(v, v_size);\ } \ v_current = (uintptr_t) v + v_size - sizeof(char *); \ *((char **) v_current) = arg ? strdup(arg) : nullptr; #define ARG_END(v) ARG_PUSH(v, nullptr) #define ARG_PUSH_FMT(v, fmt, ...) snprintf(buf_##v, PATH_MAX, fmt, __VA_ARGS__); \ ARG_PUSH(v, buf_##v) #ifdef JAVA_DEBUGGABLE #define ARG_PUSH_DEBUG_ONLY(v, arg) ARG_PUSH(v, arg) #define ARG_PUSH_DEBUG_VM_PARAMS(v) \ if (android_get_device_api_level() >= 30) { \ ARG_PUSH(v, "-Xcompiler-option"); \ ARG_PUSH(v, "--debuggable"); \ ARG_PUSH(v, "-XjdwpProvider:adbconnection"); \ ARG_PUSH(v, "-XjdwpOptions:suspend=n,server=y"); \ } else if (android_get_device_api_level() >= 28) { \ ARG_PUSH(v, "-Xcompiler-option"); \ ARG_PUSH(v, "--debuggable"); \ ARG_PUSH(v, "-XjdwpProvider:internal"); \ ARG_PUSH(v, "-XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"); \ } else { \ ARG_PUSH(v, "-Xcompiler-option"); \ ARG_PUSH(v, "--debuggable"); \ ARG_PUSH(v, "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); \ } #else #define ARG_PUSH_DEBUG_VM_PARAMS(v) #define ARG_PUSH_DEBUG_ONLY(v, arg) #endif char lib_path[PATH_MAX]{0}; snprintf(lib_path, PATH_MAX, "%s/lib/%s", dirname(dex_path), ABI); ARG(argv) ARG_PUSH(argv, "/system/bin/app_process") ARG_PUSH_FMT(argv, "-Djava.class.path=%s", dex_path) ARG_PUSH_FMT(argv, "-Dshizuku.library.path=%s", lib_path) ARG_PUSH_DEBUG_VM_PARAMS(argv) ARG_PUSH(argv, "/system/bin") ARG_PUSH_FMT(argv, "--nice-name=%s", process_name) ARG_PUSH(argv, main_class) ARG_PUSH_DEBUG_ONLY(argv, "--debug") ARG_END(argv) LOGD("exec app_process"); if (execvp((const char *) argv[0], argv)) { exit(EXIT_FATAL_APP_PROCESS); } } static void start_server(const char *path, const char *main_class, const char *process_name) { pid_t pid = fork(); switch (pid) { case -1: { perrorf("fatal: can't fork\n"); exit(EXIT_FATAL_FORK); } case 0: { LOGD("child"); setsid(); chdir("/"); int fd = open("/dev/null", O_RDWR); if (fd != -1) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd > 2) close(fd); } run_server(path, main_class, process_name); } default: { printf("info: shizuku_server pid is %d\n", pid); printf("info: shizuku_starter exit with 0\n"); exit(EXIT_SUCCESS); } } } static int check_selinux(const char *s, const char *t, const char *c, const char *p) { int res = se::selinux_check_access(s, t, c, p, nullptr); #ifndef DEBUG if (res != 0) { #endif printf("info: selinux_check_access %s %s %s %s: %d\n", s, t, c, p, res); fflush(stdout); #ifndef DEBUG } #endif return res; } static int switch_cgroup() { int pid = getpid(); if (cgroup::switch_cgroup("/acct", pid)) { printf("info: switch cgroup succeeded, cgroup in /acct\n"); return 0; } if (cgroup::switch_cgroup("/dev/cg2_bpf", pid)) { printf("info: switch cgroup succeeded, cgroup in /dev/cg2_bpf\n"); return 0; } if (cgroup::switch_cgroup("/sys/fs/cgroup", pid)) { printf("info: switch cgroup succeeded, cgroup in /sys/fs/cgroup\n"); return 0; } char buf[PROP_VALUE_MAX + 1]; if (__system_property_get("ro.config.per_app_memcg", buf) > 0 && strncmp(buf, "false", 5) != 0) { if (cgroup::switch_cgroup("/dev/memcg/apps", pid)) { printf("info: switch cgroup succeeded, cgroup in /dev/memcg/apps\n"); return 0; } } printf("warn: can't switch cgroup\n"); fflush(stdout); return -1; } int main(int argc, char *argv[]) { std::string apk_path; for (int i = 0; i < argc; ++i) { if (strncmp(argv[i], "--apk=", 6) == 0) { apk_path = argv[i] + 6; } } uid_t uid = getuid(); if (uid != 0 && uid != 2000) { perrorf("fatal: run Shizuku from non root nor adb user (uid=%d).\n", uid); exit(EXIT_FATAL_UID); } se::init(); if (uid == 0) { switch_cgroup(); if (android_get_device_api_level() >= 29) { printf("info: switching mount namespace to init...\n"); switch_mnt_ns(1); } } if (uid == 0) { char *context = nullptr; if (se::getcon(&context) == 0) { int res = 0; res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "call"); res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "transfer"); if (res != 0) { perrorf("fatal: the su you are using does not allow app (u:r:untrusted_app:s0) to connect to su (%s) with binder.\n", context); exit(EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX); } se::freecon(context); } } printf("info: starter begin\n"); fflush(stdout); // kill old server printf("info: killing old process...\n"); fflush(stdout); foreach_proc([](pid_t pid) { if (pid == getpid()) return; char name[1024]; if (get_proc_name(pid, name, 1024) != 0) return; if (strcmp(SERVER_NAME, name) != 0) return; if (kill(pid, SIGKILL) == 0) printf("info: killed %d (%s)\n", pid, name); else if (errno == EPERM) { perrorf("fatal: can't kill %d, please try to stop existing Shizuku from app first.\n", pid); exit(EXIT_FATAL_KILL); } else { printf("warn: failed to kill %d (%s)\n", pid, name); } }); if (access(apk_path.c_str(), R_OK) == 0) { printf("info: use apk path from argv\n"); fflush(stdout); } if (apk_path.empty()) { auto f = popen("pm path " PACKAGE_NAME, "r"); if (f) { char line[PATH_MAX]{0}; fgets(line, PATH_MAX, f); trim(line); if (strstr(line, "package:") == line) { apk_path = line + strlen("package:"); } pclose(f); } } if (apk_path.empty()) { perrorf("fatal: can't get path of manager\n"); exit(EXIT_FATAL_PM_PATH); } printf("info: apk path is %s\n", apk_path.c_str()); if (access(apk_path.c_str(), R_OK) != 0) { perrorf("fatal: can't access manager %s\n", apk_path.c_str()); exit(EXIT_FATAL_PM_PATH); } printf("info: starting server...\n"); fflush(stdout); LOGD("start_server"); start_server(apk_path.c_str(), SERVER_CLASS_PATH, SERVER_NAME); } ================================================ FILE: manager/src/main/res/animator/alpha_animator.xml ================================================ ================================================ FILE: manager/src/main/res/color/grant_permissions_button_ripple_color_selector.xml ================================================ ================================================ FILE: manager/src/main/res/color/home_card_background_color.xml ================================================ ================================================ FILE: manager/src/main/res/color/home_card_foreground_color.xml ================================================ ================================================ FILE: manager/src/main/res/color-night/home_card_background_color.xml ================================================ ================================================ FILE: manager/src/main/res/color-night/home_card_foreground_color.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/card_btn_background.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/grant_permissions_buttons_bottom.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/grant_permissions_buttons_top.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/home_card_foreground.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_action_about_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_action_settings_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_adb_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_baseline_link_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_close_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_code_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_default_app_icon_background.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_help_outline_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_launcher.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_learn_more_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_monochrome.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_numeric_1_circle_outline_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_numeric_2_circle_outline_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_numeric_3_circle_outline_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_arrow_upward_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_dark_mode_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_info_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_notifications_active_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_open_in_new_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_play_arrow_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_outline_translate_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_root_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_server_error_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_server_ok_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_server_restart.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_server_start_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_settings_outline_24dp.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_system_icon.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_terminal_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_wadb_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/ic_warning_24.xml ================================================ ================================================ FILE: manager/src/main/res/drawable/shape_circle_icon_background.xml ================================================ ================================================ FILE: manager/src/main/res/drawable-v24/ic_default_app_icon_foreground.xml ================================================ ================================================ FILE: manager/src/main/res/drawable-v26/ic_default_app_icon.xml ================================================ ================================================ FILE: manager/src/main/res/drawable-v26/ic_launcher.xml ================================================ ================================================ FILE: manager/src/main/res/layout/about_dialog.xml ================================================ ================================================ FILE: manager/src/main/res/layout/adb_dialog.xml ================================================ ================================================ FILE: manager/src/main/res/layout/adb_pair_dialog.xml ================================================ ================================================ FILE: manager/src/main/res/layout/adb_pairing_tutorial_activity.xml ================================================