commit
72bcc09bd6
62 changed files with 3575 additions and 0 deletions
-
24build.gradle
-
23gradle.properties
-
185gradlew
-
89gradlew.bat
-
2settings.gradle
-
1wechatvideo/.gitignore
-
46wechatvideo/build.gradle
-
21wechatvideo/proguard-rules.pro
-
26wechatvideo/src/androidTest/java/com/example/wechatvideo/ExampleInstrumentedTest.java
-
44wechatvideo/src/main/AndroidManifest.xml
-
1wechatvideo/src/main/assets/xposed_init
-
386wechatvideo/src/main/java/com/example/wechatvideo/HookVideoData825Entity.java
-
225wechatvideo/src/main/java/com/example/wechatvideo/HookVideoDataEntity.java
-
102wechatvideo/src/main/java/com/example/wechatvideo/MainActivity.java
-
864wechatvideo/src/main/java/com/example/wechatvideo/content/MultiprocessSharedPreferences.java
-
166wechatvideo/src/main/java/com/example/wechatvideo/handler/CommentListHandler.java
-
103wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUser825Handler.java
-
104wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUserHandler.java
-
125wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideo825Handler.java
-
129wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideoHandler.java
-
77wechatvideo/src/main/java/com/example/wechatvideo/model/StoredObject.java
-
146wechatvideo/src/main/java/com/example/wechatvideo/utils/SearchClassUtils.java
-
179wechatvideo/src/main/java/com/example/wechatvideo/utils/SpUtil.java
-
30wechatvideo/src/main/res/drawable-v24/ic_launcher_foreground.xml
-
170wechatvideo/src/main/res/drawable/ic_launcher_background.xml
-
30wechatvideo/src/main/res/drawable/ic_launcher_foreground.xml
-
74wechatvideo/src/main/res/drawable/new_icon_background.xml
-
118wechatvideo/src/main/res/layout/activity_main.xml
-
6wechatvideo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
-
6wechatvideo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
-
5wechatvideo/src/main/res/mipmap-anydpi-v26/new_icon.xml
-
5wechatvideo/src/main/res/mipmap-anydpi-v26/new_icon_round.xml
-
BINwechatvideo/src/main/res/mipmap-hdpi/ic_launcher.webp
-
BINwechatvideo/src/main/res/mipmap-hdpi/ic_launcher_round.webp
-
BINwechatvideo/src/main/res/mipmap-hdpi/new_icon.webp
-
BINwechatvideo/src/main/res/mipmap-hdpi/new_icon_foreground.webp
-
BINwechatvideo/src/main/res/mipmap-hdpi/new_icon_round.webp
-
BINwechatvideo/src/main/res/mipmap-mdpi/ic_launcher.webp
-
BINwechatvideo/src/main/res/mipmap-mdpi/ic_launcher_round.webp
-
BINwechatvideo/src/main/res/mipmap-mdpi/new_icon.webp
-
BINwechatvideo/src/main/res/mipmap-mdpi/new_icon_foreground.webp
-
BINwechatvideo/src/main/res/mipmap-mdpi/new_icon_round.webp
-
BINwechatvideo/src/main/res/mipmap-xhdpi/ic_launcher.webp
-
BINwechatvideo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
-
BINwechatvideo/src/main/res/mipmap-xhdpi/new_icon.webp
-
BINwechatvideo/src/main/res/mipmap-xhdpi/new_icon_foreground.webp
-
BINwechatvideo/src/main/res/mipmap-xhdpi/new_icon_round.webp
-
BINwechatvideo/src/main/res/mipmap-xxhdpi/ic_launcher.webp
-
BINwechatvideo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
-
BINwechatvideo/src/main/res/mipmap-xxhdpi/new_icon.webp
-
BINwechatvideo/src/main/res/mipmap-xxhdpi/new_icon_foreground.webp
-
BINwechatvideo/src/main/res/mipmap-xxhdpi/new_icon_round.webp
-
BINwechatvideo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
-
BINwechatvideo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
-
BINwechatvideo/src/main/res/mipmap-xxxhdpi/new_icon.webp
-
BINwechatvideo/src/main/res/mipmap-xxxhdpi/new_icon_foreground.webp
-
BINwechatvideo/src/main/res/mipmap-xxxhdpi/new_icon_round.webp
-
16wechatvideo/src/main/res/values-night/themes.xml
-
10wechatvideo/src/main/res/values/colors.xml
-
4wechatvideo/src/main/res/values/strings.xml
-
16wechatvideo/src/main/res/values/themes.xml
-
17wechatvideo/src/test/java/com/example/wechatvideo/ExampleUnitTest.java
@ -0,0 +1,24 @@ |
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules. |
|||
buildscript { |
|||
repositories { |
|||
google() |
|||
jcenter() |
|||
} |
|||
dependencies { |
|||
classpath "com.android.tools.build:gradle:4.1.2" |
|||
|
|||
// NOTE: Do not place your application dependencies here; they belong |
|||
// in the individual module build.gradle files |
|||
} |
|||
} |
|||
|
|||
allprojects { |
|||
repositories { |
|||
google() |
|||
jcenter() |
|||
} |
|||
} |
|||
|
|||
task clean(type: Delete) { |
|||
delete rootProject.buildDir |
|||
} |
@ -0,0 +1,23 @@ |
|||
# 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 |
|||
# Kotlin code style for this project: "official" or "obsolete": |
|||
kotlin.code.style=official |
|||
# Enables namespacing of each library's R class so that its R class includes only the |
|||
# resources declared in the library itself and none from the library's dependencies, |
|||
# thereby reducing the size of the R class for that library |
|||
android.nonTransitiveRClass=true |
@ -0,0 +1,185 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# |
|||
# 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. |
|||
# You may obtain a copy of the License at |
|||
# |
|||
# https://www.apache.org/licenses/LICENSE-2.0 |
|||
# |
|||
# Unless required by applicable law or agreed to in writing, software |
|||
# distributed under the License is distributed on an "AS IS" BASIS, |
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
# See the License for the specific language governing permissions and |
|||
# limitations under the License. |
|||
# |
|||
|
|||
############################################################################## |
|||
## |
|||
## Gradle start up script for UN*X |
|||
## |
|||
############################################################################## |
|||
|
|||
# Attempt to set APP_HOME |
|||
# Resolve links: $0 may be a link |
|||
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 |
|||
|
|||
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" |
|||
|
|||
warn () { |
|||
echo "$*" |
|||
} |
|||
|
|||
die () { |
|||
echo |
|||
echo "$*" |
|||
echo |
|||
exit 1 |
|||
} |
|||
|
|||
# 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 |
|||
;; |
|||
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" |
|||
else |
|||
JAVACMD="$JAVA_HOME/bin/java" |
|||
fi |
|||
if [ ! -x "$JAVACMD" ] ; then |
|||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
|||
|
|||
Please set the JAVA_HOME variable in your environment to match the |
|||
location of your Java installation." |
|||
fi |
|||
else |
|||
JAVACMD="java" |
|||
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 |
|||
|
|||
# Increase the maximum file descriptors if we can. |
|||
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 |
|||
|
|||
# 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" = "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 |
|||
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 |
|||
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 "$@"` |
|||
|
|||
# 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" "$@" |
@ -0,0 +1,89 @@ |
|||
@rem |
|||
@rem Copyright 2015 the original author or authors. |
|||
@rem |
|||
@rem Licensed under the Apache License, Version 2.0 (the "License"); |
|||
@rem you may not use this file except in compliance with the License. |
|||
@rem You may obtain a copy of the License at |
|||
@rem |
|||
@rem https://www.apache.org/licenses/LICENSE-2.0 |
|||
@rem |
|||
@rem Unless required by applicable law or agreed to in writing, software |
|||
@rem distributed under the License is distributed on an "AS IS" BASIS, |
|||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
@rem See the License for the specific language governing permissions and |
|||
@rem limitations under the License. |
|||
@rem |
|||
|
|||
@if "%DEBUG%" == "" @echo off |
|||
@rem ########################################################################## |
|||
@rem |
|||
@rem Gradle startup script for Windows |
|||
@rem |
|||
@rem ########################################################################## |
|||
|
|||
@rem Set local scope for the variables with windows NT shell |
|||
if "%OS%"=="Windows_NT" setlocal |
|||
|
|||
set DIRNAME=%~dp0 |
|||
if "%DIRNAME%" == "" set DIRNAME=. |
|||
set APP_BASE_NAME=%~n0 |
|||
set APP_HOME=%DIRNAME% |
|||
|
|||
@rem Resolve any "." and ".." in APP_HOME to make it shorter. |
|||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi |
|||
|
|||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
|||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" |
|||
|
|||
@rem Find java.exe |
|||
if defined JAVA_HOME goto findJavaFromJavaHome |
|||
|
|||
set JAVA_EXE=java.exe |
|||
%JAVA_EXE% -version >NUL 2>&1 |
|||
if "%ERRORLEVEL%" == "0" goto execute |
|||
|
|||
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. |
|||
|
|||
goto fail |
|||
|
|||
:findJavaFromJavaHome |
|||
set JAVA_HOME=%JAVA_HOME:"=% |
|||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
|||
|
|||
if exist "%JAVA_EXE%" goto execute |
|||
|
|||
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. |
|||
|
|||
goto fail |
|||
|
|||
:execute |
|||
@rem Setup the command line |
|||
|
|||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
|||
|
|||
|
|||
@rem Execute Gradle |
|||
"%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 |
|||
|
|||
:omega |
@ -0,0 +1,2 @@ |
|||
rootProject.name = "xp" |
|||
include ':wechatvideo' |
@ -0,0 +1 @@ |
|||
/build |
@ -0,0 +1,46 @@ |
|||
plugins { |
|||
id 'com.android.application' |
|||
} |
|||
|
|||
android { |
|||
compileSdk 31 |
|||
|
|||
defaultConfig { |
|||
applicationId "com.example.wechatvideo" |
|||
minSdk 24 |
|||
targetSdk 31 |
|||
versionCode 1 |
|||
versionName "1.0" |
|||
|
|||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
|||
} |
|||
|
|||
buildTypes { |
|||
release { |
|||
minifyEnabled false |
|||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
|||
} |
|||
} |
|||
compileOptions { |
|||
sourceCompatibility JavaVersion.VERSION_1_8 |
|||
targetCompatibility JavaVersion.VERSION_1_8 |
|||
} |
|||
|
|||
repositories { |
|||
maven { url "https://nexus.iinti.cn/repository/maven-public/" } |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
|
|||
implementation "com.alibaba:fastjson:1.2.78" // 引入原始的fastjson 虽然说sekiro也有 |
|||
implementation 'com.google.guava:guava:30.1.1-jre' |
|||
implementation('cn.iinti.sekiro3.business:sekiro-business-api:1.1') |
|||
compileOnly 'de.robv.android.xposed:api:82' |
|||
compileOnly 'de.robv.android.xposed:api:82:sources' |
|||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02' |
|||
implementation 'com.google.android.material:material:1.1.0' |
|||
testImplementation 'junit:junit:4.13.2' |
|||
androidTestImplementation 'androidx.test.ext:junit:1.1.0' |
|||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' |
|||
} |
@ -0,0 +1,21 @@ |
|||
# Add project specific ProGuard rules here. |
|||
# You can control the set of applied configuration files using the |
|||
# proguardFiles setting in build.gradle. |
|||
# |
|||
# For more details, see |
|||
# http://developer.android.com/guide/developing/tools/proguard.html |
|||
|
|||
# If your project uses WebView with JS, uncomment the following |
|||
# and specify the fully qualified class name to the JavaScript interface |
|||
# class: |
|||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
|||
# public *; |
|||
#} |
|||
|
|||
# Uncomment this to preserve the line number information for |
|||
# debugging stack traces. |
|||
#-keepattributes SourceFile,LineNumberTable |
|||
|
|||
# If you keep the line number information, uncomment this to |
|||
# hide the original source file name. |
|||
#-renamesourcefileattribute SourceFile |
@ -0,0 +1,26 @@ |
|||
package com.example.wechatvideo; |
|||
|
|||
import android.content.Context; |
|||
|
|||
import androidx.test.platform.app.InstrumentationRegistry; |
|||
import androidx.test.ext.junit.runners.AndroidJUnit4; |
|||
|
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
|
|||
import static org.junit.Assert.*; |
|||
|
|||
/** |
|||
* Instrumented test, which will execute on an Android device. |
|||
* |
|||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
|||
*/ |
|||
@RunWith(AndroidJUnit4.class) |
|||
public class ExampleInstrumentedTest { |
|||
@Test |
|||
public void useAppContext() { |
|||
// Context of the app under test. |
|||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); |
|||
assertEquals("com.example.wechatvideo", appContext.getPackageName()); |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.wechatvideo"> |
|||
|
|||
<application |
|||
android:allowBackup="true" |
|||
android:icon="@drawable/new_icon_background" |
|||
android:label="@string/app_name" |
|||
android:roundIcon="@mipmap/new_icon" |
|||
android:supportsRtl="true" |
|||
android:theme="@style/Theme.Xp_module" > |
|||
|
|||
<meta-data android:name="xposedmodule" android:value="true"/> |
|||
<meta-data android:name="xposeddescription" android:value="@string/xposed_description" /> |
|||
<meta-data android:name="xposedminversion" android:value="53" /> |
|||
|
|||
<!-- |
|||
name:MultiprocessSharedPreferences文件路径 |
|||
authorities:provider标志,provider数据存储路径,一般以包名开头+.provider |
|||
exported:必须为true,否则不能跨进程通信--> |
|||
<provider |
|||
android:name="com.example.wechatvideo.content.MultiprocessSharedPreferences" |
|||
android:authorities="com.example.wechatvideo.provider" |
|||
android:exported="true" |
|||
android:enabled="true" |
|||
/> |
|||
|
|||
|
|||
<activity |
|||
android:name=".MainActivity" |
|||
android:exported="true"> |
|||
<intent-filter> |
|||
<action android:name="android.intent.action.MAIN" /> |
|||
|
|||
<category android:name="android.intent.category.LAUNCHER" /> |
|||
</intent-filter> |
|||
</activity> |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
</application> |
|||
|
|||
</manifest> |
@ -0,0 +1 @@ |
|||
com.example.wechatvideo.HookVideoData825Entity |
@ -0,0 +1,386 @@ |
|||
package com.example.wechatvideo; |
|||
|
|||
import static com.example.wechatvideo.model.StoredObject.map1; |
|||
|
|||
import android.app.Application; |
|||
import android.content.Context; |
|||
import android.os.Build; |
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.handler.CommentListHandler; |
|||
import com.example.wechatvideo.handler.SearchUser825Handler; |
|||
import com.example.wechatvideo.handler.UserVideo825Handler; |
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SpUtil; |
|||
|
|||
import java.util.Base64; |
|||
import java.util.HashMap; |
|||
import java.util.LinkedList; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
|
|||
import cn.iinti.sekiro3.business.api.SekiroClient; |
|||
import cn.iinti.sekiro3.business.api.interfaze.HandlerRegistry; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequestInitializer; |
|||
import de.robv.android.xposed.IXposedHookLoadPackage; |
|||
import de.robv.android.xposed.XC_MethodHook; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
import de.robv.android.xposed.callbacks.XC_LoadPackage; |
|||
|
|||
|
|||
/** |
|||
* 8.0.25 微信视频号: |
|||
* 支持搜索、用户、评论 |
|||
*/ |
|||
public class HookVideoData825Entity implements IXposedHookLoadPackage { |
|||
public static final String packageName = "com.tencent.mm"; |
|||
public static final String TAG = "8.0.2 vxin -> "; |
|||
public static Context CONTEXT; |
|||
|
|||
public static ClassLoader classLoader; |
|||
|
|||
public static Object FEED_PROFILE; // 枚举类型获得 *视频主页 |
|||
|
|||
public static Object FinderMixSearchPresenter; // 搜索目标对象 一般只有一个 *视频搜索 |
|||
|
|||
public static Object i; // 视频载体model 很多个选其中之一即可 *视频评论 |
|||
|
|||
@Override |
|||
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { |
|||
if (packageName.equals(lpparam.packageName)){ |
|||
Log.d(TAG, "handleLoadPackage: ?" + lpparam.packageName); |
|||
InitialObject(lpparam); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 对于翻页和二级评论对于参数的修改 |
|||
* @param awClazz |
|||
* @param bClazz |
|||
* @param param |
|||
* @param map |
|||
* @return |
|||
*/ |
|||
public void setLevelComments(Class<?> awClazz, Class<?> bClazz, XC_MethodHook.MethodHookParam param, Map<String, Object> map){ |
|||
try { |
|||
Map<String, Object> paramsMap = (Map<String, Object>) map.get("params"); |
|||
long commentId = (long) paramsMap.get("commentId"); // 获得评论id |
|||
long displayId = (long) paramsMap.get("displayId"); |
|||
String lastBuffer = (String) paramsMap.get("lastBuffer"); // 获得翻页内容 |
|||
|
|||
if(lastBuffer != null){ // 有lastbuffer 就代表翻页 |
|||
param.args[10] = true; // 修改true 这个影响二级评论翻页 |
|||
Log.d(TAG, "setLevelComments: 需要处理参数 翻页"); |
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
|||
byte[] _lastBuffer = Base64.getDecoder().decode(lastBuffer); |
|||
param.args[2] = 6; |
|||
param.args[3] = 2; |
|||
param.args[6] = XposedHelpers.newInstance(bClazz, _lastBuffer); // 翻页需要构造一个对象用于翻页 |
|||
param.args[14] = 1; |
|||
param.args[16] = 40864; // 翻页需要修改的值 |
|||
} |
|||
if(commentId != 0 && displayId != 0){ // 是否调用二级评论 |
|||
Log.d(TAG, "setLevelComments: 需要处理参数 获取二级评论"); |
|||
Object aw = XposedHelpers.newInstance(awClazz); // 构造对象 给对象内部赋值 |
|||
Object ajeB = XposedHelpers.getObjectField(XposedHelpers.getObjectField(aw, "field_actionInfo"), "ajeB"); // 获得一个commentinfo对象 |
|||
XposedHelpers.callMethod(ajeB, "setCommentId", commentId); // 赋值commentId 当前二级评论的id |
|||
XposedHelpers.callMethod(ajeB, "setDisplayid", displayId); // 赋值displayId 当前二级评论id |
|||
param.args[7] = aw; |
|||
param.args[16] = 39712; // 二级评论需要 |
|||
} |
|||
Log.d(TAG, "setLevelComments: 处理参数完毕"); |
|||
} |
|||
|
|||
}catch (Exception e){ |
|||
e.printStackTrace(); |
|||
Log.d(TAG, "setLevelComments: 处理参数过程中 异常"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 过滤一些评论需要的数据 |
|||
* 拼装需要的数据 |
|||
* @param object |
|||
* @param fcr |
|||
* @param map |
|||
*/ |
|||
public void setCommentLists(Object object, Object fcr, Map<String, Object> map){ |
|||
Object feedObject = XposedHelpers.getObjectField(fcr, "feedObject"); // 对象 |
|||
LinkedList<?> linkedList = (LinkedList<?>) XposedHelpers.getObjectField(fcr, "ajlo"); // 评论列表 |
|||
Object b = XposedHelpers.getObjectField(fcr, "lastBuffer"); // b 对象控制翻页内容 二级评论这个也会刷新 第一次是 目标评论的lastbuffer |
|||
// LinkedList<?> linkedList1 = (LinkedList<?>) XposedHelpers.callMethod(object, "ah", linkedList); // 晒一下评论信息 |
|||
int commentCount = XposedHelpers.getIntField(fcr, "commentCount"); // 评论数量 |
|||
int continueFlag = XposedHelpers.getIntField(fcr, "ajlp"); // 是有有数据 |
|||
Map<String, Object> cache = new HashMap<>(); |
|||
cache.put("cursor", b); |
|||
cache.put("commentTotalCount", commentCount); |
|||
cache.put("continueFlag", continueFlag); |
|||
cache.put("commentList", linkedList); |
|||
cache.put("videoDetail", feedObject); |
|||
map.put("data", cache); |
|||
} |
|||
|
|||
/** |
|||
* 拦截处理评论信息 |
|||
*/ |
|||
public void hookComments(XC_LoadPackage.LoadPackageParam loadPackageParam){ |
|||
// 评论调用类 |
|||
Class<?> commentClazz = XposedHelpers.findClass("com.tencent.mm.plugin.finder.cgi.bj", loadPackageParam.classLoader); |
|||
// 翻页对象 不翻为null(二级评论也用得上 |
|||
Class<?> b = XposedHelpers.findClass("com.tencent.mm.cc.b", loadPackageParam.classLoader); |
|||
// 二级评论对象 需要构造 |
|||
Class<?> aw = XposedHelpers.findClass("com.tencent.mm.plugin.finder.storage.aw", loadPackageParam.classLoader); |
|||
// ? |
|||
Class<?> ccz = XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.ccz", loadPackageParam.classLoader); |
|||
// 为null 没啥用处 |
|||
Class<?> brj = XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.brj", loadPackageParam.classLoader); |
|||
// null 没啥用 |
|||
Class<?> k = XposedHelpers.findClass("kotlin.g.b.k", loadPackageParam.classLoader); |
|||
|
|||
// *** hook 构造函数 |
|||
XposedHelpers.findAndHookConstructor(commentClazz, long.class, String.class, int.class, int.class, String.class, |
|||
boolean.class, b, aw, long.class, String.class, |
|||
boolean.class, boolean.class, String.class, ccz, int.class, |
|||
brj, int.class, k, new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "beforeHookedMethod: args 0 => " + param.args[0]); |
|||
Log.d(TAG, "beforeHookedMethod: args 1 => " + param.args[1]); |
|||
Log.d(TAG, "beforeHookedMethod: args 2 => " + param.args[2]); |
|||
Log.d(TAG, "beforeHookedMethod: args 3 => " + param.args[3]); |
|||
if(uuid !=null && uuid.contains(CommentListHandler.api)){ // 确定为评论的map |
|||
Map<String, Object> map = map1.get(uuid); |
|||
setLevelComments(aw, b, param, map); // 修改参数 |
|||
} |
|||
|
|||
Log.d(TAG, "beforeHookedMethod: 触发到构造函数"); |
|||
super.beforeHookedMethod(param); |
|||
} |
|||
} |
|||
); |
|||
|
|||
// *** hook 回调函数 |
|||
XposedHelpers.findAndHookMethod(commentClazz, "b", int.class, int.class, String.class, |
|||
XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.fcr", loadPackageParam.classLoader), |
|||
XposedHelpers.findClass("com.tencent.mm.av.p", loadPackageParam.classLoader), |
|||
new XC_MethodHook() { |
|||
@Override |
|||
protected void afterHookedMethod(MethodHookParam param) throws Throwable { |
|||
super.afterHookedMethod(param); |
|||
Object object = param.thisObject; |
|||
Object response = param.args[3]; // 最终的结果 但需要处理一下 |
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "触发到回调函数 afterHookedMethod: uuidk is " + uuid); |
|||
if (uuid != null && uuid.contains(CommentListHandler.api)){ // 确定为评论的map |
|||
Map<String, Object> map = map1.get(uuid); |
|||
setCommentLists(object, response, map); |
|||
synchronized (uuid){ |
|||
uuid.notify(); // 通知消息 |
|||
Log.d(TAG, "beforeHookedMethod: 封装对象成功"); |
|||
} |
|||
} |
|||
|
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 根据类型获得视频种类 |
|||
* @param cdx |
|||
* @param map |
|||
* @return |
|||
*/ |
|||
public void setVideoList(Object cdx, Map<String, Object> map){ |
|||
String type = (String) map.get("search_order"); |
|||
Log.d(TAG, "setVideoList: 请求获取目标为 => " + type); |
|||
LinkedList<?> videoList = (LinkedList<?>) XposedHelpers.getObjectField(cdx, "Ebz"); // 视频 |
|||
LinkedList<?> userList = (LinkedList<?>) XposedHelpers.getObjectField(cdx, "RVP"); // 用户 |
|||
LinkedList<?> ajKA = (LinkedList<?>) XposedHelpers.getObjectField(cdx, "ajKA"); // 第一次这个会存放两个列表 后续才会到ebz中 |
|||
map.put("user", userList); // 获取用户列表 |
|||
|
|||
if(ajKA.size() > 0){ |
|||
for(Object ajKA_object:ajKA){ |
|||
String ajKg = (String) XposedHelpers.getObjectField(ajKA_object, "ajKg"); // 这个字段为 综合 确定为什么搜索类型 |
|||
Log.d(TAG, "getVideoList: 获取当前搜索类型为 => " + ajKg); |
|||
if(type.equals(ajKg)) { |
|||
LinkedList<?> aikW = (LinkedList<?>) XposedHelpers.getObjectField(ajKA_object, "aikW"); // 视频列表 |
|||
map.put("data", aikW); |
|||
} |
|||
} |
|||
}else{ // 一般是第二页这个才会有值 |
|||
map.put("data", videoList); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @param lpparam |
|||
*/ |
|||
public void hookSearch(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.finder.cgi.du", lpparam.classLoader, "d", |
|||
int.class, int.class, int.class, String.class, XposedHelpers.findClass("com.tencent.mm.network.t", lpparam.classLoader), byte[].class |
|||
, new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
super.beforeHookedMethod(param); |
|||
Log.d(TAG, "beforeHookedMethod: 进入搜索信息 数据 ***********************"); |
|||
Log.d(TAG, "beforeHookedMethod: 获得参数1" + param.args[0]); |
|||
Log.d(TAG, "beforeHookedMethod: 获得参数2" + param.args[1]); |
|||
Log.d(TAG, "beforeHookedMethod: 获得参数3" + param.args[2]); |
|||
Log.d(TAG, "beforeHookedMethod: 获得参数4" + param.args[3]); |
|||
Log.d(TAG, "beforeHookedMethod: 获取目标对象 长度 *************************"); |
|||
Object O = param.thisObject; |
|||
Object cdx = XposedHelpers.getObjectField( |
|||
XposedHelpers.getObjectField( |
|||
XposedHelpers.getObjectField(O, "rfO"), "qVP") |
|||
, "qVU"); // response 对象 |
|||
LinkedList videoList = (LinkedList) XposedHelpers.getObjectField(cdx, "Ebz"); // 视频 |
|||
LinkedList userList = (LinkedList) XposedHelpers.getObjectField(cdx, "RVP"); // 用户 |
|||
LinkedList ajKA = (LinkedList) XposedHelpers.getObjectField(cdx, "ajKA"); // 第一次这个会存放两个列表 后续才会到ebz中 |
|||
int offset = XposedHelpers.getIntField(cdx, "offset"); |
|||
Log.d(TAG, "beforeHookedMethod: offset is " + offset); |
|||
Log.d(TAG, "beforeHookedMethod: video size " + videoList.size()); |
|||
Log.d(TAG, "beforeHookedMethod: user size " + userList.size()); |
|||
Log.d(TAG, "beforeHookedMethod: ajKA size " + ajKA.size()); |
|||
|
|||
Log.d(TAG, "beforeHookedMethod: 测试数据*******************************"); |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "afterHookedMethod: uuidk is " + uuid); |
|||
if (uuid != null){ |
|||
Map<String, Object> map = map1.get(uuid); |
|||
setVideoList(cdx, map); |
|||
synchronized (uuid){ |
|||
uuid.notify(); // 通知消息 |
|||
Log.d(TAG, "beforeHookedMethod: 封装对象成功"); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* hook 用户主页 |
|||
* @param lpparam |
|||
*/ |
|||
public void hookUser(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
// 拦截数据 |
|||
XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.finder.cgi.ee", lpparam.classLoader, "d", |
|||
int.class, int.class, int.class, String.class, XposedHelpers.findClass("com.tencent.mm.network.t", lpparam.classLoader), |
|||
byte[].class, new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
super.beforeHookedMethod(param); |
|||
Object o = param.thisObject; |
|||
Object finderUserPageResponse = XposedHelpers.getObjectField(XposedHelpers.getObjectField(XposedHelpers.getObjectField(o, "rr"), "qVP"), "qVU"); |
|||
LinkedList<?> f = (LinkedList<?>) XposedHelpers.getObjectField(finderUserPageResponse, "object"); |
|||
Log.d(TAG, "beforeHookedMethod: LinkedList size is " + f.size()); |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "afterHookedMethod: uuidk is " + uuid); |
|||
if (uuid != null && f.size() > 0){ |
|||
Map<String, Object> map = map1.get(uuid); |
|||
map.put("data", f); // 封装消息 |
|||
synchronized (uuid){ |
|||
uuid.notify(); // 通知消息 |
|||
Log.d(TAG, "beforeHookedMethod: 封装对象成功"); |
|||
} |
|||
} |
|||
|
|||
} |
|||
}); |
|||
|
|||
|
|||
// 翻页修改目标参数 |
|||
XposedHelpers.findAndHookConstructor("com.tencent.mm.plugin.finder.cgi.ee", lpparam.classLoader, |
|||
String.class, long.class, XposedHelpers.findClass("com.tencent.mm.cc.b", lpparam.classLoader), |
|||
int.class, XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.ccz", lpparam.classLoader), |
|||
int.class, long.class, boolean.class, String.class, long.class, Integer.class, Long.class, String.class, boolean.class, |
|||
new XC_MethodHook() { |
|||
@Override |
|||
protected void afterHookedMethod(MethodHookParam param) throws Throwable { |
|||
super.afterHookedMethod(param); |
|||
String userId = (String) param.args[0]; |
|||
long maxId = (long) param.args[1]; |
|||
int a = (int) param.args[3]; // 翻页为2 不翻为1 |
|||
long b = (long) param.args[9]; // -1 翻页变为0 |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "构造函数中拦截: userId is => " + userId); |
|||
if (uuid != null){ |
|||
param.args[1] = Long.valueOf(uuid); |
|||
if(!Objects.equals(uuid, "0")){ |
|||
param.args[3] = 2; |
|||
param.args[9] = 0; |
|||
Log.d(TAG, "afterHookedMethod: 修改翻页内容 "); |
|||
} |
|||
Log.d(TAG, "构造函数中拦截: 目标偏移量修改完毕 => " + uuid); |
|||
} |
|||
|
|||
} |
|||
}); |
|||
} |
|||
|
|||
|
|||
public void InitialObject(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
Log.d(TAG, "InitialObject: 进入初始化"); |
|||
XposedHelpers.findAndHookMethod(Application.class, "attach", |
|||
Context.class, new XC_MethodHook() { |
|||
@Override |
|||
protected void afterHookedMethod(MethodHookParam param) throws Throwable { |
|||
super.afterHookedMethod(param); |
|||
CONTEXT = (Context) param.args[0]; |
|||
classLoader = lpparam.classLoader; |
|||
Log.d(TAG, "afterHookedMethod: 进入hook 某组"); |
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {// TODO 这里需要兼容 |
|||
String paramName = Application.getProcessName(); // 获取进程名程 只对主进程进行hook |
|||
Log.d(TAG, "afterHookedMethod: " + paramName); |
|||
if ("com.tencent.mm".equals(paramName)) { // 只hook主进程 |
|||
|
|||
initSekiroClient(); // 服务注入 网络波动 |
|||
Log.d(TAG, "afterHookedMethod: 注 入 服 务 成功"); |
|||
hookSearch(lpparam); |
|||
hookUser(lpparam); |
|||
hookComments(lpparam); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
|
|||
|
|||
} |
|||
|
|||
|
|||
private void initSekiroClient() { |
|||
Log.d(TAG, "initSekiroClient: 启动 sekiro ing"); |
|||
String groupName = "vxin"; |
|||
String clientId = (String) SpUtil.readObjectByProvider(CONTEXT, "vx_client", String.class); |
|||
String serverHost = (String) SpUtil.readObjectByProvider(CONTEXT, "vx_server", String.class); |
|||
int serverPort = 5612; |
|||
Log.d(TAG, String.format("initSekiroClient: 获取provier数据 clientId is %s; serverHost is %s", |
|||
clientId, serverHost)); |
|||
if ("".equals(serverHost)){ |
|||
Log.d(TAG, "initSekiroClient: provider 初始化失败"); |
|||
}else{ |
|||
Log.d(TAG, "initSekiroClient: clientId is " + clientId); |
|||
SekiroClient sekiroClient = new SekiroClient(groupName, clientId, serverHost, serverPort); |
|||
|
|||
// 注册服务 |
|||
sekiroClient.setupSekiroRequestInitializer(new SekiroRequestInitializer() { |
|||
@Override |
|||
public void onSekiroRequest(SekiroRequest sekiroRequest, HandlerRegistry handlerRegistry) { |
|||
handlerRegistry.registerSekiroHandler(new SearchUser825Handler()); // 搜索 |
|||
handlerRegistry.registerSekiroHandler(new UserVideo825Handler()); // 用户 |
|||
handlerRegistry.registerSekiroHandler(new CommentListHandler()); // 评论 |
|||
} |
|||
}).start(); |
|||
|
|||
Log.d(TAG, "initSekiroClient: 测试启动rpc服务"); |
|||
} |
|||
|
|||
} |
|||
} |
@ -0,0 +1,225 @@ |
|||
package com.example.wechatvideo; |
|||
|
|||
|
|||
import static com.example.wechatvideo.model.StoredObject.map1; |
|||
|
|||
import android.app.Application; |
|||
import android.content.Context; |
|||
import android.os.Build; |
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.handler.SearchUserHandler; |
|||
import com.example.wechatvideo.handler.UserVideoHandler; |
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SpUtil; |
|||
|
|||
import java.util.LinkedList; |
|||
import java.util.Map; |
|||
|
|||
import cn.iinti.sekiro3.business.api.SekiroClient; |
|||
import cn.iinti.sekiro3.business.api.interfaze.HandlerRegistry; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequestInitializer; |
|||
import de.robv.android.xposed.IXposedHookLoadPackage; |
|||
import de.robv.android.xposed.XC_MethodHook; |
|||
import de.robv.android.xposed.XposedBridge; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
import de.robv.android.xposed.callbacks.XC_LoadPackage; |
|||
|
|||
|
|||
/** |
|||
* 微信视频号hookdemo |
|||
*/ |
|||
public class HookVideoDataEntity implements IXposedHookLoadPackage { |
|||
|
|||
public static final String TAG = "wxdemo"; |
|||
private final String TAGLOG = "log point -> "; |
|||
|
|||
public static ClassLoader classLoader; |
|||
public static Object FEED_PROFILE; // 构造函数第一个 枚举类型 |
|||
|
|||
public static Context Main_context; // 随时修改classloader |
|||
public static Object FinderMixSearchPresenter; // 搜索目标对象 一般只有一个 |
|||
|
|||
@Override |
|||
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { |
|||
if ("com.tencent.mm".equals(lpparam.packageName)){ |
|||
InitialObject(lpparam); |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* 搜索接口 回执信息拦截 |
|||
* @param lpparam |
|||
*/ |
|||
public void hook_Video(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
// hook 搜索测试 回执函数测试 |
|||
XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.finder.cgi.bm", lpparam.classLoader, |
|||
"onGYNetEnd", |
|||
int.class, int.class, int.class, String.class, |
|||
XposedHelpers.findClass("com.tencent.mm.network.q", lpparam.classLoader), byte[].class, |
|||
new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
Object o = param.thisObject; |
|||
Log.d(TAG, "beforeHookedMethod: hook 搜索接口"); |
|||
Object avd = XposedHelpers.callMethod(XposedHelpers.getObjectField(o, "hYT"), "aGA"); |
|||
LinkedList videoList = (LinkedList) XposedHelpers.getObjectField(avd, "rKi"); // 视频 |
|||
LinkedList userList = (LinkedList) XposedHelpers.getObjectField(avd, "wND"); // 用户 |
|||
// Log.d(TAG, "beforeHookedMethod: video likedList 0 is " + JSONObject.toJSONString(videoList.get(0))); |
|||
// Log.d(TAG, "beforeHookedMethod: user likedList 0 is " + JSONObject.toJSONString(userList.get(0))); |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "hook_Video: uuid is " + uuid); |
|||
Log.d(TAG, "beforeHookedMethod: size is "+ videoList.size()); |
|||
if (uuid != null && videoList.size() > 0){ |
|||
Map<String, Object> map = map1.get(uuid); |
|||
map.put("data", videoList); // 封装消息 视频 |
|||
map.put("user", userList); // 封装消息 用户 |
|||
synchronized (uuid){ |
|||
uuid.notify(); // 通知消息 |
|||
Log.d(TAG, "hook_Video: 封装对象成功"); |
|||
} |
|||
} |
|||
super.beforeHookedMethod(param); |
|||
} |
|||
}); |
|||
|
|||
|
|||
} |
|||
|
|||
/** |
|||
* 用户接口 回执信息拦截 |
|||
* @param lpparam |
|||
*/ |
|||
public void hook_User(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
// 测试一下获得的 FinderUserPageResponse 对象 |
|||
XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.finder.cgi.bs", lpparam.classLoader, |
|||
"onGYNetEnd", |
|||
int.class, int.class, int.class, String.class, XposedHelpers.findClass("com.tencent.mm.network.q", lpparam.classLoader), |
|||
byte[].class, new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
Object o = param.thisObject; |
|||
Object FinderUserPageResponse = XposedHelpers.callMethod(XposedHelpers.getObjectField(o, "rr"), "aGA"); |
|||
// Log.d(TAG, "afterHookedMethod: 1输出对象的结果: " + JSONObject.toJSONString(XposedHelpers.getObjectField(FinderUserPageResponse, "contact"))); |
|||
LinkedList f = (LinkedList) XposedHelpers.getObjectField(FinderUserPageResponse, "object"); |
|||
Log.d(TAG, "beforeHookedMethod: LinkedList size is " + f.size()); |
|||
// Log.d(TAG, "beforeHookedMethod: likedList 0 is " + JSONObject.toJSONString(f.get(0))); |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "afterHookedMethod: uuidk is " + uuid); |
|||
if (uuid != null && f.size() > 0){ |
|||
Map<String, Object> map = map1.get(uuid); |
|||
map.put("data", f); // 封装消息 |
|||
synchronized (uuid){ |
|||
uuid.notify(); // 通知消息 |
|||
Log.d(TAG, "beforeHookedMethod: 封装对象成功"); |
|||
} |
|||
} |
|||
super.beforeHookedMethod(param); |
|||
|
|||
} |
|||
|
|||
}); |
|||
|
|||
|
|||
// 每次翻页 部分参数需要改变 |
|||
XposedHelpers.findAndHookConstructor("com.tencent.mm.plugin.finder.cgi.bs", lpparam.classLoader, |
|||
String.class, long.class, XposedHelpers.findClass("com.tencent.mm.bw.b", lpparam.classLoader), int.class, |
|||
XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.auu", lpparam.classLoader), new XC_MethodHook() { |
|||
@Override |
|||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable { |
|||
String userId = (String) param.args[0]; |
|||
long maxId = (long) param.args[1]; |
|||
|
|||
String uuid = StoredObject.getUuidKey(); |
|||
Log.d(TAG, "构造函数中拦截: userId is => " + userId); |
|||
if (uuid != null){ |
|||
param.args[1] = Long.valueOf(uuid); |
|||
Log.d(TAG, "构造函数中拦截: 目标偏移量修改完毕 => " + uuid); |
|||
} |
|||
super.beforeHookedMethod(param); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
/** |
|||
* 入口hook函数 |
|||
* @param lpparam |
|||
*/ |
|||
private void InitialObject(XC_LoadPackage.LoadPackageParam lpparam){ |
|||
XposedHelpers.findAndHookMethod(Application.class, |
|||
"attach", |
|||
Context.class, |
|||
new XC_MethodHook(){ |
|||
@Override |
|||
protected void afterHookedMethod(MethodHookParam param) throws Throwable { |
|||
Main_context = (Context) param.args[0]; |
|||
classLoader = lpparam.classLoader; |
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {// TODO 这里需要兼容 |
|||
String paramName = Application.getProcessName(); // 获取进程名程 只对主进程进行hook |
|||
Log.d(TAG, "afterHookedMethod: " + paramName); |
|||
|
|||
if("com.tencent.mm".equals(paramName)){ // 只hook主进程 |
|||
initSekiroClient(); // 服务注入 网络波动 |
|||
Log.d(TAG, "afterHookedMethod: 注 入 服 务 成功"); |
|||
hook_Video(lpparam); |
|||
hook_User(lpparam); |
|||
|
|||
} |
|||
}else{ |
|||
Log.d(TAG, "afterHookedMethod: ???"); |
|||
// ApplicationInfo applicationInfo = Main_context.getApplicationInfo(); |
|||
// String processName = applicationInfo.processName; |
|||
// Log.d(TAG, "afterHookedMethod: " + processName); |
|||
|
|||
} |
|||
super.afterHookedMethod(param); |
|||
} |
|||
}); |
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* 自定义 客户端ID以及服务器地址 |
|||
*/ |
|||
private void initSekiroClient() { |
|||
Log.d(TAG, "initSekiroClient: 启动 sekiro ing"); |
|||
String groupName = (String) SpUtil.readObjectByProvider(Main_context, "vx_group_name", String.class); |
|||
String clientId = (String) SpUtil.readObjectByProvider(Main_context, "vx_client", String.class); |
|||
String serverHost = (String) SpUtil.readObjectByProvider(Main_context, "vx_server", String.class); |
|||
int serverPort = 5612; |
|||
Log.d(TAG, String.format("initSekiroClient: 获取provier数据 groupName is %s; clientId is %s; serverHost is %s", |
|||
groupName, clientId, serverHost)); |
|||
if ("".equals(serverHost)){ |
|||
Log.d(TAG, "initSekiroClient: provider 初始化失败"); |
|||
}else{ |
|||
Log.d(TAG, "initSekiroClient: clientId is " + clientId); |
|||
SekiroClient sekiroClient = new SekiroClient(groupName, clientId, serverHost, serverPort); |
|||
|
|||
// 注册服务 |
|||
sekiroClient.setupSekiroRequestInitializer(new SekiroRequestInitializer() { |
|||
@Override |
|||
public void onSekiroRequest(SekiroRequest sekiroRequest, HandlerRegistry handlerRegistry) { |
|||
handlerRegistry.registerSekiroHandler(new UserVideoHandler()); // 用户 |
|||
handlerRegistry.registerSekiroHandler(new SearchUserHandler()); // 搜索 |
|||
} |
|||
}).start(); |
|||
|
|||
|
|||
Log.d(TAG, "initSekiroClient: 测试启动rpc服务"); |
|||
|
|||
XposedBridge.log(String.format("sekiro启动成功: 获取provier数据 groupName is %s; clientId is %s; serverHost is %s", |
|||
groupName, clientId, serverHost)); |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,102 @@ |
|||
package com.example.wechatvideo; |
|||
|
|||
import android.content.Context; |
|||
import android.os.Bundle; |
|||
import android.view.View; |
|||
import android.widget.Button; |
|||
import android.widget.EditText; |
|||
import android.widget.TextView; |
|||
import android.widget.Toast; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
import androidx.appcompat.app.AppCompatActivity; |
|||
|
|||
import com.example.wechatvideo.content.MultiprocessSharedPreferences; |
|||
import com.example.wechatvideo.utils.SpUtil; |
|||
|
|||
|
|||
public class MainActivity extends AppCompatActivity { |
|||
|
|||
private EditText clientId, server; |
|||
private Button update; |
|||
private TextView clientIdText, serverText, groupName; |
|||
private String vx_group_name = "vxin"; |
|||
|
|||
|
|||
@Override |
|||
protected void onCreate(@Nullable Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
setContentView(R.layout.activity_main); |
|||
|
|||
// 设置权限 |
|||
MultiprocessSharedPreferences.setAuthority("com.example.wechatvideo.provider"); |
|||
|
|||
// 初始化 视图 |
|||
initView(this); |
|||
|
|||
clientIdText = (TextView) findViewById(R.id.get_phone); |
|||
serverText = (TextView) findViewById(R.id.get_web); |
|||
groupName = (TextView) findViewById(R.id.get_group); |
|||
|
|||
|
|||
|
|||
update = (Button) findViewById(R.id.btn_update); |
|||
|
|||
update.setOnClickListener(new View.OnClickListener() { |
|||
@Override |
|||
public void onClick(View v) { |
|||
// 获取当前修改的 |
|||
String clientId = ((EditText) findViewById(R.id.set_phone)).getText().toString().trim(); |
|||
String server = ((EditText) findViewById(R.id.set_web)).getText().toString().trim(); |
|||
String address = String.format("%s:%s", server, "5612"); |
|||
|
|||
SpUtil.putString(MainActivity.this, "vx_client", clientId); |
|||
SpUtil.putString(MainActivity.this, "vx_server" , server); |
|||
SpUtil.putString(MainActivity.this, "vx_group_name", vx_group_name); // 初始化group_name |
|||
|
|||
// 展示区域 |
|||
clientIdText.setText(clientId); |
|||
groupName.setText(vx_group_name); |
|||
serverText.setText(address); |
|||
|
|||
Toast.makeText(MainActivity.this.getApplicationContext(), |
|||
"更新配置成功, 重启微信生效!", |
|||
Toast.LENGTH_SHORT).show(); |
|||
} |
|||
|
|||
}); |
|||
|
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* 初始化视图 |
|||
* @param context |
|||
*/ |
|||
public void initView(Context context){ |
|||
|
|||
// 可编辑区域 |
|||
clientId = (EditText) findViewById(R.id.set_phone); |
|||
server = (EditText) findViewById(R.id.set_web); |
|||
|
|||
// 展示区域 |
|||
clientIdText = (TextView) findViewById(R.id.get_phone); |
|||
serverText = (TextView) findViewById(R.id.get_web); |
|||
groupName = (TextView) findViewById(R.id.get_group); |
|||
|
|||
String clientId_text = (String)SpUtil.readObjectByProvider(context, "vx_client", String.class); |
|||
String address = String.format("%s:%s", (String) SpUtil.readObjectByProvider(context, "vx_server", String.class), "5612"); |
|||
|
|||
// 从provider中读出结果 到展示区域 |
|||
clientIdText.setText(clientId_text); |
|||
serverText.setText((String)SpUtil.readObjectByProvider(context, "vx_server", String.class)); |
|||
groupName.setText(vx_group_name); |
|||
|
|||
|
|||
// 从provider中读出结果 到可编辑区域 |
|||
clientId.setText((String)SpUtil.readObjectByProvider(context, "vx_client", String.class)); |
|||
server.setText((String)SpUtil.readObjectByProvider(context, "vx_server", String.class)); |
|||
|
|||
} |
|||
} |
@ -0,0 +1,864 @@ |
|||
|
|||
package com.example.wechatvideo.content; |
|||
|
|||
|
|||
import android.annotation.SuppressLint; |
|||
import android.content.BroadcastReceiver; |
|||
import android.content.ContentProvider; |
|||
import android.content.ContentResolver; |
|||
import android.content.ContentValues; |
|||
import android.content.Context; |
|||
import android.content.Intent; |
|||
import android.content.IntentFilter; |
|||
import android.content.SharedPreferences; |
|||
import android.content.UriMatcher; |
|||
import android.content.pm.PackageInfo; |
|||
import android.content.pm.PackageManager; |
|||
import android.content.pm.ProviderInfo; |
|||
import android.database.Cursor; |
|||
import android.database.MatrixCursor; |
|||
import android.net.Uri; |
|||
import android.os.Build; |
|||
import android.os.Bundle; |
|||
import android.os.DeadObjectException; |
|||
import android.util.Log; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import java.lang.reflect.Constructor; |
|||
import java.lang.reflect.Field; |
|||
import java.lang.reflect.InvocationTargetException; |
|||
import java.lang.reflect.Method; |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.HashMap; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.WeakHashMap; |
|||
|
|||
/** |
|||
* 使用ContentProvider实现多进程SharedPreferences读写;<br> |
|||
* 1、ContentProvider天生支持多进程访问;<br> |
|||
* 2、使用内部私有BroadcastReceiver实现多进程OnSharedPreferenceChangeListener监听;<br> |
|||
* |
|||
* 使用方法:AndroidManifest.xml中添加provider申明:<br> |
|||
* <pre> |
|||
* <provider android:name="com.android.zgj.utils.MultiprocessSharedPreferences" |
|||
* android:authorities="com.android.zgj.MultiprocessSharedPreferences" |
|||
* android:process="com.android.zgj.MultiprocessSharedPreferences" |
|||
* android:exported="false" /> |
|||
* <!-- authorities属性里面最好使用包名做前缀,apk在安装时authorities同名的provider需要校验签名,否则无法安装;--!/><br> |
|||
* </pre> |
|||
* |
|||
* ContentProvider方式实现要注意:<br> |
|||
* 1、当ContentProvider所在进程android.os.Process.killProcess(pid)时,会导致整个应用程序完全意外退出或者ContentProvider所在进程重启;<br> |
|||
* 重启报错信息:Acquiring provider <processName> for user 0: existing object's process dead;<br> |
|||
* 2、如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用,因此put值时默认返回false,get值时默认返回null;<br> |
|||
* |
|||
* 其他方式实现SharedPreferences的问题:<br> |
|||
* 使用FileLock和FileObserver也可以实现多进程SharedPreferences读写,但是维护成本高,需要定期对照系统实现更新新的特性; |
|||
* |
|||
* 引用git:https://github.com/seven456/MultiprocessSharedPreferences |
|||
*/ |
|||
public class MultiprocessSharedPreferences extends ContentProvider implements SharedPreferences { |
|||
private static final String TAG_CONTENT = "MultiprocessSharedPreferences"; |
|||
public static final boolean DEBUG = true; // TODO |
|||
private Context mContext; |
|||
private String mName; |
|||
private int mMode; |
|||
private boolean mIsSafeMode; |
|||
private static final Object CONTENT = new Object(); |
|||
private WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners; |
|||
private BroadcastReceiver mReceiver; |
|||
|
|||
private static String AUTHORITY; |
|||
private static volatile Uri AUTHORITY_URI; |
|||
private UriMatcher mUriMatcher; |
|||
private static final String KEY = "value"; |
|||
private static final String KEY_NAME = "name"; |
|||
private static final String PATH_WILDCARD = "*/"; |
|||
private static final String PATH_GET_ALL = "getAll"; |
|||
private static final String PATH_GET_STRING = "getString"; |
|||
private static final String PATH_GET_INT = "getInt"; |
|||
private static final String PATH_GET_LONG = "getLong"; |
|||
private static final String PATH_GET_FLOAT = "getFloat"; |
|||
private static final String PATH_GET_BOOLEAN = "getBoolean"; |
|||
private static final String PATH_CONTAINS = "contains"; |
|||
private static final String PATH_APPLY = "apply"; |
|||
private static final String PATH_COMMIT = "commit"; |
|||
private static final String PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = "registerOnSharedPreferenceChangeListener"; |
|||
private static final String PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = "unregisterOnSharedPreferenceChangeListener"; |
|||
private static final String PATH_GET_STRING_SET = "getStringSet"; |
|||
private static final int GET_ALL = 1; |
|||
private static final int GET_STRING = 2; |
|||
private static final int GET_INT = 3; |
|||
private static final int GET_LONG = 4; |
|||
private static final int GET_FLOAT = 5; |
|||
private static final int GET_BOOLEAN = 6; |
|||
private static final int CONTAINS = 7; |
|||
private static final int APPLY = 8; |
|||
private static final int COMMIT = 9; |
|||
private static final int REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 10; |
|||
private static final int UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 11; |
|||
private static final int GET_STRING_SET = 12; |
|||
private HashMap<String, Integer> mListenersCount; |
|||
|
|||
private static class ReflectionUtil { |
|||
|
|||
public static ContentValues contentValuesNewInstance(HashMap<String, Object> values) { |
|||
try { |
|||
Constructor<ContentValues> c = ContentValues.class.getDeclaredConstructor(new Class[] { HashMap.class }); // hide |
|||
c.setAccessible(true); |
|||
return c.newInstance(values); |
|||
} catch (IllegalArgumentException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (IllegalAccessException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (InvocationTargetException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (NoSuchMethodException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (InstantiationException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
public static Editor editorPutStringSet(Editor editor, String key, Set<String> values) { |
|||
try { |
|||
Method method = editor.getClass().getDeclaredMethod("putStringSet", new Class[] { String.class, Set.class }); // Android 3.0 |
|||
return (Editor) method.invoke(editor, key, values); |
|||
} catch (IllegalArgumentException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (IllegalAccessException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (InvocationTargetException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (NoSuchMethodException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
public static Set<String> sharedPreferencesGetStringSet(SharedPreferences sharedPreferences, String key, Set<String> values) { |
|||
try { |
|||
Method method = sharedPreferences.getClass().getDeclaredMethod("getStringSet", new Class[] { String.class, Set.class }); // Android 3.0 |
|||
return (Set<String>) method.invoke(sharedPreferences, key, values); |
|||
} catch (IllegalArgumentException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (IllegalAccessException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (InvocationTargetException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (NoSuchMethodException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
public static void editorApply(Editor editor) { |
|||
try { |
|||
Method method = editor.getClass().getDeclaredMethod("apply"); // Android 2.3 |
|||
method.invoke(editor); |
|||
} catch (IllegalArgumentException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (IllegalAccessException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (InvocationTargetException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (NoSuchMethodException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
public static String contentProvidermAuthority(ContentProvider contentProvider) { |
|||
try { |
|||
Field mAuthority = ContentProvider.class.getDeclaredField("mAuthority"); // Android 5.0 |
|||
mAuthority.setAccessible(true); |
|||
return (String) mAuthority.get(contentProvider); |
|||
} catch (NoSuchFieldException e) { |
|||
throw new RuntimeException(e); |
|||
} catch (IllegalAccessException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用; |
|||
private boolean isSafeMode(Context context) { |
|||
boolean isSafeMode = false; |
|||
try { |
|||
isSafeMode = context.getPackageManager().isSafeMode(); |
|||
// 解决崩溃: |
|||
// java.lang.RuntimeException: Package manager has died |
|||
// at android.app.ApplicationPackageManager.isSafeMode(ApplicationPackageManager.java:820) |
|||
} catch (RuntimeException e) { |
|||
if (!isPackageManagerHasDiedException(e)) { |
|||
throw e; |
|||
} |
|||
} |
|||
return isSafeMode; |
|||
} |
|||
|
|||
/** |
|||
* (可选)设置AUTHORITY,不用在初始化时遍历程序的AndroidManifest.xml文件获取android:authorities的值,减少初始化时间提高运行速度; |
|||
* @param authority |
|||
*/ |
|||
public static void setAuthority(String authority) { |
|||
AUTHORITY = authority; |
|||
} |
|||
|
|||
@SuppressLint("LongLogTag") |
|||
private boolean checkInitAuthority(Context context) { |
|||
if (AUTHORITY_URI == null) { |
|||
synchronized (MultiprocessSharedPreferences.this) { |
|||
if (AUTHORITY_URI == null) { |
|||
if(AUTHORITY == null) { |
|||
if (Build.VERSION.SDK_INT >= 21 && this instanceof ContentProvider) { |
|||
AUTHORITY = ReflectionUtil.contentProvidermAuthority(this); |
|||
} else { |
|||
PackageInfo packageInfos = null; |
|||
try { |
|||
packageInfos = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS); |
|||
} catch (PackageManager.NameNotFoundException e) { |
|||
if (DEBUG) { |
|||
e.printStackTrace(); |
|||
} |
|||
} catch (RuntimeException e) { |
|||
if (!isPackageManagerHasDiedException(e)) { |
|||
throw new RuntimeException("checkInitAuthority", e); |
|||
} |
|||
} |
|||
if (packageInfos != null && packageInfos.providers != null) { |
|||
for (ProviderInfo providerInfo : packageInfos.providers) { |
|||
if (providerInfo.name.equals(MultiprocessSharedPreferences.class.getName())) { |
|||
AUTHORITY = providerInfo.authority; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (DEBUG) { |
|||
if (AUTHORITY == null) { |
|||
throw new IllegalArgumentException("'AUTHORITY' initialize failed, Unable to find explicit provider class " + MultiprocessSharedPreferences.class.getName() + "; have you declared this provider in your AndroidManifest.xml?"); |
|||
} else { |
|||
Log.d(TAG_CONTENT, "checkInitAuthority.AUTHORITY = " + AUTHORITY); |
|||
} |
|||
} |
|||
AUTHORITY_URI = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + AUTHORITY); |
|||
} |
|||
} |
|||
} |
|||
return AUTHORITY_URI != null; |
|||
} |
|||
|
|||
private boolean isPackageManagerHasDiedException(Throwable e) { |
|||
// 1、packageManager.getPackageInfo |
|||
// java.lang.RuntimeException: Package manager has died |
|||
// at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:80) |
|||
// ... |
|||
// Caused by: android.os.DeadObjectException |
|||
// at android.os.BinderProxy.transact(Native Method) |
|||
// at android.content.pm.IPackageManager$Stub$Proxy.getPackageInfo(IPackageManager.java:1374) |
|||
|
|||
// 2、contentResolver.query |
|||
// java.lang.RuntimeException: Package manager has died |
|||
// at android.app.ApplicationPackageManager.resolveContentProvider(ApplicationPackageManager.java:636) |
|||
// at android.app.ActivityThread.acquireProvider(ActivityThread.java:4750) |
|||
// at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:2234) |
|||
// at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:1425) |
|||
// at android.content.ContentResolver.query(ContentResolver.java:445) |
|||
// at android.content.ContentResolver.query(ContentResolver.java:404) |
|||
// at com.qihoo.storager.MultiprocessSharedPreferences.getValue(AppStore:502) |
|||
// ... |
|||
// Caused by: android.os.TransactionTooLargeException |
|||
// at android.os.BinderProxy.transact(Native Method) |
|||
// at android.content.pm.IPackageManager$Stub$Proxy.resolveContentProvider(IPackageManager.java:2500) |
|||
// at android.app.ApplicationPackageManager.resolveContentProvider(ApplicationPackageManager.java:634) |
|||
if (e instanceof RuntimeException |
|||
&& e.getMessage() != null |
|||
&& e.getMessage().contains("Package manager has died")) { |
|||
Throwable cause = getLastCause(e); |
|||
if (cause instanceof DeadObjectException || cause.getClass().getName().equals("android.os.TransactionTooLargeException")) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private boolean isUnstableCountException(Throwable e) { |
|||
// java.lang.RuntimeException: java.lang.IllegalStateException: unstableCount < 0: -1 |
|||
// at com.qihoo.storager.MultiprocessSharedPreferences.getValue(AppStore:459) |
|||
// at com.qihoo.storager.MultiprocessSharedPreferences.getBoolean(AppStore:282) |
|||
// ... |
|||
// Caused by: java.lang.IllegalStateException: unstableCount < 0: -1 |
|||
// at android.os.Parcel.readException(Parcel.java:1628) |
|||
// at android.os.Parcel.readException(Parcel.java:1573) |
|||
// at android.app.ActivityManagerProxy.refContentProvider(ActivityManagerNative.java:3680) |
|||
// at android.app.ActivityThread.releaseProvider(ActivityThread.java:5052) |
|||
// at android.app.ContextImpl$ApplicationContentResolver.releaseUnstableProvider(ContextImpl.java:2036) |
|||
// at android.content.ContentResolver.query(ContentResolver.java:534) |
|||
// at android.content.ContentResolver.query(ContentResolver.java:435) |
|||
// at com.qihoo.storager.MultiprocessSharedPreferences.a(AppStore:452) |
|||
if (e instanceof RuntimeException |
|||
&& e.getMessage() != null |
|||
&& e.getMessage().contains("unstableCount < 0: -1")) { |
|||
if (getLastCause(e) instanceof IllegalStateException) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 获取异常栈中最底层的 Throwable Cause; |
|||
* |
|||
* @param tr |
|||
* @return |
|||
*/ |
|||
private Throwable getLastCause(Throwable tr) { |
|||
Throwable cause = tr.getCause(); |
|||
Throwable causeLast = null; |
|||
while (cause != null) { |
|||
causeLast = cause; |
|||
cause = cause.getCause(); |
|||
} |
|||
if (causeLast == null) { |
|||
causeLast = new Throwable(); |
|||
} |
|||
return causeLast; |
|||
} |
|||
|
|||
/** |
|||
* mode不使用{@link Context#MODE_MULTI_PROCESS}特可以支持多进程了; |
|||
* |
|||
* @param mode |
|||
* |
|||
* @see Context#MODE_PRIVATE |
|||
* @see Context#MODE_WORLD_READABLE |
|||
* @see Context#MODE_WORLD_WRITEABLE |
|||
*/ |
|||
public static SharedPreferences getSharedPreferences(Context context, String name, int mode) { |
|||
return new MultiprocessSharedPreferences(context, name, mode); |
|||
} |
|||
|
|||
/** |
|||
* @deprecated 此默认构造函数只用于父类ContentProvider在初始化时使用; |
|||
*/ |
|||
// @Deprecated TODO: 不知道为啥加这个会报错 |
|||
public MultiprocessSharedPreferences() { |
|||
|
|||
} |
|||
|
|||
private MultiprocessSharedPreferences(Context context, String name, int mode) { |
|||
mContext = context; |
|||
mName = name; |
|||
mMode = mode; |
|||
mIsSafeMode = isSafeMode(mContext); |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
@Override |
|||
public Map<String, ?> getAll() { |
|||
Map<String, ?> value = (Map<String, ?>) getValue(PATH_GET_ALL, null, null); |
|||
return value == null ? new HashMap<String, Object>() : value; |
|||
} |
|||
|
|||
@Override |
|||
public String getString(String key, String defValue) { |
|||
return (String) getValue(PATH_GET_STRING, key, defValue); |
|||
} |
|||
|
|||
// @Override // Android 3.0 |
|||
@SuppressWarnings("unchecked") |
|||
public Set<String> getStringSet(String key, Set<String> defValues) { |
|||
return (Set<String>) getValue(PATH_GET_STRING_SET, key, defValues); |
|||
} |
|||
|
|||
@Override |
|||
public int getInt(String key, int defValue) { |
|||
return (Integer) getValue(PATH_GET_INT, key, defValue); |
|||
} |
|||
|
|||
@Override |
|||
public long getLong(String key, long defValue) { |
|||
return (Long) getValue(PATH_GET_LONG, key, defValue); |
|||
} |
|||
|
|||
@Override |
|||
public float getFloat(String key, float defValue) { |
|||
return (Float) getValue(PATH_GET_FLOAT, key, defValue); |
|||
} |
|||
|
|||
@Override |
|||
public boolean getBoolean(String key, boolean defValue) { |
|||
return (Boolean) getValue(PATH_GET_BOOLEAN, key, defValue); |
|||
} |
|||
|
|||
@Override |
|||
public boolean contains(String key) { |
|||
return (Boolean) getValue(PATH_CONTAINS, key, false); |
|||
} |
|||
|
|||
@Override |
|||
public Editor edit() { |
|||
return new EditorImpl(); |
|||
} |
|||
|
|||
@Override |
|||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { |
|||
synchronized (this) { |
|||
if (mListeners == null) { |
|||
mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); |
|||
} |
|||
Boolean result = (Boolean) getValue(PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false); |
|||
if (result != null && result) { |
|||
mListeners.put(listener, CONTENT); |
|||
if (mReceiver == null) { |
|||
mReceiver = new BroadcastReceiver() { |
|||
@Override |
|||
public void onReceive(Context context, Intent intent) { |
|||
String name = intent.getStringExtra(KEY_NAME); |
|||
@SuppressWarnings("unchecked") |
|||
List<String> keysModified = (List<String>) intent.getSerializableExtra(KEY); |
|||
if (mName.equals(name) && keysModified != null) { |
|||
Set<OnSharedPreferenceChangeListener> listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); |
|||
for (int i = keysModified.size() - 1; i >= 0; i--) { |
|||
final String key = keysModified.get(i); |
|||
for (OnSharedPreferenceChangeListener listener : listeners) { |
|||
if (listener != null) { |
|||
listener.onSharedPreferenceChanged(MultiprocessSharedPreferences.this, key); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
mContext.registerReceiver(mReceiver, new IntentFilter(makeAction(mName))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { |
|||
synchronized (this) { |
|||
getValue(PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false); // WeakHashMap |
|||
if (mListeners != null) { |
|||
mListeners.remove(listener); |
|||
if (mListeners.isEmpty() && mReceiver != null) { |
|||
mContext.unregisterReceiver(mReceiver); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public final class EditorImpl implements Editor { |
|||
private final Map<String, Object> mModified = new HashMap<String, Object>(); |
|||
private boolean mClear = false; |
|||
|
|||
@Override |
|||
public Editor putString(String key, String value) { |
|||
synchronized (this) { |
|||
mModified.put(key, value); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
// @Override // Android 3.0 |
|||
public Editor putStringSet(String key, Set<String> values) { |
|||
synchronized (this) { |
|||
mModified.put(key, (values == null) ? null : new HashSet<String>(values)); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor putInt(String key, int value) { |
|||
synchronized (this) { |
|||
mModified.put(key, value); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor putLong(String key, long value) { |
|||
synchronized (this) { |
|||
mModified.put(key, value); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor putFloat(String key, float value) { |
|||
synchronized (this) { |
|||
mModified.put(key, value); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor putBoolean(String key, boolean value) { |
|||
synchronized (this) { |
|||
mModified.put(key, value); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor remove(String key) { |
|||
synchronized (this) { |
|||
mModified.put(key, null); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Editor clear() { |
|||
synchronized (this) { |
|||
mClear = true; |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void apply() { |
|||
setValue(PATH_APPLY); |
|||
} |
|||
|
|||
@Override |
|||
public boolean commit() { |
|||
return setValue(PATH_COMMIT); |
|||
} |
|||
|
|||
@SuppressLint("LongLogTag") |
|||
private boolean setValue(String pathSegment) { |
|||
boolean result = false; |
|||
if (!mIsSafeMode && checkInitAuthority(mContext)) { // 如果设备处在“安全模式”,返回false; |
|||
String[] selectionArgs = new String[] { String.valueOf(mMode), String.valueOf(mClear) }; |
|||
synchronized (this) { |
|||
Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment); |
|||
ContentValues values = ReflectionUtil.contentValuesNewInstance((HashMap<String, Object>) mModified); |
|||
try { |
|||
result = mContext.getContentResolver().update(uri, values, null, selectionArgs) > 0; |
|||
} catch (IllegalArgumentException e) { |
|||
// 解决ContentProvider所在进程被杀时的抛出的异常: |
|||
// java.lang.IllegalArgumentException: Unknown URI content://xxx.xxx.xxx/xxx/xxx |
|||
// at android.content.ContentResolver.update(ContentResolver.java:1312) |
|||
if (DEBUG) { |
|||
e.printStackTrace(); |
|||
} |
|||
} catch (RuntimeException e) { |
|||
if (!isPackageManagerHasDiedException(e) && !isUnstableCountException(e)) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (DEBUG) { |
|||
Log.d(TAG_CONTENT, "setValue.mName = " + mName + ", pathSegment = " + pathSegment + ", mModified.size() = " + mModified.size()); |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("LongLogTag") |
|||
private Object getValue(String pathSegment, String key, Object defValue) { |
|||
Object v = null; |
|||
if (!mIsSafeMode && checkInitAuthority(mContext)) { // 如果设备处在“安全模式”,返回defValue; |
|||
Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment); |
|||
String[] projection = null; |
|||
if (PATH_GET_STRING_SET.equals(pathSegment) && defValue != null) { |
|||
@SuppressWarnings("unchecked") |
|||
Set<String> set = (Set<String>) defValue; |
|||
projection = new String[set.size()]; |
|||
set.toArray(projection); |
|||
} |
|||
String[] selectionArgs = new String[] { String.valueOf(mMode), key, defValue == null ? null : String.valueOf(defValue) }; |
|||
Cursor cursor = null; |
|||
try { |
|||
cursor = mContext.getContentResolver().query(uri, projection, null, selectionArgs, null); |
|||
} catch (SecurityException e) { |
|||
// 解决崩溃: |
|||
// java.lang.SecurityException: Permission Denial: reading com.qihoo.storager.MultiprocessSharedPreferences uri content://com.qihoo.appstore.MultiprocessSharedPreferences/LogUtils/getBoolean from pid=2446, uid=10116 requires the provider be exported, or grantUriPermission() |
|||
// at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:332) |
|||
// ... |
|||
// at android.content.ContentResolver.query(ContentResolver.java:317) |
|||
if (DEBUG) { |
|||
e.printStackTrace(); |
|||
} |
|||
} catch (RuntimeException e) { |
|||
if (!isPackageManagerHasDiedException(e) && !isUnstableCountException(e)) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
if (cursor != null) { |
|||
Bundle bundle = null; |
|||
try { |
|||
bundle = cursor.getExtras(); |
|||
} catch (RuntimeException e) { |
|||
// 解决ContentProvider所在进程被杀时的抛出的异常: |
|||
// java.lang.RuntimeException: android.os.DeadObjectException |
|||
// at android.database.BulkCursorToCursorAdaptor.getExtras(BulkCursorToCursorAdaptor.java:173) |
|||
// at android.database.CursorWrapper.getExtras(CursorWrapper.java:94) |
|||
if (DEBUG) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
if (bundle != null) { |
|||
v = bundle.get(KEY); |
|||
bundle.clear(); |
|||
} |
|||
cursor.close(); |
|||
} |
|||
} |
|||
if (DEBUG) { |
|||
Log.d(TAG_CONTENT, "getValue.mName = " + mName + ", pathSegment = " + pathSegment + ", key = " + key + ", defValue = " + defValue); |
|||
} |
|||
return v == null ? defValue : v; |
|||
} |
|||
|
|||
private String makeAction(String name) { |
|||
return String.format("%1$s_%2$s", MultiprocessSharedPreferences.class.getName(), name); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onCreate() { |
|||
if (checkInitAuthority(getContext())) { |
|||
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_ALL, GET_ALL); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_STRING, GET_STRING); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_INT, GET_INT); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_LONG, GET_LONG); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_FLOAT, GET_FLOAT); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_BOOLEAN, GET_BOOLEAN); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_CONTAINS, CONTAINS); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_APPLY, APPLY); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_COMMIT, COMMIT); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER); |
|||
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_STRING_SET, GET_STRING_SET); |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { |
|||
String name = uri.getPathSegments().get(0); |
|||
int mode = Integer.parseInt(selectionArgs[0]); |
|||
String key = selectionArgs[1]; |
|||
String defValue = selectionArgs[2]; |
|||
Bundle bundle = new Bundle(); |
|||
switch (mUriMatcher.match(uri)) { |
|||
case GET_ALL: |
|||
bundle.putSerializable(KEY, (HashMap<String, ?>) getSystemSharedPreferences(name, mode).getAll()); |
|||
break; |
|||
case GET_STRING: |
|||
bundle.putString(KEY, getSystemSharedPreferences(name, mode).getString(key, defValue)); |
|||
break; |
|||
case GET_INT: |
|||
bundle.putInt(KEY, getSystemSharedPreferences(name, mode).getInt(key, Integer.parseInt(defValue))); |
|||
break; |
|||
case GET_LONG: |
|||
bundle.putLong(KEY, getSystemSharedPreferences(name, mode).getLong(key, Long.parseLong(defValue))); |
|||
break; |
|||
case GET_FLOAT: |
|||
bundle.putFloat(KEY, getSystemSharedPreferences(name, mode).getFloat(key, Float.parseFloat(defValue))); |
|||
break; |
|||
case GET_BOOLEAN: |
|||
bundle.putBoolean(KEY, getSystemSharedPreferences(name, mode).getBoolean(key, Boolean.parseBoolean(defValue))); |
|||
break; |
|||
case CONTAINS: |
|||
bundle.putBoolean(KEY, getSystemSharedPreferences(name, mode).contains(key)); |
|||
break; |
|||
case REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: { |
|||
checkInitListenersCount(); |
|||
Integer countInteger = mListenersCount.get(name); |
|||
int count = (countInteger == null ? 0 : countInteger) + 1; |
|||
mListenersCount.put(name, count); |
|||
countInteger = mListenersCount.get(name); |
|||
bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger)); |
|||
} |
|||
break; |
|||
case UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: { |
|||
checkInitListenersCount(); |
|||
Integer countInteger = mListenersCount.get(name); |
|||
int count = (countInteger == null ? 0 : countInteger) - 1; |
|||
if (count <= 0) { |
|||
mListenersCount.remove(name); |
|||
bundle.putBoolean(KEY, !mListenersCount.containsKey(name)); |
|||
} else { |
|||
mListenersCount.put(name, count); |
|||
countInteger = mListenersCount.get(name); |
|||
bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger)); |
|||
} |
|||
} |
|||
break; |
|||
case GET_STRING_SET: { |
|||
if (Build.VERSION.SDK_INT >= 11) { // Android 3.0 |
|||
Set<String> set = null; |
|||
if (projection != null) { |
|||
set = new HashSet<String>(Arrays.asList(projection)); |
|||
} |
|||
bundle.putSerializable(KEY, (HashSet<String>) ReflectionUtil.sharedPreferencesGetStringSet(getSystemSharedPreferences(name, mode), key, set)); |
|||
} |
|||
} |
|||
default: |
|||
if (DEBUG) { |
|||
throw new IllegalArgumentException("At query, This is Unknown Uri:" + uri + ", AUTHORITY = " + AUTHORITY); |
|||
} |
|||
} |
|||
return new BundleCursor(bundle); |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
@Override |
|||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
|||
int result = 0; |
|||
String name = uri.getPathSegments().get(0); |
|||
int mode = Integer.parseInt(selectionArgs[0]); |
|||
SharedPreferences preferences = getSystemSharedPreferences(name, mode); |
|||
int match = mUriMatcher.match(uri); |
|||
switch (match) { |
|||
case APPLY: |
|||
case COMMIT: |
|||
boolean hasListeners = mListenersCount != null && mListenersCount.get(name) != null && mListenersCount.get(name) > 0; |
|||
ArrayList<String> keysModified = null; |
|||
Map<String, Object> map = null; |
|||
if (hasListeners) { |
|||
keysModified = new ArrayList<String>(); |
|||
map = (Map<String, Object>) preferences.getAll(); |
|||
} |
|||
Editor editor = preferences.edit(); |
|||
boolean clear = Boolean.parseBoolean(selectionArgs[1]); |
|||
if (clear) { |
|||
if (hasListeners && !map.isEmpty()) { |
|||
for (Map.Entry<String, Object> entry : map.entrySet()) { |
|||
keysModified.add(entry.getKey()); |
|||
} |
|||
} |
|||
editor.clear(); |
|||
} |
|||
for (Map.Entry<String, Object> entry : values.valueSet()) { |
|||
String k = entry.getKey(); |
|||
Object v = entry.getValue(); |
|||
// Android 5.L_preview : "this" is the magic value for a removal mutation. In addition, |
|||
// setting a value to "null" for a given key is specified to be |
|||
// equivalent to calling remove on that key. |
|||
if (v instanceof EditorImpl || v == null) { |
|||
editor.remove(k); |
|||
if (hasListeners && map.containsKey(k)) { |
|||
keysModified.add(k); |
|||
} |
|||
} else { |
|||
if (hasListeners && (!map.containsKey(k) || (map.containsKey(k) && !v.equals(map.get(k))))) { |
|||
keysModified.add(k); |
|||
} |
|||
} |
|||
|
|||
if (v instanceof String) { |
|||
editor.putString(k, (String) v); |
|||
} else if (v instanceof Set) { |
|||
ReflectionUtil.editorPutStringSet(editor, k, (Set<String>) v); // Android 3.0 |
|||
} else if (v instanceof Integer) { |
|||
editor.putInt(k, (Integer) v); |
|||
} else if (v instanceof Long) { |
|||
editor.putLong(k, (Long) v); |
|||
} else if (v instanceof Float) { |
|||
editor.putFloat(k, (Float) v); |
|||
} else if (v instanceof Boolean) { |
|||
editor.putBoolean(k, (Boolean) v); |
|||
} |
|||
} |
|||
if (hasListeners && keysModified.isEmpty()) { |
|||
result = 1; |
|||
} else { |
|||
switch (match) { |
|||
case APPLY: |
|||
ReflectionUtil.editorApply(editor); // Android 2.3 |
|||
result = 1; |
|||
// Okay to notify the listeners before it's hit disk |
|||
// because the listeners should always get the same |
|||
// SharedPreferences instance back, which has the |
|||
// changes reflected in memory. |
|||
notifyListeners(name, keysModified); |
|||
break; |
|||
case COMMIT: |
|||
if (editor.commit()) { |
|||
result = 1; |
|||
notifyListeners(name, keysModified); |
|||
} |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
values.clear(); |
|||
break; |
|||
default: |
|||
if (DEBUG) { |
|||
throw new IllegalArgumentException("At update, This is Unknown Uri:" + uri + ", AUTHORITY = " + AUTHORITY); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
public String getType(@NonNull Uri uri) { |
|||
throw new UnsupportedOperationException("No external call"); |
|||
} |
|||
|
|||
@Override |
|||
public Uri insert(@NonNull Uri uri, ContentValues values) { |
|||
throw new UnsupportedOperationException("No external insert"); |
|||
} |
|||
|
|||
@Override |
|||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { |
|||
throw new UnsupportedOperationException("No external delete"); |
|||
} |
|||
|
|||
private SharedPreferences getSystemSharedPreferences(String name, int mode) { |
|||
return getContext().getSharedPreferences(name, mode); |
|||
} |
|||
|
|||
private void checkInitListenersCount() { |
|||
if (mListenersCount == null) { |
|||
mListenersCount = new HashMap<String, Integer>(); |
|||
} |
|||
} |
|||
|
|||
private void notifyListeners(String name, ArrayList<String> keysModified) { |
|||
if (keysModified != null && !keysModified.isEmpty()) { |
|||
Intent intent = new Intent(); |
|||
intent.setAction(makeAction(name)); |
|||
intent.setPackage(getContext().getPackageName()); |
|||
intent.putExtra(KEY_NAME, name); |
|||
intent.putExtra(KEY, keysModified); |
|||
getContext().sendBroadcast(intent); |
|||
} |
|||
} |
|||
|
|||
private static final class BundleCursor extends MatrixCursor { |
|||
private Bundle mBundle; |
|||
|
|||
public BundleCursor(Bundle extras) { |
|||
super(new String[] {}, 0); |
|||
mBundle = extras; |
|||
} |
|||
|
|||
@Override |
|||
public Bundle getExtras() { |
|||
return mBundle; |
|||
} |
|||
|
|||
@Override |
|||
public Bundle respond(Bundle extras) { |
|||
mBundle = extras; |
|||
return mBundle; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,166 @@ |
|||
package com.example.wechatvideo.handler; |
|||
|
|||
import static com.example.wechatvideo.HookVideoData825Entity.TAG; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.classLoader; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.i; |
|||
|
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SearchClassUtils; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.LinkedList; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
import java.util.Random; |
|||
import java.util.UUID; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
import cn.iinti.sekiro3.business.api.interfaze.Action; |
|||
import cn.iinti.sekiro3.business.api.interfaze.AutoBind; |
|||
import cn.iinti.sekiro3.business.api.interfaze.RequestHandler; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroResponse; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
|
|||
/** |
|||
* 8.0.25 版本视频评论 |
|||
* post请求 |
|||
*/ |
|||
@Action("comments") |
|||
public class CommentListHandler implements RequestHandler { |
|||
|
|||
|
|||
@AutoBind("videoId") |
|||
private String videoId; // 视频id |
|||
@AutoBind("objectNonceId") |
|||
private String objectNonceId; // objectid |
|||
@AutoBind("userId") |
|||
private String userId; // 用户id |
|||
@AutoBind("lastBuffer") |
|||
private String lastBuffer; // 翻页 |
|||
@AutoBind("commentId") |
|||
private String commentId; //二级评论需要 |
|||
@AutoBind("displayId") |
|||
private String displayId; // 二级评论需要 |
|||
|
|||
public final static String api = "comments"; |
|||
|
|||
|
|||
/** |
|||
* 初始化所需要的对象 |
|||
* @return |
|||
*/ |
|||
public ArrayList<Object> setVideoModel(){ |
|||
ArrayList<Object> enume_objects = SearchClassUtils.getAllInstancesOfClass(XposedHelpers.findClass("com.tencent.mm.plugin.finder.feed.model.i", classLoader)); |
|||
|
|||
Log.d(TAG, "getFinderMixSearchPresenter: 枚举对象数量 : " + enume_objects.size()); |
|||
if(enume_objects.size() > 0){ |
|||
Random random = new Random(); |
|||
int randomInt = random.nextInt(enume_objects.size() - 1); |
|||
i = enume_objects.get(randomInt); |
|||
} |
|||
return enume_objects; |
|||
} |
|||
|
|||
/** |
|||
* 主动调用获得评论 |
|||
* 并返回入参 |
|||
* @param videoId |
|||
* @param objectNonceId |
|||
* @param userId |
|||
* @param lastBuffer |
|||
* @param commentId |
|||
* @param displayId |
|||
* @return |
|||
*/ |
|||
public JSONObject invoke(long videoId, String objectNonceId, String userId, String lastBuffer, |
|||
long commentId, long displayId){ |
|||
String uuidKey = api + UUID.randomUUID().toString().replaceAll("-", "") + api; // |
|||
Map<String, Object> map = new HashMap<>(); |
|||
|
|||
Map<String, Object> params = new HashMap<>(); // 存储需要的参数 |
|||
params.put("lastBuffer", lastBuffer); |
|||
params.put("commentId", commentId); |
|||
params.put("displayId", displayId); |
|||
params.put("videoId", videoId); |
|||
params.put("objectNonceId", objectNonceId); |
|||
params.put("userId", userId); |
|||
|
|||
map.put("params", params); // 初始化需要的参数 |
|||
map.put("data", new JSONObject()); // 初始化一个最终数据 |
|||
|
|||
StoredObject.setUuidKey(uuidKey); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
Random random = new Random(); |
|||
int randomInt = random.nextInt(10); |
|||
XposedHelpers.callMethod(i, "a", videoId, objectNonceId, randomInt, false, userId, 0); |
|||
return StoredObject.getData(uuidKey); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用作处理异常 |
|||
* @param _videoId |
|||
* @param _commentId |
|||
* @param _displayId |
|||
* @return |
|||
*/ |
|||
public JSONObject retry(long _videoId, long _commentId, long _displayId){ |
|||
JSONObject jsonObject = null; |
|||
ArrayList<Object> objects = setVideoModel(); |
|||
for (Object o:objects){ |
|||
i = o; |
|||
jsonObject = invoke(_videoId, objectNonceId, userId, lastBuffer, _commentId, _displayId); |
|||
Map<String, Object> data = (Map<String, Object>) jsonObject.get("data"); |
|||
Log.d(TAG, "循环测试 run: demo -> success is " + jsonObject.toJSONString()); |
|||
if(data.size() > 0){ |
|||
break; |
|||
} |
|||
} |
|||
return jsonObject; |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) { |
|||
JSONObject jsonObject = new JSONObject(); |
|||
long _videoId = (!Objects.equals(videoId, "")) ? Long.parseLong(videoId): 0; |
|||
long _commentId = (!Objects.equals(commentId, "")) ? Long.parseLong(commentId): 0; |
|||
long _displayId = (!Objects.equals(displayId, "")) ? Long.parseLong(displayId): 0; |
|||
|
|||
Log.d(TAG, String.format("获取的 videoId: %s; objectNonceId: %s; userId: %s; lastBuffer: %s; commentId: %s; displayId:%s;", _videoId, objectNonceId, userId, lastBuffer, _commentId, _displayId)); |
|||
if(i == null){ // 初始化对象 |
|||
setVideoModel(); |
|||
} |
|||
if(classLoader != null && i != null){ // 具备调用条件 |
|||
jsonObject = invoke(_videoId, objectNonceId, userId, lastBuffer, _commentId, _displayId); |
|||
Log.d(TAG, "handleRequest: jsonObject is => " + jsonObject.size()); |
|||
|
|||
Map<String, Object> data = (Map<String, Object>) jsonObject.get("data"); |
|||
if(data.size() == 0){ |
|||
jsonObject = retry(_videoId, _commentId, _displayId); |
|||
} |
|||
LinkedList<Object> ab = new LinkedList<>(); |
|||
// 由于修改了 返回数据则修改这块读取 |
|||
LinkedList<?> m = (LinkedList<?>) data.get("commentList"); |
|||
if(m != null){ |
|||
for(Object o:m){ |
|||
LinkedList<?> p = (LinkedList<?>) XposedHelpers.getObjectField(o, "levelTwoComment"); |
|||
Log.d(TAG, "handleRequest???: canshu levelComment is => " + p.size()); |
|||
if(p.size() > 0){ |
|||
Log.d(TAG, "handleRequest: ??????????"); |
|||
ab.add(p); |
|||
} |
|||
} |
|||
Log.d(TAG, "handleRequest: ad length is => " + ab.size()); |
|||
} |
|||
sekiroResponse.success(jsonObject); |
|||
Log.d(TAG, "run: success is " + jsonObject); |
|||
}else{ |
|||
sekiroResponse.success(jsonObject); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,103 @@ |
|||
package com.example.wechatvideo.handler; |
|||
|
|||
|
|||
import static com.example.wechatvideo.HookVideoData825Entity.FinderMixSearchPresenter; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.TAG; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.classLoader; |
|||
|
|||
import android.os.Handler; |
|||
import android.os.Looper; |
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SearchClassUtils; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
import cn.iinti.sekiro3.business.api.interfaze.Action; |
|||
import cn.iinti.sekiro3.business.api.interfaze.RequestHandler; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroResponse; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
|
|||
/** |
|||
* 8.0.25版本搜索 |
|||
*/ |
|||
@Action("search825") |
|||
public class SearchUser825Handler implements RequestHandler { |
|||
|
|||
public static String KEY_WORD = ""; // 用来记录上下文的keyword |
|||
public final static Map<Integer, String> sorts = new HashMap<Integer,String>(){{ |
|||
put(2, "综合"); |
|||
put(1, "实时"); |
|||
}}; |
|||
|
|||
|
|||
/** |
|||
* 全局遍历 获取keyword关键词 |
|||
*/ |
|||
public void setFinderMixSearchPresenter(){ |
|||
ArrayList<Object> enume_objects = SearchClassUtils.getAllInstancesOfClass(XposedHelpers.findClass("com.tencent.mm.plugin.finder.search.FinderMixSearchPresenter", classLoader)); |
|||
Log.d(TAG, "getFinderMixSearchPresenter: 枚举对象 : " + enume_objects.size()); |
|||
if(enume_objects.size() > 0){ |
|||
FinderMixSearchPresenter = enume_objects.get(0); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据 isNext 来确定是重新搜索 还是翻页 |
|||
* @param keyword |
|||
* @param isNext |
|||
* @param type: 综合是2、最新是1 |
|||
* @return |
|||
*/ |
|||
public JSONObject invoke(String keyword, int isNext, int type){ |
|||
String uuidKey = UUID.randomUUID().toString().replaceAll("-", ""); // 32位 |
|||
Map<String, Object> map = new HashMap<>(); |
|||
map.put("search_order", sorts.get(type)); // 把字段加进来 |
|||
if(isNext == 1){ // 翻页 |
|||
StoredObject.setUuidKey(uuidKey); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(FinderMixSearchPresenter, "aCK", type); |
|||
}else{ // 首页 |
|||
StoredObject.setUuidKey(uuidKey); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(FinderMixSearchPresenter, "a", keyword, 1, false, null, 0); |
|||
KEY_WORD = keyword; |
|||
} |
|||
return StoredObject.getData(uuidKey); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) { |
|||
// 切换至主线程中进行 |
|||
new Handler(Looper.getMainLooper()).post(new Runnable() { |
|||
|
|||
@Override |
|||
public void run() { |
|||
Map<String, String> result = null; |
|||
JSONObject jsonObject = null; |
|||
String keyword = (String) sekiroRequest.get("keyword"); |
|||
int isNext = Integer.parseInt((String) sekiroRequest.get("is_next")); |
|||
int type = Integer.parseInt((String) sekiroRequest.get("type"));; |
|||
Log.d(TAG, String.format("获取的 keyword: %s; 是否翻页: %s; 原始keyword: %s; 排序为:%s", keyword, isNext, KEY_WORD, type)); |
|||
if(FinderMixSearchPresenter == null){ // 初始化搜索对象 |
|||
setFinderMixSearchPresenter(); |
|||
} |
|||
if(classLoader != null && FinderMixSearchPresenter != null){ |
|||
jsonObject = invoke(keyword, isNext, type); |
|||
Log.d(TAG, "handleRequest: " + sekiroRequest.getSekiroClient().getClientId() + " res is " + jsonObject); |
|||
sekiroResponse.success(jsonObject); |
|||
}else{ |
|||
sekiroResponse.success(JSONObject.toJSONString(result)); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,104 @@ |
|||
package com.example.wechatvideo.handler; |
|||
|
|||
|
|||
import static com.example.wechatvideo.HookVideoDataEntity.FinderMixSearchPresenter; |
|||
import static com.example.wechatvideo.HookVideoDataEntity.TAG; |
|||
import static com.example.wechatvideo.HookVideoDataEntity.classLoader; |
|||
|
|||
import android.os.Handler; |
|||
import android.os.Looper; |
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SearchClassUtils; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
import cn.iinti.sekiro3.business.api.interfaze.Action; |
|||
import cn.iinti.sekiro3.business.api.interfaze.RequestHandler; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroResponse; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
|
|||
|
|||
/** |
|||
* 微信视频号搜索模块 |
|||
* 用户 && 视频 |
|||
* demo: http://localhost:5612/business/invoke?group=vxin&action=search&keyword=比亚迪&is_next=0 |
|||
*/ |
|||
@Action("search") |
|||
public class SearchUserHandler implements RequestHandler { |
|||
|
|||
|
|||
public static String KEY_WORD = ""; // 用来记录上下文的keyword |
|||
|
|||
|
|||
/** |
|||
* 全局遍历 获取keyword关键词 |
|||
*/ |
|||
public void setFinderMixSearchPresenter(){ |
|||
ArrayList<Object> enume_objects = SearchClassUtils.getAllInstancesOfClass(XposedHelpers.findClass("com.tencent.mm.plugin.finder.search.FinderMixSearchPresenter", classLoader)); |
|||
Log.d(TAG, "getFinderMixSearchPresenter: 枚举对象 : " + enume_objects.size()); |
|||
if(enume_objects.size() > 0){ |
|||
FinderMixSearchPresenter = enume_objects.get(0); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 根据 isNext 来确定是重新搜索 还是翻页 |
|||
* @param keyword |
|||
* @param isNext |
|||
* @return |
|||
*/ |
|||
public JSONObject invoke(String keyword, int isNext){ |
|||
String uuidKey = UUID.randomUUID().toString().replaceAll("-", ""); // 32位 |
|||
Map<String, Object> map = new HashMap<>(); |
|||
if(isNext == 1){ |
|||
StoredObject.setUuidKey(uuidKey); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(FinderMixSearchPresenter, "cNZ"); |
|||
}else{ |
|||
StoredObject.setUuidKey(uuidKey); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(FinderMixSearchPresenter, "anp", keyword); |
|||
KEY_WORD = keyword; |
|||
} |
|||
return StoredObject.getData(uuidKey); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) { |
|||
|
|||
// 切换至主线程中进行 |
|||
new Handler(Looper.getMainLooper()).post(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
Map<String, String> result = null; |
|||
JSONObject jsonObject = null; |
|||
String keyword = (String) sekiroRequest.get("keyword"); |
|||
int isNext = Integer.parseInt((String) sekiroRequest.get("is_next")); |
|||
Log.d(TAG, String.format("获取的 keyword: %s; 是否翻页: %s; 原始keyword: %s", keyword, isNext, KEY_WORD)); |
|||
if(FinderMixSearchPresenter == null){ // 初始化搜索对象 |
|||
setFinderMixSearchPresenter(); |
|||
} |
|||
if(classLoader != null && FinderMixSearchPresenter != null){ |
|||
jsonObject = invoke(keyword, isNext); |
|||
Log.d(TAG, "handleRequest: " + sekiroRequest.getSekiroClient().getClientId() + " res is " + jsonObject); |
|||
sekiroResponse.success(jsonObject); |
|||
}else{ |
|||
sekiroResponse.success(JSONObject.toJSONString(result)); |
|||
|
|||
} |
|||
} |
|||
}); |
|||
|
|||
|
|||
|
|||
} |
|||
} |
@ -0,0 +1,125 @@ |
|||
package com.example.wechatvideo.handler; |
|||
|
|||
import static com.example.wechatvideo.HookVideoData825Entity.FEED_PROFILE; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.TAG; |
|||
import static com.example.wechatvideo.HookVideoData825Entity.classLoader; |
|||
|
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SearchClassUtils; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
import cn.iinti.sekiro3.business.api.interfaze.Action; |
|||
import cn.iinti.sekiro3.business.api.interfaze.RequestHandler; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroResponse; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
|
|||
/** |
|||
* 8.0.25 版本用户主页 |
|||
*/ |
|||
@Action("user825") |
|||
public class UserVideo825Handler implements RequestHandler { |
|||
|
|||
|
|||
/** |
|||
* 全局过滤 一个枚举对象 用于构建 作者对象 |
|||
*/ |
|||
public void setProFile(){ |
|||
ArrayList<Object> enume_objects = SearchClassUtils.getAllInstancesOfClass(XposedHelpers.findClass("com.tencent.mm.plugin.finder.feed.model.internal.g", classLoader)); |
|||
Log.d(TAG, "afterHookedMethod: 枚举对象 : " + enume_objects.size()); |
|||
for(Object oi: enume_objects){ |
|||
Log.d(TAG, "InitialObject: 枚举类初始化 " + oi); |
|||
if("FEED_PROFILE".equals(String.valueOf(oi))){ |
|||
FEED_PROFILE = oi; |
|||
Log.d(TAG, "InitialObject: 构造参数结束: "+ oi); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 构造一个对目标用户的作者的对象 |
|||
* 只构造一次过一会儿貌似就回收了 |
|||
* 因此每次请求构造一个 |
|||
* @param username |
|||
* @return |
|||
*/ |
|||
public Object setProfileFeedLoader(String username) throws Exception { |
|||
Class<?> FinderProfileFeedLoaderClazz = XposedHelpers.findClass("com.tencent.mm.plugin.finder.feed.model.FinderProfileFeedLoader", classLoader); |
|||
Class<?> auu_clazz = XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.ccz", classLoader); |
|||
Object targetObject = XposedHelpers.newInstance(FinderProfileFeedLoaderClazz, FEED_PROFILE, username, auu_clazz.newInstance(), false, false); |
|||
Log.d(TAG, "setProfileFeedLoader: 目标对象构造完毕 => " + username); |
|||
return targetObject; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用户首页 |
|||
* @param username |
|||
* @return |
|||
*/ |
|||
public JSONObject invoke(String username) throws Exception { |
|||
Object o = setProfileFeedLoader(username); |
|||
String uuidKey = "0"; |
|||
StoredObject.setUuidKey(uuidKey); |
|||
Map<String, Object> map = new HashMap<>(); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(o, "requestRefresh"); |
|||
return StoredObject.getData(uuidKey); // 初始化异步消息捕获 |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用户翻页 |
|||
* @param username 目标用户数据 |
|||
* @param offset 偏移量 默认为long |
|||
*/ |
|||
public JSONObject invoke(String username, String offset) throws Exception { |
|||
Object o = setProfileFeedLoader(username); |
|||
|
|||
StoredObject.setUuidKey(offset); // 变为offset |
|||
Map<String, Object> map = new HashMap<>(); |
|||
StoredObject.map1.put(offset, map); |
|||
XposedHelpers.callMethod(o, "requestLoadMore", false); |
|||
return StoredObject.getData(offset); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) { |
|||
Map<String, String> result = null; |
|||
JSONObject jsonObject = null; |
|||
String username = (String) sekiroRequest.get("user_id"); |
|||
String offset = (String) sekiroRequest.get("offset"); |
|||
Log.d(TAG, String.format("获取的 username: %s; 获取的偏移量: %s", username, offset)); |
|||
Log.d(TAG, "handleRequest: classLoader => " + classLoader); |
|||
Log.d(TAG, "handleRequest: FEED_PROFILE => " + FEED_PROFILE); |
|||
if(FEED_PROFILE == null){ |
|||
setProFile(); // 初始化feed |
|||
} |
|||
|
|||
if(classLoader != null && FEED_PROFILE != null){ |
|||
try { |
|||
if("0".equals(offset)){ |
|||
jsonObject = invoke(username); // 第一页 |
|||
}else{ |
|||
jsonObject = invoke(username, offset); // 第n页 |
|||
} |
|||
Log.d(TAG, "handleRequest: " + sekiroRequest.getSekiroClient().getClientId() + " res is " + jsonObject); |
|||
}catch (Exception e){ |
|||
throw new RuntimeException(e); |
|||
} |
|||
sekiroResponse.success(jsonObject); |
|||
}else { |
|||
sekiroResponse.success(JSONObject.toJSONString(result)); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,129 @@ |
|||
package com.example.wechatvideo.handler; |
|||
|
|||
|
|||
import static com.example.wechatvideo.HookVideoDataEntity.FEED_PROFILE; |
|||
import static com.example.wechatvideo.HookVideoDataEntity.TAG; |
|||
import static com.example.wechatvideo.HookVideoDataEntity.classLoader; |
|||
|
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.model.StoredObject; |
|||
import com.example.wechatvideo.utils.SearchClassUtils; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
import cn.iinti.sekiro3.business.api.interfaze.Action; |
|||
import cn.iinti.sekiro3.business.api.interfaze.RequestHandler; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroRequest; |
|||
import cn.iinti.sekiro3.business.api.interfaze.SekiroResponse; |
|||
import de.robv.android.xposed.XposedHelpers; |
|||
|
|||
|
|||
/** |
|||
* 微信视频号 用户主页视频获取 |
|||
* demo: http://localhost:5612/business/invoke?group=vxin&action=user&user_id=v2_060000231003b20faec8c6ea891ac4d5cb02e531b077c2083a8c3b90f2e4618deb0754a398a2@finder&offset=0 |
|||
*/ |
|||
@Action("user") |
|||
public class UserVideoHandler implements RequestHandler { |
|||
|
|||
|
|||
/** |
|||
* 构造一个对目标用户的作者的对象 |
|||
* 只构造一次过一会儿貌似就回收了 |
|||
* 因此每次请求构造一个 |
|||
* @param username |
|||
* @return |
|||
*/ |
|||
public Object setProfileFeedLoader(String username) throws Exception { |
|||
Class<?> FinderProfileFeedLoaderClazz = XposedHelpers.findClass("com.tencent.mm.plugin.finder.feed.model.FinderProfileFeedLoader", classLoader); |
|||
Class<?> auu_clazz = XposedHelpers.findClass("com.tencent.mm.protocal.protobuf.auu", classLoader); |
|||
Object targetObject = XposedHelpers.newInstance(FinderProfileFeedLoaderClazz, FEED_PROFILE, username, auu_clazz.newInstance()); |
|||
Log.d(TAG, "setProfileFeedLoader: 目标对象构造完毕 => " + username); |
|||
return targetObject; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用户首页 |
|||
* @param username |
|||
* @return |
|||
*/ |
|||
public JSONObject invoke(String username) throws Exception { |
|||
Object o = setProfileFeedLoader(username); |
|||
|
|||
String uuidKey = "0"; |
|||
StoredObject.setUuidKey(uuidKey); |
|||
Map<String, Object> map = new HashMap<>(); |
|||
StoredObject.map1.put(uuidKey, map); |
|||
XposedHelpers.callMethod(o, "requestRefresh"); |
|||
return StoredObject.getData(uuidKey); // 初始化异步消息捕获 |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用户翻页 |
|||
* @param username 目标用户数据 |
|||
* @param offset 偏移量 默认为long |
|||
*/ |
|||
public JSONObject invoke(String username, String offset) throws Exception { |
|||
Object o = setProfileFeedLoader(username); |
|||
|
|||
StoredObject.setUuidKey(offset); // 变为offset |
|||
Map<String, Object> map = new HashMap<>(); |
|||
StoredObject.map1.put(offset, map); |
|||
XposedHelpers.callMethod(o, "requestLoadMore"); |
|||
|
|||
return StoredObject.getData(offset); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 全局过滤 一个枚举对象 用于构建 作者对象 |
|||
*/ |
|||
public void setProFile(){ |
|||
ArrayList<Object> enume_objects = SearchClassUtils.getAllInstancesOfClass(XposedHelpers.findClass("com.tencent.mm.plugin.finder.feed.model.internal.e", classLoader)); |
|||
Log.d(TAG, "afterHookedMethod: 枚举对象 : " + enume_objects.size()); |
|||
for(Object oi: enume_objects){ |
|||
Log.d(TAG, "InitialObject: 枚举类初始化 " + oi); |
|||
if("FEED_PROFILE".equals(String.valueOf(oi))){ |
|||
FEED_PROFILE = oi; |
|||
Log.d(TAG, "InitialObject: 构造参数结束: "+ oi); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) { |
|||
Map<String, String> result = null; |
|||
JSONObject jsonObject = null; |
|||
String username = (String) sekiroRequest.get("user_id"); |
|||
String offset = (String) sekiroRequest.get("offset"); |
|||
Log.d(TAG, String.format("获取的 username: %s; 获取的偏移量: %s", username, offset)); |
|||
Log.d(TAG, "handleRequest: classLoader => " + classLoader); |
|||
Log.d(TAG, "handleRequest: FEED_PROFILE => " + FEED_PROFILE); |
|||
if(FEED_PROFILE == null){ |
|||
setProFile(); // 初始化feed |
|||
} |
|||
|
|||
if(classLoader != null && FEED_PROFILE != null){ |
|||
try { |
|||
if("0".equals(offset)){ |
|||
jsonObject = invoke(username); // 第一页 |
|||
}else{ |
|||
jsonObject = invoke(username, offset); // 第n页 |
|||
} |
|||
Log.d(TAG, "handleRequest: " + sekiroRequest.getSekiroClient().getClientId() + " res is " + jsonObject); |
|||
}catch (Exception e){ |
|||
throw new RuntimeException(e); |
|||
} |
|||
sekiroResponse.success(jsonObject); |
|||
}else { |
|||
sekiroResponse.success(JSONObject.toJSONString(result)); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
package com.example.wechatvideo.model; |
|||
|
|||
import com.google.common.collect.Maps; |
|||
|
|||
import java.util.Map; |
|||
|
|||
import cn.iinti.sekiro3.business.api.fastjson.JSONObject; |
|||
|
|||
|
|||
/** |
|||
* 异步拦截数据 |
|||
*/ |
|||
public class StoredObject { |
|||
|
|||
public static Map<String, Map<String, Object>> map1 = Maps.newConcurrentMap(); // 控制跨线程变量 |
|||
static String uuidKey; |
|||
|
|||
public static String getUuidKey() { |
|||
return uuidKey; |
|||
} |
|||
|
|||
public static void setUuidKey(String uuidKey) { |
|||
StoredObject.uuidKey = uuidKey; |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 视频号数据拦截 |
|||
* 获得目标uuid中的结果 |
|||
* @param uuidKey |
|||
* @return |
|||
*/ |
|||
public static JSONObject getData(String uuidKey){ |
|||
JSONObject jsonObject = new JSONObject(); |
|||
try { |
|||
synchronized (uuidKey){ // 等待锁结束 |
|||
uuidKey.wait(5000); // 5s可能不够 |
|||
Map<String, Object> removeMap = map1.remove(uuidKey); |
|||
jsonObject = new JSONObject(removeMap); |
|||
} |
|||
}catch (InterruptedException e){ |
|||
e.printStackTrace(); |
|||
jsonObject.put("msg", "timeOut"); |
|||
}finally { |
|||
setUuidKey(null); |
|||
} |
|||
|
|||
return jsonObject; |
|||
} |
|||
|
|||
/** |
|||
* 获得目标uuid中的结果 |
|||
* 改 => 嵌套map |
|||
* @param uuidKey |
|||
* @return |
|||
*/ |
|||
public static JSONObject getData1(String uuidKey){ |
|||
JSONObject jsonObject = new JSONObject(); |
|||
try { |
|||
synchronized (uuidKey){ // 等待锁结束 |
|||
uuidKey.wait(5000); // 5s可能不够 修改为十秒 |
|||
Map<String, Object> removeMap = map1.remove(uuidKey); |
|||
Map<String, Object> data = (Map<String, Object>) removeMap.get("data"); // 这是有数据的 |
|||
jsonObject = new JSONObject(data); |
|||
return jsonObject; |
|||
} |
|||
}catch (InterruptedException e){ |
|||
e.printStackTrace(); |
|||
jsonObject.put("msg", "timeOut"); |
|||
}finally { |
|||
setUuidKey(null); |
|||
} |
|||
|
|||
return jsonObject; |
|||
} |
|||
} |
@ -0,0 +1,146 @@ |
|||
package com.example.wechatvideo.utils; |
|||
|
|||
import android.annotation.TargetApi; |
|||
|
|||
import java.lang.reflect.Method; |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 安卓内存漫游 |
|||
* 源码参考: |
|||
* http://androidxref.com/9.0.0_r3/s?refs=ClassD&project=art |
|||
*/ |
|||
public class SearchClassUtils { |
|||
|
|||
private static final Method startMethodTracingMethod; |
|||
private static final Method stopMethodTracingMethod; |
|||
private static final Method getMethodTracingModeMethod; |
|||
private static final Method getRuntimeStatMethod; |
|||
private static final Method getRuntimeStatsMethod; |
|||
private static final Method countInstancesOfClassMethod; |
|||
private static final Method countInstancesOfClassesMethod; |
|||
private static Method getInstancesOfClassesMethod; |
|||
|
|||
static { |
|||
try { |
|||
Class<?> c = Class.forName("dalvik.system.VMDebug"); |
|||
startMethodTracingMethod = c.getDeclaredMethod("startMethodTracing", String.class, |
|||
Integer.TYPE, Integer.TYPE, Boolean.TYPE, Integer.TYPE); |
|||
stopMethodTracingMethod = c.getDeclaredMethod("stopMethodTracing"); |
|||
getMethodTracingModeMethod = c.getDeclaredMethod("getMethodTracingMode"); |
|||
getRuntimeStatMethod = c.getDeclaredMethod("getRuntimeStat", String.class); |
|||
getRuntimeStatsMethod = c.getDeclaredMethod("getRuntimeStats"); |
|||
|
|||
countInstancesOfClassMethod = c.getDeclaredMethod("countInstancesOfClass", |
|||
Class.class, Boolean.TYPE); |
|||
|
|||
|
|||
countInstancesOfClassesMethod = c.getDeclaredMethod("countInstancesOfClasses", |
|||
Class[].class, Boolean.TYPE); |
|||
|
|||
// android 9.0以上才有这个方法 |
|||
if(android.os.Build.VERSION.SDK_INT >= 28) { |
|||
getInstancesOfClassesMethod = c.getDeclaredMethod("getInstancesOfClasses", |
|||
Class[].class, Boolean.TYPE); |
|||
} |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据Class 获取当前进程该class 全部的实例 |
|||
* @param clazz |
|||
* @return |
|||
*/ |
|||
@TargetApi(28) |
|||
public static ArrayList<Object> getAllInstancesOfClass(Class clazz) { return getClassInstancesOfCurrentThread(clazz, false); } |
|||
|
|||
/** |
|||
* 根据Class 获取当前进程该class 全部的实例 |
|||
* @param clazz:当前class |
|||
* @param assignable: 是否包含子类的实例 |
|||
* @return |
|||
*/ |
|||
@TargetApi(28) |
|||
private static synchronized ArrayList<Object> getClassInstancesOfCurrentThread(Class clazz, Boolean assignable){ |
|||
ArrayList<Object> result = null; |
|||
|
|||
try { |
|||
Object[][] instancesOfClasses = getInstancesOfClasses(new Class[]{clazz}, assignable); |
|||
if (instancesOfClasses != null){ |
|||
result = new ArrayList<>(); |
|||
|
|||
for (Object[] instancesOfClass: instancesOfClasses){ |
|||
System.out.println("实体个数 ~~ " + instancesOfClass.length); |
|||
result.addAll(Arrays.asList(instancesOfClass)); |
|||
// result.add(instancesOfClass); |
|||
} |
|||
} |
|||
|
|||
}catch (Throwable e){ |
|||
e.printStackTrace(); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 获得所有class的实例 |
|||
* android 9 以上 |
|||
* @param classes:当前class集合 |
|||
* @param assignable:是否包含子类的实例 |
|||
* @return |
|||
* @throws Exception |
|||
*/ |
|||
@TargetApi(28) |
|||
private static Object[][] getInstancesOfClasses(Class<?>[] classes, boolean assignable)throws Exception { |
|||
return (Object[][]) getInstancesOfClassesMethod.invoke(null, new Object[]{classes, assignable}); |
|||
} |
|||
|
|||
|
|||
public static void startMethodTracing(String filename, int bufferSize, int flags, |
|||
boolean samplingEnabled, int intervalUs) throws Exception { |
|||
startMethodTracingMethod.invoke(null, filename, bufferSize, flags, samplingEnabled, intervalUs); |
|||
} |
|||
|
|||
public static void stopMethodTracing() throws Exception { |
|||
stopMethodTracingMethod.invoke(null); |
|||
} |
|||
|
|||
public static int getMethodTracingMode() throws Exception { |
|||
return (int) getMethodTracingModeMethod.invoke(null); |
|||
} |
|||
|
|||
/** |
|||
* String gc_count = VMDebug.getRuntimeStat("art.gc.gc-count"); |
|||
* String gc_time = VMDebug.getRuntimeStat("art.gc.gc-time"); |
|||
* String bytes_allocated = VMDebug.getRuntimeStat("art.gc.bytes-allocated"); |
|||
* String bytes_freed = VMDebug.getRuntimeStat("art.gc.bytes-freed"); |
|||
* String blocking_gc_count = VMDebug.getRuntimeStat("art.gc.blocking-gc-count"); |
|||
* String blocking_gc_time = VMDebug.getRuntimeStat("art.gc.blocking-gc-time"); |
|||
* String gc_count_rate_histogram = VMDebug.getRuntimeStat("art.gc.gc-count-rate-histogram"); |
|||
* String blocking_gc_count_rate_histogram =VMDebug.getRuntimeStat("art.gc.gc-count-rate-histogram"); |
|||
*/ |
|||
public static String getRuntimeStat(String statName) throws Exception { |
|||
return (String) getRuntimeStatMethod.invoke(null, statName); |
|||
} |
|||
|
|||
/** |
|||
* 获取当前进程的状态信息 |
|||
*/ |
|||
public static Map<String, String> getRuntimeStats() throws Exception { |
|||
return (Map<String, String>) getRuntimeStatsMethod.invoke(null); |
|||
} |
|||
|
|||
public static long countInstancesofClass(Class<?> c, boolean assignable) throws Exception { |
|||
return (long) countInstancesOfClassMethod.invoke(null, new Object[]{c, assignable}); |
|||
} |
|||
|
|||
public static long[] countInstancesofClasses(Class<?>[] classes, boolean assignable) throws Exception { |
|||
return (long[]) countInstancesOfClassesMethod.invoke(null, new Object[]{classes, assignable}); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,179 @@ |
|||
package com.example.wechatvideo.utils; |
|||
|
|||
import android.content.ContentResolver; |
|||
import android.content.Context; |
|||
import android.content.SharedPreferences; |
|||
import android.database.Cursor; |
|||
import android.net.Uri; |
|||
import android.os.Bundle; |
|||
import android.util.Log; |
|||
|
|||
import com.example.wechatvideo.content.MultiprocessSharedPreferences; |
|||
|
|||
|
|||
public class SpUtil { |
|||
// 声明为 static volatile,会迫使线程每次读取时作为一个全局变量读取 |
|||
private static volatile SpUtil spUtil = null; |
|||
private static SharedPreferences sp = null; |
|||
private static final String XML_NAME = "config"; |
|||
|
|||
private SpUtil(Context context) { |
|||
// 上下文、配置名称(data/data/shared_prefs )、 |
|||
sp = MultiprocessSharedPreferences. |
|||
getSharedPreferences(context, XML_NAME, Context.MODE_PRIVATE); |
|||
// sp = |
|||
} |
|||
|
|||
public static SpUtil newInstance(Context context) { |
|||
if(spUtil == null){ |
|||
synchronized (SpUtil.class){ |
|||
while (spUtil == null){ |
|||
spUtil = new SpUtil(context); |
|||
} |
|||
} |
|||
} |
|||
return spUtil; |
|||
} |
|||
|
|||
/** |
|||
* 存储字符串 |
|||
* @param context |
|||
* @param key |
|||
* @param value |
|||
*/ |
|||
public static void putString(Context context, String key, String value){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
sp.edit().putString(key, value).commit(); |
|||
} |
|||
|
|||
/** |
|||
* 获取字符串 |
|||
* @param context |
|||
* @param key |
|||
* @param defValue |
|||
* @return |
|||
*/ |
|||
public static String getString(Context context, String key, String defValue){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
return sp.getString(key, defValue); |
|||
} |
|||
|
|||
/** |
|||
* 存储boolean |
|||
* @param context |
|||
* @param key |
|||
* @param value |
|||
*/ |
|||
public static void putBoolean(Context context, String key, boolean value){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
sp.edit().putBoolean(key, value).commit(); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取boolean |
|||
* @param context |
|||
* @param key |
|||
* @param defValue |
|||
* @return |
|||
*/ |
|||
public static boolean getBoolean(Context context,String key,boolean defValue){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
return sp.getBoolean(key, defValue); |
|||
} |
|||
|
|||
/** |
|||
* 存储int |
|||
* @param context |
|||
* @param key |
|||
* @param defValue |
|||
*/ |
|||
public static void putInt(Context context,String key, int defValue){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
sp.edit().putInt(key, defValue).commit(); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取int |
|||
* @param context |
|||
* @param key |
|||
* @param defValue |
|||
* @return |
|||
*/ |
|||
public static int getInt(Context context,String key,int defValue){ |
|||
//(存储节点文件名称,读写方式) |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
return sp.getInt(key, defValue); |
|||
} |
|||
|
|||
/** |
|||
* 删除节点 |
|||
* @param context |
|||
* @param key |
|||
*/ |
|||
public static void remove(Context context, String key) { |
|||
if(spUtil == null){ |
|||
spUtil = newInstance(context); |
|||
} |
|||
sp.edit().remove(key).commit(); |
|||
} |
|||
|
|||
/** |
|||
* 根据类型 通过provider 返回结果 |
|||
* @param context 传递当前上下文 |
|||
* @param key 传递key |
|||
* @param objType 存储key的类型 |
|||
* @return 对应key-value |
|||
*/ |
|||
public static Object readObjectByProvider(Context context, String key, Class objType){ |
|||
StringBuilder stringBuilder = new StringBuilder("content://"); |
|||
stringBuilder.append("com.example.wechatvideo.provider/"); // 包名 |
|||
stringBuilder.append(XML_NAME + "/"); |
|||
//(存储节点文件名称,读写方式) 追加方式 参考MultiprocessSharedPreferences中 |
|||
// eg. content://com.px.xpcrossprocess.provider/config/getString 获取config.xml中的字符串类型的 |
|||
// TODO |
|||
if(objType.getName() == "java.lang.String"){ |
|||
stringBuilder.append("getString"); |
|||
}else if(objType.getName() == "java.lang.Boolean"){ |
|||
stringBuilder.append("getBoolean"); |
|||
}else if(objType.getName() == "int"){ |
|||
stringBuilder.append("getInt"); |
|||
} |
|||
|
|||
|
|||
Uri uri = Uri.parse(stringBuilder.toString()); |
|||
ContentResolver provider = context.getContentResolver(); |
|||
Cursor cursorTid; |
|||
try { |
|||
//第一个参数是否是安全模式,一般为0。第二个参数读取的key,第三个参数defaultValue |
|||
String[] selectionArgs = {"0", key, ""}; |
|||
cursorTid = provider.query(uri, null, null, selectionArgs, null); |
|||
Bundle extras = cursorTid.getExtras(); |
|||
Object res = extras.get("value"); |
|||
return res; |
|||
}catch (Exception e){ |
|||
Log.e("crossprocess",e.getMessage()+"\t"+e.toString()); |
|||
e.printStackTrace(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:aapt="http://schemas.android.com/aapt" |
|||
android:width="108dp" |
|||
android:height="108dp" |
|||
android:viewportWidth="108" |
|||
android:viewportHeight="108"> |
|||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> |
|||
<aapt:attr name="android:fillColor"> |
|||
<gradient |
|||
android:endX="85.84757" |
|||
android:endY="92.4963" |
|||
android:startX="42.9492" |
|||
android:startY="49.59793" |
|||
android:type="linear"> |
|||
<item |
|||
android:color="#44000000" |
|||
android:offset="0.0" /> |
|||
<item |
|||
android:color="#00000000" |
|||
android:offset="1.0" /> |
|||
</gradient> |
|||
</aapt:attr> |
|||
</path> |
|||
<path |
|||
android:fillColor="#FFFFFF" |
|||
android:fillType="nonZero" |
|||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" |
|||
android:strokeWidth="1" |
|||
android:strokeColor="#00000000" /> |
|||
</vector> |
@ -0,0 +1,170 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="108dp" |
|||
android:height="108dp" |
|||
android:viewportWidth="108" |
|||
android:viewportHeight="108"> |
|||
<path |
|||
android:fillColor="#3DDC84" |
|||
android:pathData="M0,0h108v108h-108z" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M9,0L9,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,0L19,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M29,0L29,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M39,0L39,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M49,0L49,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M59,0L59,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M69,0L69,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M79,0L79,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M89,0L89,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M99,0L99,108" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,9L108,9" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,19L108,19" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,29L108,29" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,39L108,39" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,49L108,49" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,59L108,59" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,69L108,69" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,79L108,79" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,89L108,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M0,99L108,99" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,29L89,29" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,39L89,39" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,49L89,49" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,59L89,59" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,69L89,69" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M19,79L89,79" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M29,19L29,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M39,19L39,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M49,19L49,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M59,19L59,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M69,19L69,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
<path |
|||
android:fillColor="#00000000" |
|||
android:pathData="M79,19L79,89" |
|||
android:strokeWidth="0.8" |
|||
android:strokeColor="#33FFFFFF" /> |
|||
</vector> |
@ -0,0 +1,30 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:aapt="http://schemas.android.com/aapt" |
|||
android:width="108dp" |
|||
android:height="108dp" |
|||
android:viewportWidth="108" |
|||
android:viewportHeight="108"> |
|||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> |
|||
<aapt:attr name="android:fillColor"> |
|||
<gradient |
|||
android:endX="85.84757" |
|||
android:endY="92.4963" |
|||
android:startX="42.9492" |
|||
android:startY="49.59793" |
|||
android:type="linear"> |
|||
<item |
|||
android:color="#44000000" |
|||
android:offset="0.0" /> |
|||
<item |
|||
android:color="#00000000" |
|||
android:offset="1.0" /> |
|||
</gradient> |
|||
</aapt:attr> |
|||
</path> |
|||
<path |
|||
android:fillColor="#FFFFFF" |
|||
android:fillType="nonZero" |
|||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" |
|||
android:strokeWidth="1" |
|||
android:strokeColor="#00000000" /> |
|||
</vector> |
@ -0,0 +1,74 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<vector |
|||
android:height="108dp" |
|||
android:width="108dp" |
|||
android:viewportHeight="108" |
|||
android:viewportWidth="108" |
|||
xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<path android:fillColor="#3DDC84" |
|||
android:pathData="M0,0h108v108h-108z"/> |
|||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89" |
|||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|||
</vector> |
@ -0,0 +1,118 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:orientation="vertical" |
|||
tools:context=".MainActivity"> |
|||
<ScrollView |
|||
android:layout_width="fill_parent" |
|||
android:layout_height="wrap_content" > |
|||
|
|||
<LinearLayout |
|||
android:layout_width="fill_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" > |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="50dp" |
|||
android:layout_gravity="center" |
|||
android:gravity="center" |
|||
android:text="RPC配置信息" /> |
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="设备调度编号: "/> |
|||
|
|||
<EditText |
|||
android:id="@+id/set_phone" |
|||
android:singleLine="true" |
|||
android:hint="数字或字母" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
</LinearLayout> |
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="服务调度地址: "/> |
|||
<EditText |
|||
android:id="@+id/set_web" |
|||
android:singleLine="true" |
|||
android:hint="ip" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
|
|||
<Button |
|||
android:id="@+id/btn_update" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="20dp" |
|||
android:text="更新" /> |
|||
|
|||
<LinearLayout |
|||
android:layout_marginTop="20dp" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="30dp"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="设备调度编号: "/> |
|||
<TextView |
|||
android:id="@+id/get_phone" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
</LinearLayout> |
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="30dp"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="服务调度地址: "/> |
|||
<TextView |
|||
android:id="@+id/get_web" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
</LinearLayout> |
|||
|
|||
|
|||
|
|||
<LinearLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="30dp"> |
|||
|
|||
<TextView |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="调度组名为: "/> |
|||
<TextView |
|||
android:id="@+id/get_group" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
</LinearLayout> |
|||
|
|||
|
|||
</LinearLayout> |
|||
</ScrollView> |
|||
|
|||
|
|||
</LinearLayout> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/ic_launcher_background" /> |
|||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
|||
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> |
|||
</adaptive-icon> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/ic_launcher_background" /> |
|||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
|||
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> |
|||
</adaptive-icon> |
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/new_icon_background"/> |
|||
<foreground android:drawable="@mipmap/new_icon_foreground"/> |
|||
</adaptive-icon> |
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/new_icon_background"/> |
|||
<foreground android:drawable="@mipmap/new_icon_foreground"/> |
|||
</adaptive-icon> |
@ -0,0 +1,16 @@ |
|||
<resources xmlns:tools="http://schemas.android.com/tools"> |
|||
<!-- Base application theme. --> |
|||
<style name="Theme.Xp_module" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> |
|||
<!-- Primary brand color. --> |
|||
<item name="colorPrimary">@color/purple_200</item> |
|||
<item name="colorPrimaryVariant">@color/purple_700</item> |
|||
<item name="colorOnPrimary">@color/black</item> |
|||
<!-- Secondary brand color. --> |
|||
<item name="colorSecondary">@color/teal_200</item> |
|||
<item name="colorSecondaryVariant">@color/teal_200</item> |
|||
<item name="colorOnSecondary">@color/black</item> |
|||
<!-- Status bar color. --> |
|||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> |
|||
<!-- Customize your theme here. --> |
|||
</style> |
|||
</resources> |
@ -0,0 +1,10 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="purple_200">#FFBB86FC</color> |
|||
<color name="purple_500">#FF6200EE</color> |
|||
<color name="purple_700">#FF3700B3</color> |
|||
<color name="teal_200">#FF03DAC5</color> |
|||
<color name="teal_700">#FF018786</color> |
|||
<color name="black">#FF000000</color> |
|||
<color name="white">#FFFFFFFF</color> |
|||
</resources> |
@ -0,0 +1,4 @@ |
|||
<resources> |
|||
<string name="app_name">视频号模块</string> |
|||
<string name="xposed_description">微信 视频号hook组件 8.0.25版本</string> |
|||
</resources> |
@ -0,0 +1,16 @@ |
|||
<resources xmlns:tools="http://schemas.android.com/tools"> |
|||
<!-- Base application theme. --> |
|||
<style name="Theme.Xp_module" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> |
|||
<!-- Primary brand color. --> |
|||
<item name="colorPrimary">@color/purple_500</item> |
|||
<item name="colorPrimaryVariant">@color/purple_700</item> |
|||
<item name="colorOnPrimary">@color/white</item> |
|||
<!-- Secondary brand color. --> |
|||
<item name="colorSecondary">@color/teal_200</item> |
|||
<item name="colorSecondaryVariant">@color/teal_700</item> |
|||
<item name="colorOnSecondary">@color/black</item> |
|||
<!-- Status bar color. --> |
|||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> |
|||
<!-- Customize your theme here. --> |
|||
</style> |
|||
</resources> |
@ -0,0 +1,17 @@ |
|||
package com.example.wechatvideo; |
|||
|
|||
import org.junit.Test; |
|||
|
|||
import static org.junit.Assert.*; |
|||
|
|||
/** |
|||
* Example local unit test, which will execute on the development machine (host). |
|||
* |
|||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
|||
*/ |
|||
public class ExampleUnitTest { |
|||
@Test |
|||
public void addition_isCorrect() { |
|||
assertEquals(4, 2 + 2); |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue