commit 72bcc09bd625236ad068d5dd5b722376fafe9e82 Author: steve.gao Date: Tue Dec 17 11:07:04 2024 +0800 initial diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dfbc7c1 --- /dev/null +++ b/build.gradle @@ -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 +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..57202fd --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "xp" +include ':wechatvideo' diff --git a/wechatvideo/.gitignore b/wechatvideo/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/wechatvideo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/wechatvideo/build.gradle b/wechatvideo/build.gradle new file mode 100644 index 0000000..ffdc712 --- /dev/null +++ b/wechatvideo/build.gradle @@ -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' +} \ No newline at end of file diff --git a/wechatvideo/proguard-rules.pro b/wechatvideo/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/wechatvideo/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/wechatvideo/src/androidTest/java/com/example/wechatvideo/ExampleInstrumentedTest.java b/wechatvideo/src/androidTest/java/com/example/wechatvideo/ExampleInstrumentedTest.java new file mode 100644 index 0000000..5d57b02 --- /dev/null +++ b/wechatvideo/src/androidTest/java/com/example/wechatvideo/ExampleInstrumentedTest.java @@ -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 Testing documentation + */ +@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()); + } +} \ No newline at end of file diff --git a/wechatvideo/src/main/AndroidManifest.xml b/wechatvideo/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fad47f5 --- /dev/null +++ b/wechatvideo/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wechatvideo/src/main/assets/xposed_init b/wechatvideo/src/main/assets/xposed_init new file mode 100644 index 0000000..3202a09 --- /dev/null +++ b/wechatvideo/src/main/assets/xposed_init @@ -0,0 +1 @@ +com.example.wechatvideo.HookVideoData825Entity \ No newline at end of file diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoData825Entity.java b/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoData825Entity.java new file mode 100644 index 0000000..0d7161f --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoData825Entity.java @@ -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 map){ + try { + Map paramsMap = (Map) 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 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 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 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 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 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 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 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服务"); + } + + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoDataEntity.java b/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoDataEntity.java new file mode 100644 index 0000000..6a8c838 --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/HookVideoDataEntity.java @@ -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 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 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)); + } + + } + +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/MainActivity.java b/wechatvideo/src/main/java/com/example/wechatvideo/MainActivity.java new file mode 100644 index 0000000..2dd7e41 --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/MainActivity.java @@ -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)); + + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/content/MultiprocessSharedPreferences.java b/wechatvideo/src/main/java/com/example/wechatvideo/content/MultiprocessSharedPreferences.java new file mode 100644 index 0000000..232c0ab --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/content/MultiprocessSharedPreferences.java @@ -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读写;
+ * 1、ContentProvider天生支持多进程访问;
+ * 2、使用内部私有BroadcastReceiver实现多进程OnSharedPreferenceChangeListener监听;
+ * + * 使用方法:AndroidManifest.xml中添加provider申明:
+ *
+ * <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需要校验签名,否则无法安装;--!/>
+ *
+ * + * ContentProvider方式实现要注意:
+ * 1、当ContentProvider所在进程android.os.Process.killProcess(pid)时,会导致整个应用程序完全意外退出或者ContentProvider所在进程重启;
+ * 重启报错信息:Acquiring provider for user 0: existing object's process dead;
+ * 2、如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用,因此put值时默认返回false,get值时默认返回null;
+ * + * 其他方式实现SharedPreferences的问题:
+ * 使用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 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 mListenersCount; + + private static class ReflectionUtil { + + public static ContentValues contentValuesNewInstance(HashMap values) { + try { + Constructor 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 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 sharedPreferencesGetStringSet(SharedPreferences sharedPreferences, String key, Set values) { + try { + Method method = sharedPreferences.getClass().getDeclaredMethod("getStringSet", new Class[] { String.class, Set.class }); // Android 3.0 + return (Set) 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 getAll() { + Map value = (Map) getValue(PATH_GET_ALL, null, null); + return value == null ? new HashMap() : 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 getStringSet(String key, Set defValues) { + return (Set) 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(); + } + 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 keysModified = (List) intent.getSerializableExtra(KEY); + if (mName.equals(name) && keysModified != null) { + Set listeners = new HashSet(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 mModified = new HashMap(); + 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 values) { + synchronized (this) { + mModified.put(key, (values == null) ? null : new HashSet(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) 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 set = (Set) 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) 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 set = null; + if (projection != null) { + set = new HashSet(Arrays.asList(projection)); + } + bundle.putSerializable(KEY, (HashSet) 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 keysModified = null; + Map map = null; + if (hasListeners) { + keysModified = new ArrayList(); + map = (Map) preferences.getAll(); + } + Editor editor = preferences.edit(); + boolean clear = Boolean.parseBoolean(selectionArgs[1]); + if (clear) { + if (hasListeners && !map.isEmpty()) { + for (Map.Entry entry : map.entrySet()) { + keysModified.add(entry.getKey()); + } + } + editor.clear(); + } + for (Map.Entry 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) 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(); + } + } + + private void notifyListeners(String name, ArrayList 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; + } + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/handler/CommentListHandler.java b/wechatvideo/src/main/java/com/example/wechatvideo/handler/CommentListHandler.java new file mode 100644 index 0000000..5c4e570 --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/handler/CommentListHandler.java @@ -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 setVideoModel(){ + ArrayList 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 map = new HashMap<>(); + + Map 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 objects = setVideoModel(); + for (Object o:objects){ + i = o; + jsonObject = invoke(_videoId, objectNonceId, userId, lastBuffer, _commentId, _displayId); + Map data = (Map) 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 data = (Map) jsonObject.get("data"); + if(data.size() == 0){ + jsonObject = retry(_videoId, _commentId, _displayId); + } + LinkedList 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); + } + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUser825Handler.java b/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUser825Handler.java new file mode 100644 index 0000000..00667e8 --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUser825Handler.java @@ -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 sorts = new HashMap(){{ + put(2, "综合"); + put(1, "实时"); + }}; + + + /** + * 全局遍历 获取keyword关键词 + */ + public void setFinderMixSearchPresenter(){ + ArrayList 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 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 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)); + } + } + }); + } + +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUserHandler.java b/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUserHandler.java new file mode 100644 index 0000000..3fd41cd --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/handler/SearchUserHandler.java @@ -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 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 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 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)); + + } + } + }); + + + + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideo825Handler.java b/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideo825Handler.java new file mode 100644 index 0000000..ccf385f --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideo825Handler.java @@ -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 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 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 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 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)); + } + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideoHandler.java b/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideoHandler.java new file mode 100644 index 0000000..76b82db --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/handler/UserVideoHandler.java @@ -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 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 map = new HashMap<>(); + StoredObject.map1.put(offset, map); + XposedHelpers.callMethod(o, "requestLoadMore"); + + return StoredObject.getData(offset); + } + + + /** + * 全局过滤 一个枚举对象 用于构建 作者对象 + */ + public void setProFile(){ + ArrayList 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 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)); + } + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/model/StoredObject.java b/wechatvideo/src/main/java/com/example/wechatvideo/model/StoredObject.java new file mode 100644 index 0000000..24c8ccd --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/model/StoredObject.java @@ -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> 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 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 removeMap = map1.remove(uuidKey); + Map data = (Map) removeMap.get("data"); // 这是有数据的 + jsonObject = new JSONObject(data); + return jsonObject; + } + }catch (InterruptedException e){ + e.printStackTrace(); + jsonObject.put("msg", "timeOut"); + }finally { + setUuidKey(null); + } + + return jsonObject; + } +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/utils/SearchClassUtils.java b/wechatvideo/src/main/java/com/example/wechatvideo/utils/SearchClassUtils.java new file mode 100644 index 0000000..b89dbdc --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/utils/SearchClassUtils.java @@ -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 getAllInstancesOfClass(Class clazz) { return getClassInstancesOfCurrentThread(clazz, false); } + + /** + * 根据Class 获取当前进程该class 全部的实例 + * @param clazz:当前class + * @param assignable: 是否包含子类的实例 + * @return + */ + @TargetApi(28) + private static synchronized ArrayList getClassInstancesOfCurrentThread(Class clazz, Boolean assignable){ + ArrayList 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 getRuntimeStats() throws Exception { + return (Map) 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}); + } + +} diff --git a/wechatvideo/src/main/java/com/example/wechatvideo/utils/SpUtil.java b/wechatvideo/src/main/java/com/example/wechatvideo/utils/SpUtil.java new file mode 100644 index 0000000..9433969 --- /dev/null +++ b/wechatvideo/src/main/java/com/example/wechatvideo/utils/SpUtil.java @@ -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; + } +} diff --git a/wechatvideo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/wechatvideo/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/wechatvideo/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wechatvideo/src/main/res/drawable/ic_launcher_background.xml b/wechatvideo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/wechatvideo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wechatvideo/src/main/res/drawable/ic_launcher_foreground.xml b/wechatvideo/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/wechatvideo/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wechatvideo/src/main/res/drawable/new_icon_background.xml b/wechatvideo/src/main/res/drawable/new_icon_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/wechatvideo/src/main/res/drawable/new_icon_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wechatvideo/src/main/res/layout/activity_main.xml b/wechatvideo/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b534aa1 --- /dev/null +++ b/wechatvideo/src/main/res/layout/activity_main.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + +