diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..edda2b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.cmd text eol=crlf +*.bat text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.so binary +*.dex binary +*.jar binary +*.png binary diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index b9b32d0..0000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Android CI - -on: - push: - branches: [ "master" ] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - fetch-depth: 0 - - name: set up JDK - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: gradle - - - name: Build with Gradle - run: | - echo 'org.gradle.caching=true' >> gradle.properties - echo 'org.gradle.parallel=true' >> gradle.properties - echo 'org.gradle.vfs.watch=true' >> gradle.properties - echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - ./gradlew publishToMavenLocal - ./gradlew --stop - env: - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.maven_pgp_signingKey }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.maven_pgp_signingPassword }} - - name: Upload library - uses: actions/upload-artifact@v4 - with: - name: library - path: ~/.m2/repository/ - include-hidden-files: true - if-no-files-found: error - - name: Upload pages - uses: actions/upload-pages-artifact@v3 - with: - path: 'service/build/dokka/javadoc' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 5774974..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Deploy to Maven Central - -on: - workflow_dispatch: - inputs: - deploy_interface: - description: 'Deploy interface module' - type: boolean - default: true - deploy_service: - description: 'Deploy service module' - type: boolean - default: true - -permissions: - contents: read - -jobs: - deploy: - if: github.event.inputs.deploy_interface == 'true' || github.event.inputs.deploy_service == 'true' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - fetch-depth: 0 - - - name: set up JDK - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: gradle - - - name: Publish to Maven Central - run: | - echo 'org.gradle.caching=true' >> gradle.properties - echo 'org.gradle.parallel=true' >> gradle.properties - echo 'org.gradle.vfs.watch=true' >> gradle.properties - echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - TASKS="" - if [[ "${{ github.event.inputs.deploy_interface }}" == "true" ]]; then - TASKS="$TASKS publishInterfacePublicationToOssrhRepository" - fi - if [[ "${{ github.event.inputs.deploy_service }}" == "true" ]]; then - TASKS="$TASKS publishServicePublicationToOssrhRepository" - fi - ./gradlew $TASKS - ./gradlew --stop - curl -X POST -H "Authorization: Bearer $(echo -n ${ORG_GRADLE_PROJECT_ossrhUsername}:${ORG_GRADLE_PROJECT_ossrhPassword} | base64)" https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.libxposed - env: - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.maven_pgp_signingKey }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.maven_pgp_signingPassword }} - ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRHUSERNAME }} - ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRHPASSWORD }} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml deleted file mode 100644 index 5992cff..0000000 --- a/.github/workflows/snapshot.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Publish Sonatype Snapshot - -on: - workflow_dispatch: - inputs: - deploy_interface: - description: 'Deploy interface module' - type: boolean - default: true - deploy_service: - description: 'Deploy service module' - type: boolean - default: true - dependency_snapshot: - description: 'Use snapshot annotation and lint dependencies' - type: boolean - default: false - -permissions: - contents: read - -jobs: - publish: - if: github.event.inputs.deploy_interface == 'true' || github.event.inputs.deploy_service == 'true' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - fetch-depth: 0 - - - name: set up JDK - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: gradle - - - name: Publish snapshot to Sonatype - run: | - echo 'org.gradle.caching=true' >> gradle.properties - echo 'org.gradle.parallel=true' >> gradle.properties - echo 'org.gradle.vfs.watch=true' >> gradle.properties - echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - echo 'publishSnapshot=true' >> gradle.properties - echo 'dependencySnapshot=${{ github.event.inputs.dependency_snapshot }}' >> gradle.properties - TASKS="" - if [[ "${{ github.event.inputs.deploy_interface }}" == "true" ]]; then - TASKS="$TASKS publishInterfacePublicationToSnapshotsRepository" - fi - if [[ "${{ github.event.inputs.deploy_service }}" == "true" ]]; then - TASKS="$TASKS publishServicePublicationToSnapshotsRepository" - fi - ./gradlew $TASKS - ./gradlew --stop - env: - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.maven_pgp_signingKey }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.maven_pgp_signingPassword }} - ORG_GRADLE_PROJECT_snapshotsUsername: ${{ secrets.OSSRHUSERNAME }} - ORG_GRADLE_PROJECT_snapshotsPassword: ${{ secrets.OSSRHPASSWORD }} diff --git a/.gitignore b/.gitignore index 2ec908f..10cfdbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ *.iml -.DS_Store .gradle +/local.properties /.idea +.DS_Store +/build /captures -/local.properties -build +.externalNativeBuild +.cxx +local.properties diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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. diff --git a/README.md b/README.md deleted file mode 100644 index bc9b361..0000000 --- a/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# libxposed Service - -[![API](https://img.shields.io/badge/API-102-brightgreen)](https://github.com/libxposed/api) -[![Maven Central](https://img.shields.io/maven-central/v/io.github.libxposed/service?color=blue)](https://central.sonatype.com/artifact/io.github.libxposed/service) -[![Android Min SDK](https://img.shields.io/badge/minSdk-26-orange)](https://developer.android.com/about/versions/oreo) -[![License](https://img.shields.io/github/license/libxposed/service)](LICENSE) - -Modern Xposed Service — communication interface between Xposed framework and module app. - -## Integration - -### For Module Developers - -```kotlin -dependencies { - implementation("io.github.libxposed:service:102.0.0") -} -``` - -### For Framework Developers - -```kotlin -dependencies { - implementation("io.github.libxposed:interface:102.0.0") -} -``` - -## Documentation - -- [Javadoc](https://libxposed.github.io/service/) — API reference - -## Related Projects - -- [libxposed/api](https://github.com/libxposed/api) — Modern Xposed Module API -- [libxposed/helper](https://github.com/libxposed/helper) — Friendly development kit library -- [libxposed/example](https://github.com/libxposed/example) — Example module using the modern Xposed API - -## License - -This project is licensed under the [Apache License 2.0](LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts index e14c7ef..389d4b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,10 @@ -val dependencySnapshot = providers.gradleProperty("dependencySnapshot").orNull == "true" - -allprojects { - configurations.configureEach { - if (dependencySnapshot) { - resolutionStrategy.cacheChangingModulesFor(0, "seconds") - } - - } -} - tasks.register("Delete", Delete::class) { - delete(rootProject.layout.buildDirectory) + delete(rootProject.buildDir) } + +val androidCompileSdkVersion by extra(31) +val androidBuildToolsVersion by extra("31.0.0") +val androidTargetSdkVersion by extra(31) +val androidMinSdkVersion by extra(15) +val androidSourceCompatibility by extra(JavaVersion.VERSION_11) +val androidTargetCompatibility by extra(JavaVersion.VERSION_11) diff --git a/gradle.properties b/gradle.properties index 78a36aa..c09e1e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,17 @@ -android.enableR8.fullMode=true +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 37bb64b..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,13 +0,0 @@ -[versions] -agp = "9.2.1" -dokka = "2.2.0" -libxposed-annotation = "1.0.0" -libxposed-lint = "1.0.0" - -[plugins] -agp-lib = { id = "com.android.library", version.ref = "agp" } -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -dokka-javadoc = { id = "org.jetbrains.dokka-javadoc", version.ref = "dokka" } - -[libraries] -androidx-annotation = { module = "androidx.annotation:annotation", version = "1.10.0" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index b1b8ef5..7454180 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5dd3c01..aa24542 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ +#Tue Aug 03 01:54:51 CST 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685..744e882 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/bin/sh +#!/usr/bin/env sh # -# Copyright © 2015 the original authors. +# Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,114 +15,81 @@ # 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/. -# +## +## Gradle start up script for UN*X +## ############################################################################## # 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 +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null -# 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 +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum +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 ;; +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; esac +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # 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 + JAVACMD="$JAVA_HOME/jre/sh/java" else - JAVACMD=$JAVA_HOME/bin/java + JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -131,118 +98,88 @@ 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. + JAVACMD="java" + which java >/dev/null 2>&1 || 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 +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi 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 Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi # For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi # 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" ) + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$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 + i=`expr $i + 1` done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac fi +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` -# 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" \ - -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' ' ' - )" '"$@"' +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24c62d5..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,22 +13,19 @@ @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 +@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions +@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 +if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -43,15 +40,15 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute +if "%ERRORLEVEL%" == "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 +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. -"%COMSPEC%" /c exit 1 +goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,24 +56,34 @@ 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 +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. -"%COMSPEC%" /c exit 1 +goto fail :execute @rem Setup the command line +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +:omega diff --git a/interface/.gitignore b/interface/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/interface/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/interface/build.gradle.kts b/interface/build.gradle.kts index c5c4ae8..a61f1d8 100644 --- a/interface/build.gradle.kts +++ b/interface/build.gradle.kts @@ -1,109 +1,30 @@ plugins { - alias(libs.plugins.agp.lib) - alias(libs.plugins.dokka) - alias(libs.plugins.dokka.javadoc) - `maven-publish` - signing + id("com.android.library") } +val androidCompileSdkVersion: Int by rootProject.extra +val androidBuildToolsVersion: String by rootProject.extra +val androidTargetSdkVersion: Int by rootProject.extra +val androidMinSdkVersion: Int by rootProject.extra +val androidSourceCompatibility: JavaVersion by rootProject.extra +val androidTargetCompatibility: JavaVersion by rootProject.extra android { - namespace = "io.github.libxposed.service.interfaces" - compileSdk = 37 - buildToolsVersion = "37.0.0" - androidResources.enable = false - enableKotlin = false + compileSdk = androidCompileSdkVersion + buildToolsVersion = androidBuildToolsVersion defaultConfig { - minSdk = 26 + minSdk = androidMinSdkVersion + targetSdk = androidTargetSdkVersion + consumerProguardFiles("proguard-rules.pro") } - buildFeatures { - buildConfig = false - aidl = true - } - - compileOptions { - targetCompatibility = JavaVersion.VERSION_17 - sourceCompatibility = JavaVersion.VERSION_17 - } - - publishing { - singleVariant("release") { - withSourcesJar() + buildTypes { + release { + isMinifyEnabled = false } } -} - -val libVersion = "102.0.0" -val publishSnapshot = providers.gradleProperty("publishSnapshot").orNull == "true" -fun String.real(snapshot: Boolean) = if (snapshot) "$this-SNAPSHOT" else this - -val dokkaJavadocJar by tasks.registering(Jar::class) { - archiveClassifier.set("javadoc") - dependsOn("dokkaGeneratePublicationJavadoc") - from(layout.buildDirectory.dir("dokka/javadoc")) -} - -publishing { - publications { - register("interface") { - artifactId = "interface" - group = "io.github.libxposed" - version = libVersion.real(publishSnapshot) - artifact(dokkaJavadocJar) - pom { - name.set("interface") - description.set("Modern Xposed Service Interface") - url.set("https://github.com/libxposed/service") - licenses { - license { - name.set("Apache License 2.0") - url.set("https://github.com/libxposed/service/blob/master/LICENSE") - } - } - developers { - developer { - name.set("libxposed") - url.set("https://libxposed.github.io") - } - } - scm { - connection.set("scm:git:https://github.com/libxposed/service.git") - url.set("https://github.com/libxposed/service") - } - } - afterEvaluate { - from(components.getByName("release")) - } - } - } - repositories { - maven { - name = "ossrh" - url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/") - credentials(PasswordCredentials::class) - } - maven { - name = "snapshots" - url = uri("https://central.sonatype.com/repository/maven-snapshots/") - credentials(PasswordCredentials::class) - } - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/libxposed/service") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } -} - -signing { - val signingKey = findProperty("signingKey") as String? - val signingPassword = findProperty("signingPassword") as String? - if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) { - useInMemoryPgpKeys(signingKey, signingPassword) - sign(publishing.publications) + compileOptions { + sourceCompatibility = androidSourceCompatibility + targetCompatibility = androidTargetCompatibility } } diff --git a/interface/proguard-rules.pro b/interface/proguard-rules.pro new file mode 100644 index 0000000..b249168 --- /dev/null +++ b/interface/proguard-rules.pro @@ -0,0 +1,4 @@ +-keepnames class * implements android.os.Parcelable +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} diff --git a/interface/src/main/AndroidManifest.xml b/interface/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9e9832c --- /dev/null +++ b/interface/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/interface/src/main/aidl/io/github/libxposed/service/HookedProcess.aidl b/interface/src/main/aidl/io/github/libxposed/service/HookedProcess.aidl deleted file mode 100644 index 9239947..0000000 --- a/interface/src/main/aidl/io/github/libxposed/service/HookedProcess.aidl +++ /dev/null @@ -1,59 +0,0 @@ -package io.github.libxposed.service; - -/** - * Information about a process currently hooked by this module. - */ -parcelable HookedProcess { - /** - * The target is running the currently installed module code. - */ - const int TARGET_STATE_UP_TO_DATE = 0; - - /** - * The target is still running old module code and may be hot-reloaded. - */ - const int TARGET_STATE_STALE = 1; - - /** - * The target is currently being hot-reloaded. - */ - const int TARGET_STATE_RELOADING = 2; - - /** - * The target's last hot reload attempt failed because the old module refused reload or reload - * raised an exception. - */ - const int TARGET_STATE_FAILED = 3; - - /** - * Opaque identifier assigned by the framework. Module apps must only pass this value back to - * the service and must not infer ordering, lifetime, or process identity from it. - */ - long targetId; - - /** - * The process uid, provided for display and diagnostics. - */ - int uid; - - /** - * The process id, provided for display and diagnostics. It must not be used as target identity. - */ - int pid; - - /** - * The Android process name, provided for display and diagnostics. - */ - String processName; - - /** - * One of TARGET_STATE_*. - */ - int state; - - /** - * Version code of the module package loaded in this process. This is only a diagnostic value; - * the framework may use a stronger internal code identity to determine state. - */ - long loadedVersionCode; -} diff --git a/interface/src/main/aidl/io/github/libxposed/service/IHotReloadCallback.aidl b/interface/src/main/aidl/io/github/libxposed/service/IHotReloadCallback.aidl deleted file mode 100644 index 05c7bc5..0000000 --- a/interface/src/main/aidl/io/github/libxposed/service/IHotReloadCallback.aidl +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.libxposed.service; - -/** - * Callback for asynchronous hot reload completion. - */ -interface IHotReloadCallback { - /** - * Called when hot reload completes or fails. - * - * @param status The raw hot reload status - * @param message Optional diagnostic message; null for module-refused reloads - */ - oneway void onHotReloadResult(int status, String message) = 1; -} diff --git a/interface/src/main/aidl/io/github/libxposed/service/IXposedScopeCallback.aidl b/interface/src/main/aidl/io/github/libxposed/service/IXposedScopeCallback.aidl deleted file mode 100644 index 11531fa..0000000 --- a/interface/src/main/aidl/io/github/libxposed/service/IXposedScopeCallback.aidl +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.libxposed.service; - -interface IXposedScopeCallback { - oneway void onScopeRequestApproved(in List approved) = 1; - oneway void onScopeRequestFailed(String message) = 2; -} diff --git a/interface/src/main/aidl/io/github/libxposed/service/IXposedService.aidl b/interface/src/main/aidl/io/github/libxposed/service/IXposedService.aidl deleted file mode 100644 index 6b64cb0..0000000 --- a/interface/src/main/aidl/io/github/libxposed/service/IXposedService.aidl +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.libxposed.service; - -import io.github.libxposed.service.HookedProcess; -import io.github.libxposed.service.IHotReloadCallback; -import io.github.libxposed.service.IXposedScopeCallback; - -interface IXposedService { - const String AUTHORITY_SUFFIX = ".XposedService"; - const String SEND_BINDER = "SendBinder"; - - /** - * API version 101. - */ - const int API_101 = 101; - /** - * API version 102. - */ - const int API_102 = 102; - /** - * API version implemented by this interface. - */ - const int LIB_API = API_102; - - /** - * Framework property bit: system process hooking is supported. - */ - const long PROP_CAP_SYSTEM = 1L; - /** - * Framework property bit: remote preferences and files are supported. - */ - const long PROP_CAP_REMOTE = 1L << 1; - /** - * Framework property bit: runtime API protection is enforced. - */ - const long PROP_RT_API_PROTECTION = 1L << 2; - /** - * Raw hot reload status: succeeded. - */ - const int HOT_RELOAD_SUCCEEDED = 0; - /** - * Raw hot reload status: module refused reload or reload raised an exception. - */ - const int HOT_RELOAD_FAILED = 1; - /** - * Raw hot reload status: unsupported. - */ - const int HOT_RELOAD_UNSUPPORTED = 2; - /** - * Raw hot reload status: target already reloading. - */ - const int HOT_RELOAD_IN_PROGRESS = 3; - /** - * Raw hot reload status: target process died. - */ - const int HOT_RELOAD_PROCESS_DIED = 4; - - // framework details - int getApiVersion() = 1; - String getFrameworkName() = 2; - String getFrameworkVersion() = 3; - long getFrameworkVersionCode() = 4; - long getFrameworkProperties() = 5; - - // scope utilities - List getScope() = 10; - oneway void requestScope(in List packages, IXposedScopeCallback callback) = 11; - void removeScope(in List packages) = 12; - - /** - * Returns running processes currently hooked by this module. Returned target ids are opaque, - * process-scoped tokens and may become invalid after this call returns. - */ - List getRunningTargets() = 13; - - /** - * Requests hot reload for a target returned by getRunningTargets(). Implementations should - * validate and enqueue the request promptly, then report completion through the callback. - * If hot reload is unsupported for the module or target, implementations should report - * HOT_RELOAD_UNSUPPORTED through the callback. - * - * @throws SecurityException if the target id is invalid or no longer belongs to this module - */ - void hotReloadModule(long targetId, in Bundle data, IHotReloadCallback callback) = 14; - - // remote preference utilities - Bundle requestRemotePreferences(String group) = 20; - void updateRemotePreferences(String group, in Bundle diff) = 21; - void deleteRemotePreferences(String group) = 22; - - // remote file utilities - String[] listRemoteFiles() = 30; - ParcelFileDescriptor openRemoteFile(String name) = 31; - boolean deleteRemoteFile(String name) = 32; -} diff --git a/interface/src/main/aidl/io/github/xposed/xposedservice/IXRemotePreferenceCallback.aidl b/interface/src/main/aidl/io/github/xposed/xposedservice/IXRemotePreferenceCallback.aidl new file mode 100644 index 0000000..82dc154 --- /dev/null +++ b/interface/src/main/aidl/io/github/xposed/xposedservice/IXRemotePreferenceCallback.aidl @@ -0,0 +1,5 @@ +package io.github.xposed.xposedservice; + +interface IXRemotePreferenceCallback { + oneway void onUpdate(in Bundle map); +} diff --git a/interface/src/main/aidl/io/github/xposed/xposedservice/IXposedService.aidl b/interface/src/main/aidl/io/github/xposed/xposedservice/IXposedService.aidl new file mode 100644 index 0000000..c42b811 --- /dev/null +++ b/interface/src/main/aidl/io/github/xposed/xposedservice/IXposedService.aidl @@ -0,0 +1,7 @@ +package io.github.xposed.xposedservice; + +import io.github.xposed.xposedservice.utils.ParceledListSlice; + +interface IXposedService { + int getVersion() = 1; +} diff --git a/interface/src/main/aidl/io/github/xposed/xposedservice/utils/ParceledListSlice.aidl b/interface/src/main/aidl/io/github/xposed/xposedservice/utils/ParceledListSlice.aidl new file mode 100644 index 0000000..190a6b0 --- /dev/null +++ b/interface/src/main/aidl/io/github/xposed/xposedservice/utils/ParceledListSlice.aidl @@ -0,0 +1,3 @@ +package io.github.xposed.xposedservice.utils; + +parcelable ParceledListSlice; \ No newline at end of file diff --git a/interface/src/main/java/io/github/xposed/xposedservice/utils/BaseParceledListSlice.java b/interface/src/main/java/io/github/xposed/xposedservice/utils/BaseParceledListSlice.java new file mode 100644 index 0000000..ac60753 --- /dev/null +++ b/interface/src/main/java/io/github/xposed/xposedservice/utils/BaseParceledListSlice.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.xposed.xposedservice.utils; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. + *

+ * Caveat: for efficiency and security, all elements must be the same concrete type. + * In order to avoid writing the class name of each object, we must ensure that + * each object is the same type, or else unparceling then reparceling the data may yield + * a different result if the class name encoded in the Parcelable is a Base type. + * See b/17671747. + */ +abstract class BaseParceledListSlice implements Parcelable { + private static final String TAG = "ParceledListSlice"; + private static final boolean DEBUG = false; + private static final int MAX_IPC_SIZE = 64 * 1024; + + private final List mList; + + private int mInlineCountLimit = Integer.MAX_VALUE; + + public BaseParceledListSlice(List list) { + mList = list; + } + + BaseParceledListSlice(Parcel p, ClassLoader loader) { + final int N = p.readInt(); + mList = new ArrayList<>(N); + if (DEBUG) Log.d(TAG, "Retrieving " + N + " items"); + if (N <= 0) { + return; + } + + Parcelable.Creator creator = readParcelableCreator(p, loader); + Class listElementClass = null; + + int i = 0; + while (i < N) { + if (p.readInt() == 0) { + break; + } + + final T parcelable = readCreator(creator, p, loader); + if (listElementClass == null) { + listElementClass = parcelable.getClass(); + } else { + verifySameType(listElementClass, parcelable.getClass()); + } + + mList.add(parcelable); + + if (DEBUG) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size() - 1)); + i++; + } + if (i >= N) { + return; + } + final IBinder retriever = p.readStrongBinder(); + while (i < N) { + if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever); + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInt(i); + try { + retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failure retrieving array; only received " + i + " of " + N, e); + return; + } + while (i < N && reply.readInt() != 0) { + final T parcelable = readCreator(creator, reply, loader); + verifySameType(listElementClass, parcelable.getClass()); + + mList.add(parcelable); + + if (DEBUG) Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size() - 1)); + i++; + } + reply.recycle(); + data.recycle(); + } + } + + @SuppressWarnings("unchecked") + private T readCreator(Parcelable.Creator creator, Parcel p, ClassLoader loader) { + if (creator instanceof Parcelable.ClassLoaderCreator) { + Parcelable.ClassLoaderCreator classLoaderCreator = + (Parcelable.ClassLoaderCreator) creator; + return (T) classLoaderCreator.createFromParcel(p, loader); + } + return (T) creator.createFromParcel(p); + } + + private static void verifySameType(final Class expected, final Class actual) { + if (!Objects.equals(actual, expected)) { + throw new IllegalArgumentException("Can't unparcel type " + + (actual == null ? null : actual.getName()) + " in list of type " + + (expected == null ? null : expected.getName())); + } + } + + public List getList() { + return mList; + } + + /** + * Set a limit on the maximum number of entries in the array that will be included + * inline in the initial parcelling of this object. + */ + public void setInlineCountLimit(int maxCount) { + mInlineCountLimit = maxCount; + } + + /** + * Write this to another Parcel. Note that this discards the internal Parcel + * and should not be used anymore. This is so we can pass this to a Binder + * where we won't have a chance to call recycle on this. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + final int N = mList.size(); + final int callFlags = flags; + dest.writeInt(N); + if (DEBUG) Log.d(TAG, "Writing " + N + " items"); + if (N > 0) { + final Class listElementClass = mList.get(0).getClass(); + writeParcelableCreator(mList.get(0), dest); + int i = 0; + while (i < N && i < mInlineCountLimit && dest.dataSize() < MAX_IPC_SIZE) { + dest.writeInt(1); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + writeElement(parcelable, dest, callFlags); + + if (DEBUG) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + dest.writeInt(0); + Binder retriever = new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + if (code != FIRST_CALL_TRANSACTION) { + return super.onTransact(code, data, reply, flags); + } + int i = data.readInt(); + if (DEBUG) Log.d(TAG, "Writing more @" + i + " of " + N); + while (i < N && reply.dataSize() < MAX_IPC_SIZE) { + reply.writeInt(1); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + writeElement(parcelable, reply, callFlags); + + if (DEBUG) Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N); + reply.writeInt(0); + } + return true; + } + }; + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever); + dest.writeStrongBinder(retriever); + } + } + } + + protected abstract void writeElement(T parcelable, Parcel reply, int callFlags); + + protected abstract void writeParcelableCreator(T parcelable, Parcel dest); + + protected abstract Parcelable.Creator readParcelableCreator(Parcel from, ClassLoader loader); +} diff --git a/interface/src/main/java/io/github/xposed/xposedservice/utils/ParceledListSlice.java b/interface/src/main/java/io/github/xposed/xposedservice/utils/ParceledListSlice.java new file mode 100644 index 0000000..4de385a --- /dev/null +++ b/interface/src/main/java/io/github/xposed/xposedservice/utils/ParceledListSlice.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.xposed.xposedservice.utils; + +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Collections; +import java.util.List; + +/** + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. + * + * @see BaseParceledListSlice + */ +public class ParceledListSlice extends BaseParceledListSlice { + public ParceledListSlice(List list) { + super(list); + } + + private ParceledListSlice(Parcel in, ClassLoader loader) { + super(in, loader); + } + + public static ParceledListSlice emptyList() { + return new ParceledListSlice<>(Collections.emptyList()); + } + + @Override + public int describeContents() { + int contents = 0; + final List list = getList(); + for (int i = 0; i < list.size(); i++) { + contents |= list.get(i).describeContents(); + } + return contents; + } + + @Override + protected void writeElement(T parcelable, Parcel dest, int callFlags) { + parcelable.writeToParcel(dest, callFlags); + } + + @SuppressLint("NewApi") + @Override + protected void writeParcelableCreator(T parcelable, Parcel dest) { + dest.writeParcelableCreator((Parcelable) parcelable); + } + + @SuppressLint("NewApi") + @Override + protected Parcelable.Creator readParcelableCreator(Parcel from, ClassLoader loader) { + return from.readParcelableCreator(loader == null ? getClass().getClassLoader() : loader); + } + + @SuppressWarnings("rawtypes") + public static final Parcelable.ClassLoaderCreator CREATOR = + new Parcelable.ClassLoaderCreator<>() { + public ParceledListSlice createFromParcel(Parcel in) { + return new ParceledListSlice<>(in, null); + } + + @Override + public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { + return new ParceledListSlice<>(in, loader); + } + + @Override + public ParceledListSlice[] newArray(int size) { + return new ParceledListSlice[size]; + } + }; +} diff --git a/service/.gitignore b/service/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/service/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/service/build.gradle.kts b/service/build.gradle.kts index bd7cc55..eaa978a 100644 --- a/service/build.gradle.kts +++ b/service/build.gradle.kts @@ -1,125 +1,35 @@ plugins { - alias(libs.plugins.agp.lib) - alias(libs.plugins.dokka) - alias(libs.plugins.dokka.javadoc) - `maven-publish` - signing + id("com.android.library") } +val androidCompileSdkVersion: Int by rootProject.extra +val androidBuildToolsVersion: String by rootProject.extra +val androidTargetSdkVersion: Int by rootProject.extra +val androidMinSdkVersion: Int by rootProject.extra +val androidSourceCompatibility: JavaVersion by rootProject.extra +val androidTargetCompatibility: JavaVersion by rootProject.extra + android { - namespace = "io.github.libxposed.service" - compileSdk = 37 - buildToolsVersion = "37.0.0" - androidResources.enable = false - enableKotlin = false + compileSdk = androidCompileSdkVersion + buildToolsVersion = androidBuildToolsVersion defaultConfig { - minSdk = 26 + minSdk = androidMinSdkVersion + targetSdk = androidTargetSdkVersion consumerProguardFiles("proguard-rules.pro") } - buildFeatures { - buildConfig = false + buildTypes { + release { + isMinifyEnabled = false + } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - publishing { - singleVariant("release") { - withSourcesJar() - } + sourceCompatibility = androidSourceCompatibility + targetCompatibility = androidTargetCompatibility } } -val libVersion = "102.0.0" -val publishSnapshot = providers.gradleProperty("publishSnapshot").orNull == "true" -val dependencySnapshot = providers.gradleProperty("dependencySnapshot").orNull == "true" -fun String.real(snapshot: Boolean) = if (snapshot) "$this-SNAPSHOT" else this -val libxposedAnnotation = "io.github.libxposed:annotation:" + libs.versions.libxposed.annotation.get() -val libxposedLint = "io.github.libxposed:lint:" + libs.versions.libxposed.lint.get() - dependencies { - implementation(project(":interface")) - compileOnly(libs.androidx.annotation) - compileOnly(libxposedAnnotation.real(dependencySnapshot)) - lintPublish(libxposedLint.real(dependencySnapshot)) -} - -dokka { - dokkaSourceSets.register("main") { - sourceRoots.from(file("src/main/java")) - } -} - -val dokkaJavadocJar by tasks.registering(Jar::class) { - archiveClassifier.set("javadoc") - dependsOn("dokkaGeneratePublicationJavadoc") - from(layout.buildDirectory.dir("dokka/javadoc")) -} - -publishing { - publications { - register("service") { - artifactId = "service" - group = "io.github.libxposed" - version = libVersion.real(publishSnapshot) - artifact(dokkaJavadocJar) - pom { - name.set("service") - description.set("Modern Xposed Service Interface") - url.set("https://github.com/libxposed/service") - licenses { - license { - name.set("Apache License 2.0") - url.set("https://github.com/libxposed/service/blob/master/LICENSE") - } - } - developers { - developer { - name.set("libxposed") - url.set("https://libxposed.github.io") - } - } - scm { - connection.set("scm:git:https://github.com/libxposed/service.git") - url.set("https://github.com/libxposed/service") - } - } - afterEvaluate { - from(components.getByName("release")) - } - } - } - repositories { - maven { - name = "ossrh" - url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/") - credentials(PasswordCredentials::class) - } - maven { - name = "snapshots" - url = uri("https://central.sonatype.com/repository/maven-snapshots/") - credentials(PasswordCredentials::class) - } - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/libxposed/service") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } -} - -signing { - val signingKey = findProperty("signingKey") as String? - val signingPassword = findProperty("signingPassword") as String? - if (!signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()) { - useInMemoryPgpKeys(signingKey, signingPassword) - sign(publishing.publications) - } + api(project(":interface")) } diff --git a/service/proguard-rules.pro b/service/proguard-rules.pro index bb07a47..a44d839 100644 --- a/service/proguard-rules.pro +++ b/service/proguard-rules.pro @@ -1 +1,3 @@ --dontwarn io.github.libxposed.annotation.** +-keepclassmembers class io.github.xposed.xposedservice.XposedService { + private static android.os.IBinder serviceBinder; +} diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 715f03f..9e9832c 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -1,11 +1,2 @@ - - - - - - + + \ No newline at end of file diff --git a/service/src/main/java/io/github/libxposed/service/HookedTarget.java b/service/src/main/java/io/github/libxposed/service/HookedTarget.java deleted file mode 100644 index 633aacc..0000000 --- a/service/src/main/java/io/github/libxposed/service/HookedTarget.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.github.libxposed.service; - -import androidx.annotation.NonNull; - -import java.util.Objects; - -import io.github.libxposed.annotation.SinceApi; - -/** - * Information about a process currently hooked by this module. - *

Instances are snapshot handles and do not define stable value equality.

- */ -@SinceApi(XposedService.API_102) -@SuppressWarnings("unused") -public final class HookedTarget { - /** - * State of a hooked target. - */ - public enum State { - /** - * The target is running the currently installed module code. - */ - UP_TO_DATE, - - /** - * The target is still running old module code and may be hot-reloaded. - */ - STALE, - - /** - * The target is currently being hot-reloaded. - */ - RELOADING, - - /** - * The target's last hot reload attempt failed because the old module refused reload or - * reload raised an exception. - */ - FAILED - } - - final long mTargetId; - private final int mUid; - private final int mPid; - private final String mProcessName; - private final State mState; - private final long mLoadedVersionCode; - - HookedTarget(long targetId, int uid, int pid, @NonNull String processName, - @NonNull State state, long loadedVersionCode) { - mTargetId = targetId; - mUid = uid; - mPid = pid; - mProcessName = Objects.requireNonNull(processName); - mState = Objects.requireNonNull(state); - mLoadedVersionCode = loadedVersionCode; - } - - /** - * Gets the process uid, provided for display and diagnostics. - */ - public int getUid() { - return mUid; - } - - /** - * Gets the process id, provided for display and diagnostics. - * It must not be used as target identity. - */ - public int getPid() { - return mPid; - } - - /** - * Gets the Android process name, provided for display and diagnostics. - */ - @NonNull - public String getProcessName() { - return mProcessName; - } - - /** - * Gets the target state. - */ - @NonNull - public State getState() { - return mState; - } - - /** - * Gets the version code of the module package loaded in this process. - * This is only a diagnostic value; the framework may use a stronger internal code identity to - * determine state. - */ - public long getLoadedVersionCode() { - return mLoadedVersionCode; - } -} diff --git a/service/src/main/java/io/github/libxposed/service/HotReloadResult.java b/service/src/main/java/io/github/libxposed/service/HotReloadResult.java deleted file mode 100644 index 01f2d10..0000000 --- a/service/src/main/java/io/github/libxposed/service/HotReloadResult.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.libxposed.service; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.github.libxposed.annotation.SinceApi; - -/** - * Result of an asynchronous hot reload request. - * - * @param status The completion status - * @param message Optional framework-provided diagnostic message. For {@link Status#FAILED}, a - * null message means the old module refused reload by returning {@code false} from - * {@code onHotReloading}; a non-null message describes a reload exception. - */ -@SinceApi(XposedService.API_102) -public record HotReloadResult(@NonNull Status status, @Nullable String message) { - /** - * Hot reload completion status. - */ - public enum Status { - /** - * Hot reload completed successfully. - */ - SUCCEEDED, - - /** - * The old module refused reload, or hot reload raised an exception. - *

- * When the old module refuses reload by returning {@code false} from - * {@code onHotReloading}, {@link HotReloadResult#message()} is null. When reload fails - * because of an exception, the message contains a framework-provided diagnostic string. - *

- */ - FAILED, - - /** - * The target does not support hot reload. - *

- * For example, this can be returned for modules that do not declare exactly one Java - * entry class or targets for which the framework cannot provide a valid new module - * generation. - *

- */ - UNSUPPORTED, - - /** - * The target is already being hot-reloaded. - */ - IN_PROGRESS, - - /** - * The target process died before hot reload could complete. - */ - PROCESS_DIED - } - - static HotReloadResult from(int code, @Nullable String message) { - var status = switch (code) { - case IXposedService.HOT_RELOAD_SUCCEEDED -> Status.SUCCEEDED; - case IXposedService.HOT_RELOAD_FAILED -> Status.FAILED; - case IXposedService.HOT_RELOAD_UNSUPPORTED -> Status.UNSUPPORTED; - case IXposedService.HOT_RELOAD_IN_PROGRESS -> Status.IN_PROGRESS; - case IXposedService.HOT_RELOAD_PROCESS_DIED -> Status.PROCESS_DIED; - default -> throw new XposedService.ServiceException("Invalid hot reload status code: " + code); - }; - return new HotReloadResult(status, message); - } -} diff --git a/service/src/main/java/io/github/libxposed/service/RemotePreferences.java b/service/src/main/java/io/github/libxposed/service/RemotePreferences.java deleted file mode 100644 index b08dfd7..0000000 --- a/service/src/main/java/io/github/libxposed/service/RemotePreferences.java +++ /dev/null @@ -1,247 +0,0 @@ -package io.github.libxposed.service; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.WeakHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@SuppressWarnings("unchecked") -public final class RemotePreferences implements SharedPreferences { - - private static final String TAG = "RemotePreferences"; - private static final Object CONTENT = new Object(); - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); - - static volatile boolean shouldNotifyCleared = false; - - private final XposedService mService; - private final String mGroup; - private final Map mListeners = Collections.synchronizedMap(new WeakHashMap<>()); - - private volatile Map mMap; - - private RemotePreferences(XposedService service, String group) { - this.mService = service; - this.mGroup = group; - } - - @NonNull - static RemotePreferences newInstance(XposedService service, String group) throws RemoteException { - var output = service.asInterface().requestRemotePreferences(group); - if (output == null) throw new RemoteException("Framework returns null"); - var prefs = new RemotePreferences(service, group); - var map = (Map) output.getSerializable("map"); - if (map != null) prefs.mMap = Collections.unmodifiableMap(map); - else prefs.mMap = Collections.emptyMap(); - return prefs; - } - - synchronized void onDelete() { - mMap = Collections.emptyMap(); - } - - @Override - public Map getAll() { - return new TreeMap<>(mMap); - } - - @Nullable - @Override - public String getString(String key, @Nullable String defValue) { - return (String) mMap.getOrDefault(key, defValue); - } - - @Nullable - @Override - public Set getStringSet(String key, @Nullable Set defValues) { - return (Set) mMap.getOrDefault(key, defValues); - } - - @Override - public int getInt(String key, int defValue) { - var v = mMap.get(key); - return v != null ? (int) v : defValue; - } - - @Override - public long getLong(String key, long defValue) { - var v = mMap.get(key); - return v != null ? (long) v : defValue; - } - - @Override - public float getFloat(String key, float defValue) { - var v = mMap.get(key); - return v != null ? (float) v : defValue; - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - var v = mMap.get(key); - return v != null ? (boolean) v : defValue; - } - - @Override - public boolean contains(String key) { - return mMap.containsKey(key); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - mListeners.put(listener, CONTENT); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - mListeners.remove(listener); - } - - @Override - public Editor edit() { - return new Editor(); - } - - public class Editor implements SharedPreferences.Editor { - - private final HashSet mDelete = new HashSet<>(); - private final HashMap mPut = new HashMap<>(); - private boolean mClear = false; - - private void put(String key, @NonNull Object value) { - mDelete.remove(key); - mPut.put(key, value); - } - - @Override - public SharedPreferences.Editor putString(String key, @Nullable String value) { - if (value == null) remove(key); - else put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putStringSet(String key, @Nullable Set values) { - if (values == null) remove(key); - else put(key, values); - return this; - } - - @Override - public SharedPreferences.Editor putInt(String key, int value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putLong(String key, long value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putFloat(String key, float value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putBoolean(String key, boolean value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor remove(String key) { - mDelete.add(key); - mPut.remove(key); - return this; - } - - @Override - public SharedPreferences.Editor clear() { - mClear = true; - mDelete.clear(); - mPut.clear(); - return this; - } - - private void doUpdate() { - synchronized (RemotePreferences.this) { - var newMap = new HashMap<>(mMap); - if (mClear) newMap.clear(); - mDelete.forEach(newMap::remove); - newMap.putAll(mPut); - mMap = Collections.unmodifiableMap(newMap); - } - // Snapshot listeners to avoid ConcurrentModificationException - // Collections.synchronizedMap does not synchronize iteration - List listeners; - synchronized (mListeners) { - listeners = new ArrayList<>(mListeners.keySet()); - } - for (var listener : listeners) { - if (mClear && shouldNotifyCleared) { - listener.onSharedPreferenceChanged(RemotePreferences.this, null); - } - for (var key : mDelete) { - listener.onSharedPreferenceChanged(RemotePreferences.this, key); - } - for (var key : mPut.keySet()) { - listener.onSharedPreferenceChanged(RemotePreferences.this, key); - } - } - } - - private Bundle buildCommitBundle() { - if (!mClear && mDelete.isEmpty() && mPut.isEmpty()) return null; - var bundle = new Bundle(); - bundle.putBoolean("clear", mClear); - bundle.putSerializable("delete", new HashSet<>(mDelete)); - bundle.putSerializable("put", new HashMap<>(mPut)); - return bundle; - } - - private boolean doCommit(Bundle bundle) { - if (bundle != null) { - try { - mService.asInterface().updateRemotePreferences(mGroup, bundle); - } catch (RemoteException e) { - Log.e(TAG, "Failed to commit changes to framework", e); - return false; - } - } - return true; - } - - @Override - public boolean commit() { - var bundle = buildCommitBundle(); - if (bundle == null) return true; - doUpdate(); - return doCommit(bundle); - } - - @Override - public void apply() { - var bundle = buildCommitBundle(); - if (bundle == null) return; - doUpdate(); - EXECUTOR.execute(() -> doCommit(bundle)); - } - } -} diff --git a/service/src/main/java/io/github/libxposed/service/XposedProvider.java b/service/src/main/java/io/github/libxposed/service/XposedProvider.java deleted file mode 100644 index be87f8e..0000000 --- a/service/src/main/java/io/github/libxposed/service/XposedProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.libxposed.service; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Objects; - -public final class XposedProvider extends ContentProvider { - - private static final String TAG = "XposedProvider"; - - @Override - public boolean onCreate() { - var targetSdk = Objects.requireNonNull(getContext()).getApplicationInfo().targetSdkVersion; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && targetSdk >= Build.VERSION_CODES.R) { - RemotePreferences.shouldNotifyCleared = true; - } - return true; - } - - @Nullable - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { - return null; - } - - @Nullable - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - @Nullable - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } - - @Override - public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } - - @Nullable - @Override - public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { - if (method.equals(IXposedService.SEND_BINDER) && extras != null) { - IBinder binder = extras.getBinder("binder"); - if (binder != null) { - Log.d(TAG, "binder received: " + binder); - XposedServiceHelper.onBinderReceived(binder); - } - return new Bundle(); - } - return null; - } -} diff --git a/service/src/main/java/io/github/libxposed/service/XposedService.java b/service/src/main/java/io/github/libxposed/service/XposedService.java deleted file mode 100644 index 703bb8f..0000000 --- a/service/src/main/java/io/github/libxposed/service/XposedService.java +++ /dev/null @@ -1,458 +0,0 @@ -package io.github.libxposed.service; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.annotation.SinceApi; - -@SuppressWarnings("unused") -public final class XposedService { - /** - * API version 101. - */ - public static final int API_101 = IXposedService.API_101; - - /** - * API version 102. - *

API additions:

- *
    - *
  • Running hooked targets can be queried.
  • - *
  • Hot reload can be requested for a hooked target when permitted by the framework.
  • - *
- */ - public static final int API_102 = IXposedService.API_102; - - /** - * The framework has the capability to hook system_server and other system processes. - */ - public static final long PROP_CAP_SYSTEM = IXposedService.PROP_CAP_SYSTEM; - - /** - * The framework provides remote preferences and remote files support. - */ - public static final long PROP_CAP_REMOTE = IXposedService.PROP_CAP_REMOTE; - - /** - * The framework disallows accessing Xposed API via reflection or dynamically loaded code. - */ - public static final long PROP_RT_API_PROTECTION = IXposedService.PROP_RT_API_PROTECTION; - - public static final class ServiceException extends RuntimeException { - ServiceException(String message) { - super(message); - } - - ServiceException(RemoteException e) { - super("Xposed service error", e); - } - } - - private static final Map scopeCallbacks = new ConcurrentHashMap<>(); - private static final Set hotReloadCallbacks = ConcurrentHashMap.newKeySet(); - - /** - * Callback interface for module scope request. - */ - public interface OnScopeEventListener { - /** - * Callback when the request is approved. - * - * @param approved Approved packages for the request - */ - default void onScopeRequestApproved(@NonNull List approved) { - } - - /** - * Callback when the request is failed. - * - * @param message Error message - */ - default void onScopeRequestFailed(@NonNull String message) { - } - - private IXposedScopeCallback asInterface() { - return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() { - @Override - public void onScopeRequestApproved(List approved) { - listener.onScopeRequestApproved(approved); - scopeCallbacks.remove(listener); - } - - @Override - public void onScopeRequestFailed(String message) { - listener.onScopeRequestFailed(message); - scopeCallbacks.remove(listener); - } - }); - } - } - - /** - * Callback interface for hot reload requests. - */ - @SinceApi(API_102) - public interface HotReloadCallback { - /** - * Called when hot reload completes or fails. - *

- * This callback may run on a Binder thread. Dispatch to the main thread before touching UI. - *

- * - * @param target The target process passed to - * {@link #hotReloadModule(HookedTarget, Bundle, HotReloadCallback)} - * @param result The hot reload result - */ - void onHotReloadResult(@NonNull HookedTarget target, @NonNull HotReloadResult result); - } - - private final IXposedService mService; - private final Map mRemotePrefs = new HashMap<>(); - - XposedService(IXposedService service) { - mService = service; - } - - IXposedService asInterface() { - return mService; - } - - private void requireApi(int api) { - if (getApiVersion() < api) { - throw new UnsupportedOperationException("Requires Xposed service API " + api); - } - } - - /** - * Get the Xposed API version of current implementation. - * - * @return API version - * @throws ServiceException If the service is dead or an error occurred - */ - public int getApiVersion() { - try { - return mService.getApiVersion(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Get the Xposed framework name of current implementation. - * - * @return Framework name - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public String getFrameworkName() { - try { - return mService.getFrameworkName(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Get the Xposed framework version of current implementation. - * - * @return Framework version - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public String getFrameworkVersion() { - try { - return mService.getFrameworkVersion(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Get the Xposed framework version code of current implementation. - * - * @return Framework version code - * @throws ServiceException If the service is dead or an error occurred - */ - public long getFrameworkVersionCode() { - try { - return mService.getFrameworkVersionCode(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Gets the Xposed framework properties. - * Properties with prefix PROP_RT_ may change among launches. - * - * @return Framework properties - * @throws ServiceException If the service is dead or an error occurred - */ - public long getFrameworkProperties() { - try { - return mService.getFrameworkProperties(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Get the application scope of current module. - * - * @return Module scope - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public List getScope() { - try { - return mService.getScope(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Request to add a new app to the module scope. - * - * @param packages Packages to be added - * @param callback Callback to be invoked when the request is completed or error occurred - * @throws ServiceException If the service is dead or an error occurred - */ - public void requestScope(@NonNull List packages, @NonNull OnScopeEventListener callback) { - try { - mService.requestScope(packages, callback.asInterface()); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Remove an app from the module scope. - * - * @param packages Packages to be removed - * @throws ServiceException If the service is dead or an error occurred - */ - public void removeScope(@NonNull List packages) { - try { - mService.removeScope(packages); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Get a list of currently running processes that are hooked by the module. - *

- * Returned targets can be passed to - * {@link #hotReloadModule(HookedTarget, Bundle, HotReloadCallback)}. The pid, uid, process - * name, and loaded version code are diagnostic values only. - *

- * - * @return The list of hooked processes - * @throws UnsupportedOperationException If the framework does not support service API 102 - * @throws ServiceException If the service is dead or an error occurred - */ - @SinceApi(API_102) - @NonNull - public List getRunningTargets() { - requireApi(API_102); - try { - var processes = mService.getRunningTargets(); - if (processes == null) throw new ServiceException("Framework returns null"); - var targets = new ArrayList(processes.size()); - for (var process : processes) { - if (process == null) throw new ServiceException("Framework returns null target"); - targets.add(toHookedTarget(process)); - } - return Collections.unmodifiableList(targets); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Request hot reload for the module in the specified process. The process must be one of the - * targets returned by {@link #getRunningTargets()}. - *

- * This method only validates and submits the request. The actual reload result is delivered - * asynchronously through {@code callback}. If the framework cannot provide a valid new module - * generation for the target, the callback receives {@link HotReloadResult.Status#UNSUPPORTED}. - *

- *

- * If the old module rejects reload by returning {@code false} from {@code onHotReloading}, - * the callback receives {@link HotReloadResult.Status#FAILED} with a null message. If reload - * fails because of an exception, the callback receives {@link HotReloadResult.Status#FAILED} - * with a framework-provided diagnostic message. - *

- *

- * Hot reload is intended for loading a new module generation after the module app is updated. - * It should not be used to propagate configuration changes. For configuration updates, use - * {@link #getRemotePreferences(String)} and - * {@link SharedPreferences.OnSharedPreferenceChangeListener}. - *

- *

- * The optional data should contain only classloader-neutral values that can be unmarshalled - * without the module's class loader. Do not put module-defined - * {@link android.os.Parcelable} or {@link java.io.Serializable} objects in this bundle. - *

- * - * @param target The target process - * @param data Optional data to be passed to the old module - * @param callback Callback to be invoked when the request completes or fails - * @throws ServiceException If the service is dead or an error occurred - * @throws SecurityException If the target is invalid or no longer belongs to this module - */ - @SinceApi(API_102) - public void hotReloadModule(@NonNull HookedTarget target, @Nullable Bundle data, - @NonNull HotReloadCallback callback) { - requireApi(API_102); - Objects.requireNonNull(target); - Objects.requireNonNull(callback); - var remoteCallback = new IHotReloadCallback.Stub() { - @Override - public void onHotReloadResult(int code, String message) { - try { - callback.onHotReloadResult(target, HotReloadResult.from(code, message)); - } finally { - hotReloadCallbacks.remove(this); - } - } - }; - hotReloadCallbacks.add(remoteCallback); - try { - mService.hotReloadModule(target.mTargetId, data, remoteCallback); - } catch (RemoteException e) { - hotReloadCallbacks.remove(remoteCallback); - throw new ServiceException(e); - } catch (RuntimeException e) { - hotReloadCallbacks.remove(remoteCallback); - throw e; - } - } - - private static HookedTarget toHookedTarget(HookedProcess process) { - if (process.processName == null) { - throw new ServiceException("Framework returns target with null processName"); - } - return new HookedTarget( - process.targetId, - process.uid, - process.pid, - process.processName, - toHookedTargetState(process.state), - process.loadedVersionCode - ); - } - - private static HookedTarget.State toHookedTargetState(int state) { - return switch (state) { - case HookedProcess.TARGET_STATE_UP_TO_DATE -> HookedTarget.State.UP_TO_DATE; - case HookedProcess.TARGET_STATE_STALE -> HookedTarget.State.STALE; - case HookedProcess.TARGET_STATE_RELOADING -> HookedTarget.State.RELOADING; - case HookedProcess.TARGET_STATE_FAILED -> HookedTarget.State.FAILED; - default -> throw new ServiceException("Invalid hooked target state: " + state); - }; - } - - /** - * Get remote preferences from Xposed framework. If the group does not exist, it will be created. - * - * @param group Group name - * @return The preferences - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework does not have remote capability - */ - @NonNull - public synchronized SharedPreferences getRemotePreferences(@NonNull String group) { - return mRemotePrefs.computeIfAbsent(group, k -> { - try { - return RemotePreferences.newInstance(this, k); - } catch (RemoteException e) { - throw new ServiceException(e); - } - }); - } - - /** - * Delete a group of remote preferences. - * - * @param group Group name - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework does not have remote capability - */ - public synchronized void deleteRemotePreferences(@NonNull String group) { - try { - var prefs = mRemotePrefs.get(group); - if (prefs != null) prefs.onDelete(); - mService.deleteRemotePreferences(group); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * List all files in the module's shared data directory. - * - * @return The file list - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework does not have remote capability - */ - @NonNull - public String[] listRemoteFiles() { - try { - var files = mService.listRemoteFiles(); - if (files == null) throw new ServiceException("Framework returns null"); - return files; - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Open a file in the module's shared data directory. The file will be created if not exists. - * - * @param name File name, must not contain path separators and . or .. - * @return The file descriptor - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework does not have remote capability - */ - @NonNull - public ParcelFileDescriptor openRemoteFile(@NonNull String name) { - try { - var file = mService.openRemoteFile(name); - if (file == null) throw new ServiceException("Framework returns null"); - return file; - } catch (RemoteException e) { - throw new ServiceException(e); - } - } - - /** - * Delete a file in the module's shared data directory. - * - * @param name File name, must not contain path separators and . or .. - * @return true if successful, false if the file does not exist - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework does not have remote capability - */ - public boolean deleteRemoteFile(@NonNull String name) { - try { - return mService.deleteRemoteFile(name); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } -} diff --git a/service/src/main/java/io/github/libxposed/service/XposedServiceHelper.java b/service/src/main/java/io/github/libxposed/service/XposedServiceHelper.java deleted file mode 100644 index 401f418..0000000 --- a/service/src/main/java/io/github/libxposed/service/XposedServiceHelper.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.libxposed.service; - -import android.os.IBinder; -import android.util.Log; - -import androidx.annotation.NonNull; - -import java.util.HashSet; -import java.util.Set; - -@SuppressWarnings("unused") -public final class XposedServiceHelper { - - /** - * Callback interface for Xposed service. - */ - public interface OnServiceListener { - /** - * Callback when the service is connected.
- * This method could be called multiple times if multiple Xposed frameworks exist. - * - * @param service Service instance - */ - void onServiceBind(@NonNull XposedService service); - - /** - * Callback when the service is dead. - */ - void onServiceDied(@NonNull XposedService service); - } - - private static final String TAG = "XposedServiceHelper"; - private static final Set mCache = new HashSet<>(); - private static OnServiceListener mListener = null; - - static void onBinderReceived(IBinder binder) { - if (binder == null) return; - synchronized (mCache) { - try { - var service = new XposedService(IXposedService.Stub.asInterface(binder)); - if (mListener == null) { - mCache.add(service); - } else { - binder.linkToDeath(() -> mListener.onServiceDied(service), 0); - mListener.onServiceBind(service); - } - } catch (Throwable t) { - Log.e(TAG, "onBinderReceived", t); - } - } - } - - /** - * Register a ServiceListener to receive service binders from Xposed frameworks.
- * This method should only be called once. - * - * @param listener Listener to register - */ - public static void registerListener(OnServiceListener listener) { - synchronized (mCache) { - mListener = listener; - if (!mCache.isEmpty()) { - for (var it = mCache.iterator(); it.hasNext(); ) { - try { - var service = it.next(); - service.asInterface().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0); - mListener.onServiceBind(service); - } catch (Throwable t) { - Log.e(TAG, "registerListener", t); - it.remove(); - } - } - mCache.clear(); - } - } - } -} diff --git a/service/src/main/java/io/github/xposed/xposedservice/XposedService.java b/service/src/main/java/io/github/xposed/xposedservice/XposedService.java new file mode 100644 index 0000000..d36bbb7 --- /dev/null +++ b/service/src/main/java/io/github/xposed/xposedservice/XposedService.java @@ -0,0 +1,30 @@ +package io.github.xposed.xposedservice; + +import android.os.IBinder; +import android.os.RemoteException; + +public class XposedService implements IXposedService { + private static IBinder serviceBinder = null; + private static IXposedService service = null; + private static XposedService instance = null; + + public static XposedService getService() { + if (serviceBinder != null && service == null) { + service = IXposedService.Stub.asInterface(serviceBinder); + } + if (instance == null) { + instance = new XposedService(); + } + return service == null ? null : instance; + } + + @Override + public int getVersion() throws RemoteException { + return service.getVersion(); + } + + @Override + public IBinder asBinder() { + return serviceBinder; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 951c8d2..fdf16ba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,30 +4,17 @@ pluginManagement { google() mavenCentral() } + plugins { + id("com.android.library").version("7.0.0") + } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - mavenLocal { - content { - includeGroup("io.github.libxposed") - } - } - maven { - url = uri("https://central.sonatype.com/repository/maven-snapshots/") - mavenContent { - snapshotsOnly() - } - content { - includeGroup("io.github.libxposed") - } - } google() mavenCentral() } } - -rootProject.name = "libxposed-service" - -include(":interface", ":service") +include(":service", ":interface") +rootProject.name = "XposedService"