├── app
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ └── xposed_init
│ │ ├── res
│ │ ├── values
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ └── xml
│ │ │ └── xposed_prefs.xml
│ │ ├── java
│ │ └── me
│ │ │ └── zjns
│ │ │ └── lovecloudmusic
│ │ │ ├── hooker
│ │ │ ├── base
│ │ │ │ └── BaseHook.java
│ │ │ ├── CommonAdHook.java
│ │ │ ├── OtherHook.java
│ │ │ ├── VipFeatureHook.java
│ │ │ ├── VideoFlowHook.java
│ │ │ ├── CommentListHook.java
│ │ │ └── SideBarHook.java
│ │ │ ├── Constants.java
│ │ │ ├── HookInit.java
│ │ │ ├── ClassHelper.java
│ │ │ ├── CloudMusic.java
│ │ │ ├── MultiDexHelper.java
│ │ │ ├── HookInfo.java
│ │ │ ├── Utils.java
│ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
├── build.gradle
└── proguard-rules.pro
├── settings.gradle
├── .gitignore
├── README.md
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | me.zjns.lovecloudmusic.HookInit
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | .DS_Store
6 | /build
7 | /captures
8 | *.apk
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PureNeteaseCloudMusic-Xposed
2 | A Xposed module to pure Netease CloudMusic android client.
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/PureNeteaseCloudMusic-Xposed/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 23 22:40:26 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## Project-wide Gradle settings.
2 | #
3 | # For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # Specifies the JVM arguments used for the daemon process.
7 | # The setting is particularly useful for tweaking memory settings.
8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
10 | #
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | # org.gradle.parallel=true
15 | #Sat Dec 30 14:51:28 CST 2017
16 | org.gradle.jvmargs=-Xmx1536m
17 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | buildToolsVersion '28.0.3'
5 | compileSdkVersion 28
6 | defaultConfig {
7 | applicationId "me.zjns.lovecloudmusic"
8 | minSdkVersion 14
9 | //noinspection ExpiredTargetSdkVersion
10 | targetSdkVersion 19
11 | versionCode 21
12 | versionName "2.7.1"
13 | archivesBaseName = "Pure163Music-Xposed_v${versionName}"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled true
18 | zipAlignEnabled true
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | compileOptions {
23 | sourceCompatibility JavaVersion.VERSION_1_8
24 | targetCompatibility JavaVersion.VERSION_1_8
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(include: ['*.jar'], dir: 'libs')
30 | compileOnly 'de.robv.android.xposed:api:82'
31 | }
32 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -keep public class me.zjns.lovecloudmusic.HookInit
23 | -keepclassmembers class me.zjns.lovecloudmusic.MainActivity {
24 | boolean isModuleEnabled();
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/base/BaseHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker.base;
2 |
3 | import de.robv.android.xposed.XSharedPreferences;
4 | import me.zjns.lovecloudmusic.CloudMusic;
5 | import me.zjns.lovecloudmusic.Constants;
6 | import me.zjns.lovecloudmusic.HookInfo;
7 |
8 | import static de.robv.android.xposed.XposedBridge.log;
9 |
10 | public abstract class BaseHook {
11 | public static boolean hasExtraException = false;
12 | protected String versionName;
13 | protected ClassLoader loader;
14 | protected HookInfo hookInfo;
15 | protected XSharedPreferences prefs;
16 |
17 | public BaseHook() {
18 | this.versionName = CloudMusic.getVersionName();
19 | this.loader = CloudMusic.getLoader();
20 | this.hookInfo = CloudMusic.getHookInfo();
21 | this.prefs = CloudMusic.getSharedPrefs();
22 | }
23 |
24 | protected abstract void hookMain();
25 |
26 | public final void startHook() {
27 | if (disableHook()) return;
28 | try {
29 | hookMain();
30 | } catch (Throwable t) {
31 | log(t);
32 | }
33 | }
34 |
35 | protected boolean disableHook() {
36 | // no longer support version that lower 5.5.2.
37 | return versionName.compareTo(Constants.CM_VERSION_552) < 0;
38 | }
39 |
40 | protected final void loadPrefs() {
41 | prefs = CloudMusic.getSharedPrefs();
42 | prefs.reload();
43 | initPrefs();
44 | }
45 |
46 | protected void initPrefs() {
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/Constants.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | public final class Constants {
4 | public static final String META_TYPE_COMMENT_LIST_ENTRY = "CommentListEntry";
5 | public static final String META_TYPE_VIDEO_TIMELINE_DATA = "VideoTimelineData";
6 | public static final String CM_VERSION_421 = "4.2.1";
7 | public static final String CM_VERSION_520 = "5.2.0";
8 | public static final String CM_VERSION_530 = "5.3.0";
9 | public static final String CM_VERSION_552 = "5.5.2";
10 | public static final String CM_VERSION_580 = "5.8.0";
11 | public static final String CM_VERSION_590 = "5.9.0";
12 | public static final String CM_VERSION_600 = "6.0.0";
13 | static final String MODULE_PACKAGE_NAME = "me.zjns.lovecloudmusic";
14 | static final String HOOK_PACKAGE_NAME = "com.netease.cloudmusic";
15 | static final String FILE_HOOK_INFO = "HookInfo.dat";
16 | static final String KEY_METHOD_CHANNEL = "method_channel";
17 | static final String KEY_CLASS_CHANNEL = "class_channel";
18 | static final String KEY_CLASS_COMMENT_LIST_ENTRY = "class_CommentListEntry";
19 | static final String KEY_METHOD_IS_WEEKEND = "method_is_weekend";
20 | static final String KEY_CLASS_IS_WEEKEND = "class_is_weekend";
21 | static final String KEY_METHOD_USER_GROUP = "method_user_group";
22 | static final String KEY_CLASS_USER_GROUP = "class_user_group";
23 | static final String KEY_CLASS_VIDEO_TIMELINE_DATA = "class_VideoTimelineData";
24 | static final String CM_VERSION_411 = "4.1.1";
25 | static final String CM_VERSION_413 = "4.1.3";
26 | static final String CM_VERSION_433 = "4.3.3";
27 | static final String CM_VERSION_435 = "4.3.5";
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/HookInit.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.app.Application;
4 | import android.app.Instrumentation;
5 | import android.content.Context;
6 |
7 | import de.robv.android.xposed.IXposedHookLoadPackage;
8 | import de.robv.android.xposed.XC_MethodHook;
9 | import de.robv.android.xposed.XC_MethodReplacement;
10 | import de.robv.android.xposed.callbacks.XC_LoadPackage;
11 |
12 | import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
13 |
14 | /**
15 | * Created by YiTry on 2017/12/30
16 | */
17 |
18 | public class HookInit implements IXposedHookLoadPackage {
19 |
20 | @Override
21 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
22 | if (lpparam.packageName.equals(Constants.MODULE_PACKAGE_NAME)) {
23 | makeModuleActive(lpparam.classLoader);
24 | }
25 | if (lpparam.packageName.equals(Constants.HOOK_PACKAGE_NAME)) {
26 | findAndHookMethod(Instrumentation.class, "callApplicationOnCreate", Application.class, new XC_MethodHook() {
27 | @Override
28 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
29 | Context context = (Context) param.args[0];
30 | if (Utils.isInMainProcess(context)) {
31 | CloudMusic.getInstance().hookHandler(lpparam);
32 | }
33 | }
34 | });
35 | }
36 | }
37 |
38 | private void makeModuleActive(ClassLoader loader) {
39 | findAndHookMethod(Constants.MODULE_PACKAGE_NAME + ".MainActivity", loader, "isModuleEnabled", XC_MethodReplacement.returnConstant(true));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/ClassHelper.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.content.pm.PackageManager;
4 |
5 | import java.lang.ref.WeakReference;
6 | import java.util.ArrayList;
7 | import java.util.Collections;
8 | import java.util.Comparator;
9 | import java.util.List;
10 | import java.util.regex.Pattern;
11 |
12 |
13 | /**
14 | * Created by YiTry on 2018/3/9
15 | */
16 |
17 | final class ClassHelper {
18 | private static WeakReference> allClasses = new WeakReference<>(null);
19 |
20 | static List getFilteredClasses(boolean useCache, Pattern pattern) {
21 | try {
22 | return getFilteredClasses(useCache, pattern, null);
23 | } catch (PackageManager.NameNotFoundException e) {
24 | return new ArrayList<>();
25 | }
26 | }
27 |
28 | private static List getFilteredClasses(boolean useCache, Pattern pattern, Comparator comparator) throws PackageManager.NameNotFoundException {
29 | List list = filterList(getAllClasses(useCache), pattern);
30 | Collections.sort(list, comparator);
31 | return list;
32 | }
33 |
34 | private static List filterList(List list, Pattern pattern) {
35 | List filteredList = new ArrayList<>();
36 | for (String curStr : list) {
37 | if (pattern.matcher(curStr).find()) {
38 | filteredList.add(curStr);
39 | }
40 | }
41 | return filteredList;
42 | }
43 |
44 | private static List getAllClasses(boolean useCache) throws PackageManager.NameNotFoundException {
45 | List list = allClasses.get();
46 | if (list == null) {
47 | list = MultiDexHelper.getAllClasses(useCache);
48 | allClasses = new WeakReference<>(list);
49 | }
50 | return list;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/CommonAdHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import android.graphics.Canvas;
4 | import android.os.BaseBundle;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.view.View;
8 | import android.widget.LinearLayout;
9 |
10 | import de.robv.android.xposed.XC_MethodHook;
11 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
12 |
13 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
14 |
15 | public class CommonAdHook extends BaseHook {
16 |
17 | private boolean removeSplashAd;
18 | private boolean removeSearchAd;
19 |
20 | @Override
21 | protected void hookMain() {
22 | removeSplashAd();
23 | removeSearchBannerAd();
24 | }
25 |
26 | @Override
27 | protected void initPrefs() {
28 | removeSplashAd = prefs.getBoolean("convert_to_play", false);
29 | removeSearchAd = prefs.getBoolean("remove_search_banner_ad", false);
30 | }
31 |
32 | private void removeSplashAd() {
33 | Class> BundleClass = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? BaseBundle.class : Bundle.class;
34 | findAndHookMethod(BundleClass,
35 | "getSerializable", String.class,
36 | new XC_MethodHook() {
37 | @Override
38 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
39 | if ("adInfo".equals(param.args[0])) {
40 | loadPrefs();
41 | if (!removeSplashAd) return;
42 | param.setResult(null);
43 | }
44 | }
45 | });
46 | }
47 |
48 | private void removeSearchBannerAd() {
49 | findAndHookMethod("com.netease.cloudmusic.ui.AdBannerView", loader,
50 | "onDraw", Canvas.class,
51 | new XC_MethodHook() {
52 | @Override
53 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
54 | loadPrefs();
55 | if (!removeSearchAd) return;
56 |
57 | View view = (View) param.thisObject;
58 | View parent = (View) view.getParent().getParent();
59 | boolean isTarget = parent instanceof LinearLayout;
60 | if (isTarget) {
61 | LinearLayout real = (LinearLayout) parent;
62 | if (real.getChildCount() != 0) {
63 | real.setVisibility(View.GONE);
64 | real.removeAllViews();
65 | param.setResult(null);
66 | }
67 | }
68 | }
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/CloudMusic.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import java.lang.ref.WeakReference;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | import de.robv.android.xposed.XSharedPreferences;
8 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
9 | import me.zjns.lovecloudmusic.hooker.CommentListHook;
10 | import me.zjns.lovecloudmusic.hooker.CommonAdHook;
11 | import me.zjns.lovecloudmusic.hooker.OtherHook;
12 | import me.zjns.lovecloudmusic.hooker.SideBarHook;
13 | import me.zjns.lovecloudmusic.hooker.VideoFlowHook;
14 | import me.zjns.lovecloudmusic.hooker.VipFeatureHook;
15 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
16 |
17 | import static me.zjns.lovecloudmusic.Utils.getPackageVersionName;
18 |
19 | public final class CloudMusic {
20 | private static String versionName;
21 | private static ClassLoader loader;
22 | private static WeakReference mSharedPrefs = new WeakReference<>(null);
23 | private static HookInfo hookInfo;
24 | private List hooks = new ArrayList<>();
25 |
26 | private CloudMusic() {
27 | }
28 |
29 | static CloudMusic getInstance() {
30 | return new CloudMusic();
31 | }
32 |
33 | public static XSharedPreferences getSharedPrefs() {
34 | XSharedPreferences prefs = mSharedPrefs.get();
35 | if (prefs == null) {
36 | prefs = new XSharedPreferences(Constants.MODULE_PACKAGE_NAME);
37 | mSharedPrefs = new WeakReference<>(prefs);
38 | }
39 | return prefs;
40 | }
41 |
42 | public static String getVersionName() {
43 | return versionName;
44 | }
45 |
46 | public static ClassLoader getLoader() {
47 | return loader;
48 | }
49 |
50 | public static HookInfo getHookInfo() {
51 | return hookInfo;
52 | }
53 |
54 | void hookHandler(LoadPackageParam lpparam) throws Throwable {
55 | if (!isHookSwitchOpened()) return;
56 |
57 | versionName = getPackageVersionName(Constants.HOOK_PACKAGE_NAME);
58 | hookInfo = new HookInfo(lpparam.classLoader);
59 | loader = lpparam.classLoader;
60 |
61 | startAllHook();
62 |
63 | hookInfo.saveHookInfo(BaseHook.hasExtraException);
64 | }
65 |
66 | private void startAllHook() {
67 | hooks.clear();
68 | hooks.add(new CommentListHook());
69 | hooks.add(new CommonAdHook());
70 | hooks.add(new SideBarHook());
71 | hooks.add(new VipFeatureHook());
72 | hooks.add(new OtherHook());
73 | hooks.add(new VideoFlowHook());
74 | for (BaseHook hook : hooks) {
75 | hook.startHook();
76 | }
77 | }
78 |
79 | private boolean isHookSwitchOpened() {
80 | XSharedPreferences prefs = getSharedPrefs();
81 | return prefs.getBoolean("enable_all_functions", false);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 隐藏桌面图标
4 | 隐藏后,下次可从Xposed Installer中打开
5 | 通用
6 | 伪装为Play版本
7 | 获得Play版本的全部特性,去除大部分广告,如首屏广告,日推横幅广告等
8 | 禁止签到跳转
9 | 评论区管理
10 | 移除广告
11 | 移除视频
12 | 移除文章
13 | 移除音乐会信息
14 | 移除直播
15 | 移除相关推荐
16 | 启用部分会员特权
17 | 免费使用会员主题,歌词图片及鲸云音效功能
18 | 免积分下载超清MV
19 | 功能分区管理
20 | 隐藏动态分区
21 | 隐藏视频分区
22 | 隐藏电台TAB分区
23 | 侧边栏管理
24 | 隐藏小红点
25 | 隐藏VIP会员
26 | 隐藏云村有票
27 | 隐藏商城
28 | 隐藏游戏推荐
29 | 隐藏在线听歌免流量
30 | 隐藏附近的人
31 | 隐藏优惠券
32 | 隐藏亲子频道
33 | 隐藏网易音乐人
34 | 移除主页顶部个人信息栏
35 | 启用模块
36 | 自动签到
37 | 移除搜索界面Banner广告
38 | 更改立即生效
39 | 更改在结束软件进程或者重启后生效
40 | 模块未激活,请先激活模块并重启手机!
41 | 激活
42 | 忽略
43 | 未安装 Xposed Installer!
44 | 未安装太极!
45 | 视频流管理
46 | 移除广告
47 | 移除直播
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/OtherHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 |
7 | import java.lang.reflect.Method;
8 |
9 | import de.robv.android.xposed.XC_MethodHook;
10 | import me.zjns.lovecloudmusic.Constants;
11 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
12 |
13 | import static de.robv.android.xposed.XposedHelpers.findClass;
14 | import static me.zjns.lovecloudmusic.Utils.findAndHookConstructor;
15 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
16 | import static me.zjns.lovecloudmusic.Utils.hookMethod;
17 |
18 | public class OtherHook extends BaseHook {
19 |
20 | private boolean zeroPointDLMV;
21 | private boolean removeMyInfoView;
22 | private boolean changeChannel;
23 |
24 | @Override
25 | protected void hookMain() {
26 | hookVideoPoint();
27 | setChannelToGoogle();
28 | if (versionName.compareTo(Constants.CM_VERSION_590) <= 0) {
29 | removeMyInfoView();
30 | }
31 | }
32 |
33 | @Override
34 | protected void initPrefs() {
35 | zeroPointDLMV = prefs.getBoolean("zero_point_video", false);
36 | removeMyInfoView = prefs.getBoolean("remove_my_info_view", false);
37 | changeChannel = prefs.getBoolean("convert_to_play", false);
38 | }
39 |
40 | private void hookVideoPoint() {
41 | Class> metaMV = findClass("com.netease.cloudmusic.meta.MV", loader);
42 | findAndHookMethod(metaMV, "isDownloadNeedPoint", new XC_MethodHook() {
43 | @Override
44 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
45 | loadPrefs();
46 | if (zeroPointDLMV) {
47 | param.setResult(false);
48 | }
49 | }
50 | });
51 | findAndHookMethod(metaMV, "isFreePointCurBitMvDownload", new XC_MethodHook() {
52 | @Override
53 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
54 | loadPrefs();
55 | if (zeroPointDLMV) {
56 | param.setResult(true);
57 | }
58 | }
59 | });
60 | }
61 |
62 | private void removeMyInfoView() {
63 | Class> CardView = findClass("com.netease.cloudmusic.adapter.MyMusicAdapter$CardView", loader);
64 | findAndHookConstructor(View.class,
65 | Context.class, AttributeSet.class, Integer.TYPE, Integer.TYPE,
66 | new XC_MethodHook() {
67 | @Override
68 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
69 | if (param.thisObject.getClass() != CardView) return;
70 | loadPrefs();
71 | if (!removeMyInfoView) return;
72 | View view = (View) param.thisObject;
73 | view.setVisibility(View.GONE);
74 | }
75 | });
76 | }
77 |
78 | private void setChannelToGoogle() {
79 | Method method = hookInfo.getMethodChannel();
80 | if (method == null) return;
81 | XC_MethodHook.Unhook unhook = hookMethod(method, new XC_MethodHook() {
82 | @Override
83 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
84 | loadPrefs();
85 | if (changeChannel) {
86 | param.setResult("google");
87 | }
88 | }
89 | });
90 | if (unhook == null) hasExtraException = true;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/VipFeatureHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 |
6 | import java.lang.reflect.Method;
7 |
8 | import de.robv.android.xposed.XC_MethodHook;
9 | import me.zjns.lovecloudmusic.Constants;
10 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
11 |
12 | import static de.robv.android.xposed.XposedBridge.log;
13 | import static de.robv.android.xposed.XposedHelpers.callMethod;
14 | import static de.robv.android.xposed.XposedHelpers.callStaticMethod;
15 | import static de.robv.android.xposed.XposedHelpers.findClass;
16 | import static me.zjns.lovecloudmusic.Utils.findAndHookConstructor;
17 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
18 | import static me.zjns.lovecloudmusic.Utils.findMethodByExactParameters;
19 | import static me.zjns.lovecloudmusic.Utils.hookMethod;
20 |
21 | public class VipFeatureHook extends BaseHook {
22 |
23 | private Boolean mIsVipPro = null;
24 |
25 | private boolean enableVipFeature;
26 |
27 | @Override
28 | protected void hookMain() {
29 | hookVIPTheme();
30 | hookLyricTemplate();
31 | enableVIPAudioEffect();
32 | }
33 |
34 | @Override
35 | protected void initPrefs() {
36 | enableVipFeature = prefs.getBoolean("enable_vip_feature", false);
37 | }
38 |
39 | private void hookVIPTheme() {
40 | String ThemeDetailActivity = "com.netease.cloudmusic.activity.ThemeDetailActivity";
41 | String ThemeDetailActivity$1 = ThemeDetailActivity + "$1";
42 | if (versionName.compareTo(Constants.CM_VERSION_580) < 0) {
43 | findAndHookConstructor(ThemeDetailActivity$1, loader,
44 | ThemeDetailActivity, Integer.TYPE, Boolean.TYPE, Boolean.TYPE, Integer.TYPE, Intent.class, String.class,
45 | new XC_MethodHook() {
46 | @Override
47 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
48 | loadPrefs();
49 | if (!enableVipFeature || isVipPro()) return;
50 | param.args[2] = false;
51 | param.args[3] = false;
52 | }
53 | });
54 | } else {
55 | Class> ThemeInfo = findClass("com.netease.cloudmusic.theme.core.ThemeInfo", loader);
56 | findAndHookMethod(Bundle.class,
57 | "getParcelable", String.class,
58 | new XC_MethodHook() {
59 | @Override
60 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
61 | Object result = param.getResult();
62 | if (result != null && result.getClass() == ThemeInfo) {
63 | loadPrefs();
64 | if (enableVipFeature && !isVipPro()) {
65 | callMethod(result, "setPaid", true);
66 | callMethod(result, "setVip", false);
67 | }
68 | }
69 | }
70 | });
71 | }
72 | }
73 |
74 | private void hookLyricTemplate() {
75 | try {
76 | Method e = findMethodByExactParameters(
77 | findClass("com.netease.cloudmusic.module.lyrictemplate.d", loader),
78 | "e", Boolean.TYPE);
79 | hookMethod(e, new XC_MethodHook() {
80 | @Override
81 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
82 | loadPrefs();
83 | if (enableVipFeature && !isVipPro()) {
84 | param.setResult(false);
85 | }
86 | }
87 | });
88 | } catch (Throwable t) {
89 | log(t);
90 | }
91 | }
92 |
93 | private void enableVIPAudioEffect() {
94 | if (versionName.compareTo(Constants.CM_VERSION_580) < 0) return;
95 | Class> AudioEffectButtonData = findClass("com.netease.cloudmusic.meta.AudioEffectButtonData", loader);
96 | Class> AudioActionView = findClass("com.netease.cloudmusic.ui.AudioActionView", loader);
97 | findAndHookMethod(AudioActionView,
98 | "setDownloadTypeViews", AudioEffectButtonData, Object.class, Integer.TYPE,
99 | new XC_MethodHook() {
100 | @Override
101 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
102 | Object data = param.args[0];
103 | if (data != null && (Integer) callMethod(data, "getType") == 3) {
104 | loadPrefs();
105 | if (enableVipFeature && !isVipPro()) {
106 | callMethod(data, "setType", 1);
107 | callMethod(data, "setAnimVipType", 1);
108 | callMethod(data, "setAeVipType", 1);
109 | }
110 | }
111 | }
112 | });
113 | }
114 |
115 | private boolean isVipPro() {
116 | if (mIsVipPro != null) return mIsVipPro;
117 | if (versionName.compareTo(Constants.CM_VERSION_530) >= 0) {
118 | Class> UserPrivilege = findClass("com.netease.cloudmusic.meta.virtual.UserPrivilege", loader);
119 | String vipType = callStaticMethod(UserPrivilege, "getLogVipType").toString().trim();
120 | return mIsVipPro = "100".equals(vipType);
121 | }
122 | Object profile = hookInfo.getProfile();
123 | return mIsVipPro = profile != null && (boolean) callMethod(profile, "isVipPro");
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/VideoFlowHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import java.util.Iterator;
4 | import java.util.List;
5 |
6 | import de.robv.android.xposed.XC_MethodHook;
7 | import me.zjns.lovecloudmusic.Constants;
8 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
9 |
10 | import static de.robv.android.xposed.XposedHelpers.findClassIfExists;
11 | import static de.robv.android.xposed.XposedHelpers.getBooleanField;
12 | import static de.robv.android.xposed.XposedHelpers.getIntField;
13 | import static de.robv.android.xposed.XposedHelpers.getStaticIntField;
14 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
15 |
16 | /**
17 | * @author kofua
18 | * @date 2019/11/3 下午 3:01
19 | */
20 | public class VideoFlowHook extends BaseHook {
21 |
22 | private DATA_TYPES mTypes;
23 | private boolean removeAd;
24 | private boolean removeLive;
25 |
26 | @Override
27 | protected void hookMain() {
28 | mTypes = new DATA_TYPES(loader);
29 | removeVideoFlowAd();
30 | }
31 |
32 | @Override
33 | protected void initPrefs() {
34 | removeAd = prefs.getBoolean("remove_video_flow_ad", false);
35 | removeLive = prefs.getBoolean("remove_video_flow_live", false);
36 | }
37 |
38 | private void removeVideoFlowAd() {
39 | findAndHookMethod("androidx.loader.content.AsyncTaskLoader$LoadTask", loader,
40 | "onPostExecute", Object.class, new VideoListHook());
41 | }
42 |
43 | private class VideoListHook extends XC_MethodHook {
44 | @Override
45 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
46 | Object arg = param.args[0];
47 | if (!(arg instanceof List)) return;
48 |
49 | List list = (List) arg;
50 | if (list.isEmpty()) return;
51 |
52 | Object o = list.get(0);
53 | if (o == null) return;
54 |
55 | String name = o.getClass().getSimpleName();
56 | if (!Constants.META_TYPE_VIDEO_TIMELINE_DATA.equals(name)) return;
57 |
58 | loadPrefs();
59 |
60 | Iterator it = list.iterator();
61 | while (it.hasNext()) {
62 | if (shouldRemove(it.next())) {
63 | it.remove();
64 | }
65 | }
66 | }
67 |
68 | private boolean shouldRemove(Object entity) {
69 | int type = getIntField(entity, "type");
70 | return type == mTypes.TIMELINE_BANNER_AD && removeAd
71 | || type == mTypes.TIMELINE_PIC_AD && removeAd
72 | || type == mTypes.VIDEO_AD && removeAd
73 | || isVideoGameAd(entity) && removeAd
74 | || type == mTypes.LIVE && removeLive
75 | || type == mTypes.LIVE_LIST && removeLive;
76 | }
77 |
78 | private boolean isVideoGameAd(Object entity) {
79 | boolean isVideoGameAd = false;
80 | try {
81 | isVideoGameAd = getBooleanField(entity, "isVideoGameAd");
82 | } catch (Throwable ignored) {
83 | }
84 | return isVideoGameAd;
85 | }
86 | }
87 |
88 | private static class DATA_TYPES {
89 | private static final String CLASS_NAME = "com.netease.cloudmusic.meta.VideoTimelineData$DATA_TYPES";
90 |
91 | private Class classType;
92 |
93 | private final int BANNER;
94 | private final int DISPLAYED;
95 | private final int LIVE;
96 | private final int LIVE_LIST;
97 | private final int MV;
98 | private final int MVBILLBOARD;
99 | private final int MVSELECTED;
100 | private final int MVTITLE_MORE;
101 | private final int MVTITLE_SELECTED;
102 | private final int MV_VIDEO;
103 | private final int PIC_ACTIVITY;
104 | private final int PREFERENCE;
105 | private final int PROFILE_RMD;
106 | private final int RELATED_SONG;
107 | private final int RELATED_SONG_EMPTY;
108 | private final int SHOWROOM;
109 | private final int TALENT_LIVE;
110 | private final int TIMELINE_BANNER_AD;
111 | private final int TIMELINE_PIC_AD;
112 | private final int UNKNOWN;
113 | private final int VIDEO;
114 | private final int VIDEO_AD;
115 |
116 | DATA_TYPES(ClassLoader loader) {
117 | classType = findClassIfExists(CLASS_NAME, loader);
118 |
119 | BANNER = getValue("BANNER");
120 | DISPLAYED = getValue("DISPLAYED");
121 | LIVE = getValue("LIVE");
122 | LIVE_LIST = getValue("LIVE_LIST");
123 | MV = getValue("MV");
124 | MVBILLBOARD = getValue("MVBILLBOARD");
125 | MVSELECTED = getValue("MVSELECTED");
126 | MVTITLE_MORE = getValue("MVTITLE_MORE");
127 | MVTITLE_SELECTED = getValue("MVTITLE_SELECTED");
128 | MV_VIDEO = getValue("MV_VIDEO");
129 | PIC_ACTIVITY = getValue("PIC_ACTIVITY");
130 | PREFERENCE = getValue("PREFERENCE");
131 | PROFILE_RMD = getValue("PROFILE_RMD");
132 | RELATED_SONG = getValue("RELATED_SONG");
133 | RELATED_SONG_EMPTY = getValue("RELATED_SONG_EMPTY");
134 | SHOWROOM = getValue("SHOWROOM");
135 | TALENT_LIVE = getValue("TALENT_LIVE");
136 | TIMELINE_BANNER_AD = getValue("TIMELINE_BANNER_AD");
137 | TIMELINE_PIC_AD = getValue("TIMELINE_PIC_AD");
138 | UNKNOWN = getValue("UNKNOWN");
139 | VIDEO = getValue("VIDEO");
140 | VIDEO_AD = getValue("VIDEO_AD");
141 | }
142 |
143 | private int getValue(String field) {
144 | try {
145 | return getStaticIntField(classType, field);
146 | } catch (Throwable ignored) {
147 | }
148 | return -1;
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/CommentListHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import java.util.Iterator;
4 | import java.util.List;
5 |
6 | import de.robv.android.xposed.XC_MethodHook;
7 | import me.zjns.lovecloudmusic.Constants;
8 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
9 |
10 | import static de.robv.android.xposed.XposedHelpers.callMethod;
11 | import static de.robv.android.xposed.XposedHelpers.findClass;
12 | import static de.robv.android.xposed.XposedHelpers.getStaticIntField;
13 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
14 |
15 | public class CommentListHook extends BaseHook {
16 |
17 | private TYPE mType;
18 |
19 | private boolean removeAd;
20 | private boolean removeConcert;
21 | private boolean removeVideo;
22 | private boolean removeLive;
23 | private boolean removeTopic;
24 | private boolean removeVipRcmd;
25 |
26 | @Override
27 | protected void hookMain() {
28 | mType = new TYPE();
29 | mType.initType();
30 | removeCommentAd();
31 | }
32 |
33 | @Override
34 | protected void initPrefs() {
35 | removeAd = prefs.getBoolean("remove_comment_ad", false);
36 | removeConcert = prefs.getBoolean("remove_comment_concert_info", false);
37 | removeVideo = prefs.getBoolean("remove_comment_video", false);
38 | removeLive = prefs.getBoolean("remove_comment_live", false);
39 | removeTopic = prefs.getBoolean("remove_comment_topic", false);
40 | removeVipRcmd = prefs.getBoolean("remove_comment_vip_rcmd", false);
41 | }
42 |
43 | private void removeCommentAd() {
44 | findAndHookMethod("com.netease.cloudmusic.ui.PagerListView$LoadTaskAware", loader,
45 | "realOnPostExecute", List.class,
46 | new ListHook());
47 | }
48 |
49 | private boolean shouldRemove(Object obj) {
50 | int type = (int) callMethod(obj, "getType");
51 | return type == mType.AD && removeAd
52 | || type == mType.BANNER_AD && removeAd
53 | || type == mType.VIDEO_AD && removeAd
54 | || type == mType.AD_POSITION_LIVING_TYPE && removeAd
55 | || type == mType.VIDEO && removeVideo
56 | || type == mType.CONCERT_INFO && removeConcert
57 | || type == mType.LIVE && removeLive
58 | || type == mType.LIVE_RCMD && removeLive
59 | || type == mType.RELATIVE_TOPIC && removeTopic
60 | || type == mType.VIP_RCMD && removeVipRcmd;
61 | }
62 |
63 | @SuppressWarnings("unused")
64 | private final class TYPE {
65 | private int AD;
66 | private int AD_POSITION_LIVING_TYPE;
67 | private int BANNER_AD;
68 | private int COMMENT;
69 | private int COMMENT_DELETE;
70 | private int COMMENT_IN_PICTURE;
71 | private int COMMENT_LIVING_TYPE;
72 | private int COMMENT_WITH_RES_CARD;
73 | private int COMMON_SECTION;
74 | private int COMMON_SECTION_IN_PICTURE;
75 | private int CONCERT_INFO;
76 | private int FESTIVAL;
77 | private int GENERAL;
78 | private int LIVE;
79 | private int LIVE_RCMD;
80 | private int MORE_HOT_COMMENT;
81 | private int RECENT_COMMENT_SECTION;
82 | private int RELATIVE_TOPIC;
83 | private int UNKONWN;
84 | private int VIDEO;
85 | private int VIDEO_AD;
86 | private int VIP_RCMD;
87 |
88 | private Class> classTYPE;
89 |
90 | private void initType() {
91 | String className = "com.netease.cloudmusic.meta.virtual.CommentListEntry$TYPE";
92 | classTYPE = findClass(className, loader);
93 | AD = getValue("AD");
94 | AD_POSITION_LIVING_TYPE = getValue("AD_POSITION_LIVING_TYPE");
95 | BANNER_AD = getValue("BANNER_AD");
96 | COMMENT = getValue("COMMENT");
97 | COMMENT_DELETE = getValue("COMMENT_DELETE");
98 | COMMENT_IN_PICTURE = getValue("COMMENT_IN_PICTURE");
99 | COMMENT_LIVING_TYPE = getValue("COMMENT_LIVING_TYPE");
100 | COMMENT_WITH_RES_CARD = getValue("COMMENT_WITH_RES_CARD");
101 | COMMON_SECTION = getValue("COMMON_SECTION");
102 | COMMON_SECTION_IN_PICTURE = getValue("COMMON_SECTION_IN_PICTURE");
103 | CONCERT_INFO = getValue("CONCERT_INFO");
104 | FESTIVAL = getValue("FESTIVAL");
105 | GENERAL = getValue("GENERAL");
106 | LIVE = getValue("LIVE");
107 | LIVE_RCMD = getValue("LIVE_RCMD");
108 | MORE_HOT_COMMENT = getValue("MORE_HOT_COMMENT");
109 | RECENT_COMMENT_SECTION = getValue("RECENT_COMMENT_SECTION");
110 | RELATIVE_TOPIC = getValue("RELATIVE_TOPIC");
111 | UNKONWN = getValue("UNKONWN");
112 | VIDEO = getValue("VIDEO");
113 | VIDEO_AD = getValue("VIDEO_AD");
114 | VIP_RCMD = getValue("VIP_RCMD");
115 | }
116 |
117 | private int getValue(String field) {
118 | try {
119 | return getStaticIntField(classTYPE, field);
120 | } catch (Throwable t) {
121 | return -2;
122 | }
123 | }
124 | }
125 |
126 | private class ListHook extends XC_MethodHook {
127 | @Override
128 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
129 | List list = (List) param.args[0];
130 | if (list == null || list.isEmpty()) return;
131 |
132 | Object obj = list.get(0);
133 | if (obj == null) return;
134 |
135 | String name = obj.getClass().getSimpleName();
136 | if (!Constants.META_TYPE_COMMENT_LIST_ENTRY.equals(name)) return;
137 |
138 | loadPrefs();
139 |
140 | Iterator it = list.iterator();
141 | while (it.hasNext()) {
142 | Object entry = it.next();
143 | if (shouldRemove(entry)) {
144 | it.remove();
145 | }
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/xposed_prefs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
16 |
20 |
25 |
29 |
34 |
38 |
43 |
47 |
48 |
49 |
52 |
56 |
60 |
64 |
68 |
72 |
76 |
77 |
78 |
81 |
85 |
89 |
90 |
91 |
94 |
98 |
102 |
106 |
110 |
114 |
118 |
122 |
126 |
130 |
134 |
135 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/MultiDexHelper.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.content.pm.ApplicationInfo;
6 | import android.content.pm.PackageManager;
7 | import android.os.Build;
8 |
9 | import java.io.File;
10 | import java.io.FileInputStream;
11 | import java.io.FileOutputStream;
12 | import java.io.ObjectInputStream;
13 | import java.io.ObjectOutputStream;
14 | import java.util.ArrayList;
15 | import java.util.Enumeration;
16 | import java.util.List;
17 |
18 | import dalvik.system.DexFile;
19 |
20 | import static de.robv.android.xposed.XposedBridge.log;
21 |
22 | /**
23 | * Created by xudshen@hotmail.com on 14/11/13.
24 | * http://stackoverflow.com/questions/26623905/android-multidex-list-all-classes
25 | */
26 |
27 | final class MultiDexHelper {
28 | private static final String EXTRACTED_NAME_EXT = ".classes";
29 | private static final String EXTRACTED_SUFFIX = ".zip";
30 |
31 | private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
32 | "secondary-dexes";
33 |
34 | private static final String PREFS_FILE = "multidex.version";
35 | private static final String KEY_DEX_NUMBER = "dex.number";
36 |
37 | private static SharedPreferences getMultiDexPreferences(Context context) {
38 | return context.getSharedPreferences(PREFS_FILE,
39 | Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
40 | ? Context.MODE_PRIVATE
41 | : Context.MODE_MULTI_PROCESS);
42 | }
43 |
44 | /**
45 | * get all the dex path
46 | *
47 | * @param context the application context
48 | * @return all the dex path
49 | * @throws PackageManager.NameNotFoundException not found
50 | */
51 | private static List getSourcePaths(Context context) throws PackageManager.NameNotFoundException {
52 | ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
53 | File sourceApk = new File(applicationInfo.sourceDir);
54 | File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
55 |
56 | List sourcePaths = new ArrayList<>();
57 | //add the default apk path
58 | sourcePaths.add(applicationInfo.sourceDir);
59 |
60 | //the prefix of extracted file, ie: test.classes
61 | String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
62 | //the total dex numbers
63 | int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
64 |
65 | for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
66 | //for each dex file, ie: test.classes2.zip, test.classes3.zip...
67 | String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
68 | File extractedFile = new File(dexDir, fileName);
69 | if (extractedFile.isFile()) {
70 | sourcePaths.add(extractedFile.getAbsolutePath());
71 | //we ignore the verify zip part
72 | } else {
73 | log("Missing extracted secondary dex file '" +
74 | extractedFile.getPath() + "'");
75 | }
76 | }
77 |
78 | return sourcePaths;
79 | }
80 |
81 | /**
82 | * get all the classes name in "classes.dex", "classes2.dex", ....
83 | *
84 | * @return all the classes name
85 | * @throws PackageManager.NameNotFoundException not found
86 | */
87 | @SuppressWarnings({"ResultOfMethodCallIgnored", "deprecation"})
88 | static List getAllClasses(boolean useCache) throws PackageManager.NameNotFoundException {
89 | // read class list from cache
90 | Context context = Utils.getPackageContext(Constants.HOOK_PACKAGE_NAME);
91 | long lastUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
92 | File classesFile = new File(context.getCacheDir(), "ClassList.dat");
93 |
94 | if (useCache && classesFile.exists() && classesFile.canRead()) {
95 | try {
96 | ObjectInputStream in = new ObjectInputStream(new FileInputStream(classesFile));
97 | long lastUpdateTimeFromFile = in.readLong();
98 | if (lastUpdateTime == lastUpdateTimeFromFile) {
99 | //noinspection unchecked
100 | return (List) in.readObject();
101 | }
102 | } catch (Exception e) {
103 | log(e);
104 | }
105 | }
106 |
107 |
108 | List classNames = new ArrayList<>();
109 | boolean hasException = false;
110 | for (String path : getSourcePaths(context)) {
111 | try {
112 | DexFile dexfile;
113 | String pathTmp = null;
114 | if (path.endsWith(EXTRACTED_SUFFIX)) {
115 | //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
116 | pathTmp = path + ".tmp";
117 | dexfile = DexFile.loadDex(path, pathTmp, 0);
118 | } else {
119 | dexfile = new DexFile(path);
120 | }
121 | Enumeration dexEntries = dexfile.entries();
122 | while (dexEntries.hasMoreElements()) {
123 | classNames.add(dexEntries.nextElement());
124 | }
125 | if (pathTmp != null) {
126 | new File(pathTmp).delete();
127 | }
128 | } catch (Throwable t) {
129 | hasException = true;
130 | log("Error at loading dex file '" +
131 | path + "'");
132 | }
133 | }
134 |
135 | // write class list cache
136 | if (useCache && !hasException) {
137 | try {
138 | ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(classesFile));
139 | out.writeLong(lastUpdateTime);
140 | out.writeObject(classNames);
141 | out.flush();
142 | out.close();
143 | } catch (Throwable t) {
144 | log(t);
145 | }
146 | }
147 |
148 | return classNames;
149 | }
150 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/HookInfo.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.content.Context;
4 |
5 | import java.io.File;
6 | import java.io.FileInputStream;
7 | import java.io.FileOutputStream;
8 | import java.io.ObjectInputStream;
9 | import java.io.ObjectOutputStream;
10 | import java.lang.reflect.Method;
11 | import java.lang.reflect.Modifier;
12 | import java.util.HashMap;
13 | import java.util.HashSet;
14 | import java.util.List;
15 | import java.util.Set;
16 | import java.util.regex.Pattern;
17 |
18 | import static de.robv.android.xposed.XposedBridge.log;
19 | import static de.robv.android.xposed.XposedHelpers.findClass;
20 | import static de.robv.android.xposed.XposedHelpers.findMethodExact;
21 |
22 | @SuppressWarnings("unchecked")
23 | public final class HookInfo {
24 | private final Pattern PATTERN_CLASSES_CHANNEL = Pattern.compile("^com\\.netease\\.cloudmusic\\.utils\\.[a-z]$");
25 | private ClassLoader loader;
26 | private boolean needUpdate = true;
27 | private Set keys = new HashSet<>();
28 | private HashMap hookInfoCache = new HashMap<>();
29 |
30 | HookInfo(ClassLoader classLoader) {
31 | setKeys();
32 | setClassLoader(classLoader);
33 | readHookInfo();
34 | }
35 |
36 | private void setClassLoader(ClassLoader classLoader) {
37 | loader = classLoader;
38 | }
39 |
40 | private void setKeys() {
41 | keys.add(Constants.KEY_METHOD_CHANNEL);
42 | }
43 |
44 | private void readHookInfo() {
45 | try {
46 | Context context = Utils.getPackageContext(Constants.HOOK_PACKAGE_NAME);
47 | long lastUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
48 | File hookInfoFile = new File(context.getCacheDir(), Constants.FILE_HOOK_INFO);
49 | if (hookInfoFile.isFile() && hookInfoFile.canRead()) {
50 | ObjectInputStream in = new ObjectInputStream(new FileInputStream(hookInfoFile));
51 | if (in.readLong() == lastUpdateTime) {
52 | HashMap info = (HashMap) in.readObject();
53 | if (info.keySet().containsAll(keys)) {
54 | hookInfoCache = info;
55 | needUpdate = false;
56 | }
57 | }
58 | }
59 | } catch (Exception e) {
60 | log(e);
61 | }
62 | }
63 |
64 | @SuppressWarnings("ResultOfMethodCallIgnored")
65 | void saveHookInfo(boolean hasExtraException) {
66 | if (hookInfoCache.keySet().containsAll(keys) && needUpdate && !hasExtraException) {
67 | try {
68 | Context context = Utils.getPackageContext(Constants.HOOK_PACKAGE_NAME);
69 | long lastUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
70 | File hookInfoFile = new File(context.getCacheDir(), Constants.FILE_HOOK_INFO);
71 | ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(hookInfoFile));
72 | out.writeLong(lastUpdateTime);
73 | out.writeObject(hookInfoCache);
74 | out.flush();
75 | out.close();
76 | } catch (Exception e) {
77 | log(e);
78 | }
79 | }
80 | if (!hookInfoCache.keySet().containsAll(keys) || hasExtraException) {
81 | try {
82 | Context context = Utils.getPackageContext(Constants.HOOK_PACKAGE_NAME);
83 | File hookInfoFile = new File(context.getCacheDir(), Constants.FILE_HOOK_INFO);
84 | if (hookInfoFile.exists()) {
85 | hookInfoFile.delete();
86 | }
87 | } catch (Exception e) {
88 | log(e);
89 | }
90 | }
91 | }
92 |
93 | public Method getMethodChannel() {
94 | if (hookInfoCache.containsKey(Constants.KEY_METHOD_CHANNEL)) {
95 | String tempMethodName = hookInfoCache.get(Constants.KEY_METHOD_CHANNEL);
96 | String tempClassName = hookInfoCache.get(Constants.KEY_CLASS_CHANNEL);
97 | return findMethodExact(tempClassName, loader, tempMethodName, Context.class, String.class);
98 | }
99 | List list = ClassHelper.getFilteredClasses(true, PATTERN_CLASSES_CHANNEL);
100 | for (String clazzName : list) {
101 | Class> clazz = findClass(clazzName, loader);
102 | for (Method method : clazz.getDeclaredMethods()) {
103 | if (method.getReturnType() == String.class
104 | && method.getParameterTypes().length == 2
105 | && method.getParameterTypes()[0] == Context.class
106 | && method.getParameterTypes()[1] == String.class
107 | && Modifier.isPublic(method.getModifiers())
108 | && Modifier.isStatic(method.getModifiers())) {
109 | if (!hookInfoCache.containsKey(Constants.KEY_METHOD_CHANNEL)) {
110 | hookInfoCache.put(Constants.KEY_METHOD_CHANNEL, method.getName());
111 | hookInfoCache.put(Constants.KEY_CLASS_CHANNEL, method.getDeclaringClass().getName());
112 | }
113 | return method;
114 | }
115 | }
116 | }
117 | return null;
118 | }
119 |
120 | public Object getProfile() {
121 | Class> NeteaseMusicUtils = findClass("com.netease.cloudmusic.utils.NeteaseMusicUtils", loader);
122 | Method mProfile = null;
123 | for (Method m : NeteaseMusicUtils.getDeclaredMethods()) {
124 | if (m.getReturnType() == Object.class
125 | && m.getParameterTypes().length == 3
126 | && m.getParameterTypes()[0] == Context.class
127 | && m.getParameterTypes()[1] == String.class
128 | && m.getParameterTypes()[2] == Boolean.TYPE) {
129 | mProfile = m;
130 | mProfile.setAccessible(true);
131 | break;
132 | }
133 | }
134 | if (mProfile == null) {
135 | return null;
136 | }
137 | try {
138 | Context context = Utils.getPackageContext(Constants.HOOK_PACKAGE_NAME);
139 | return mProfile.invoke(null, context, "Session.Profile", true);
140 | } catch (Throwable t) {
141 | return null;
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/Utils.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.app.ActivityManager;
4 | import android.app.Application;
5 | import android.content.Context;
6 | import android.content.pm.PackageInfo;
7 | import android.content.pm.PackageManager;
8 | import android.os.Handler;
9 | import android.os.Looper;
10 | import android.widget.Toast;
11 |
12 | import java.lang.reflect.Member;
13 | import java.lang.reflect.Method;
14 | import java.util.List;
15 |
16 | import de.robv.android.xposed.XC_MethodHook;
17 | import de.robv.android.xposed.XposedBridge;
18 | import de.robv.android.xposed.XposedHelpers;
19 |
20 | import static de.robv.android.xposed.XposedBridge.log;
21 | import static de.robv.android.xposed.XposedHelpers.callMethod;
22 | import static de.robv.android.xposed.XposedHelpers.callStaticMethod;
23 | import static de.robv.android.xposed.XposedHelpers.findClass;
24 |
25 | /**
26 | * Created by YiTry on 2018/3/14
27 | */
28 |
29 | public final class Utils {
30 |
31 | public static Context getPackageContext(String packageName) throws PackageManager.NameNotFoundException {
32 | Object currentThread = callStaticMethod(findClass("android.app.ActivityThread", null), "currentActivityThread");
33 | Context systemContext = (Context) callMethod(currentThread, "getSystemContext");
34 | return systemContext.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY);
35 | }
36 |
37 | public static String getPackageVersionName(String packageName) {
38 | Object thread = callStaticMethod(findClass("android.app.ActivityThread", null), "currentActivityThread");
39 | Context context = (Context) callMethod(thread, "getSystemContext");
40 | try {
41 | PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0);
42 | return packageInfo.versionName;
43 | } catch (PackageManager.NameNotFoundException e) {
44 | return null;
45 | }
46 | }
47 |
48 | public static Application getCurrentApplication() {
49 | Object thread = callStaticMethod(findClass("android.app.ActivityThread", null), "currentActivityThread");
50 | return (Application) callMethod(thread, "getApplication");
51 | }
52 |
53 | public static boolean isPackageInstalled(Context context, String pkgName) {
54 | boolean flag = false;
55 | try {
56 | context.getPackageManager().getApplicationInfo(pkgName, 0);
57 | flag = true;
58 | } catch (PackageManager.NameNotFoundException ignored) {
59 | }
60 |
61 | return flag;
62 | }
63 |
64 | public static String getCurrentProcessName(Context context) {
65 | String currentProcessName = "";
66 | int pid = android.os.Process.myPid();
67 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
68 | List runningProcesses = null;
69 | try {
70 | runningProcesses = manager.getRunningAppProcesses();
71 | } catch (SecurityException ignored) {
72 | }
73 | if (runningProcesses != null) {
74 | for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
75 | if (processInfo.pid == pid) {
76 | currentProcessName = processInfo.processName;
77 | break;
78 | }
79 | }
80 | }
81 | return currentProcessName;
82 | }
83 |
84 | public static boolean isInMainProcess(Context context) {
85 | String current = getCurrentProcessName(context);
86 | return current.equals(context.getPackageName());
87 | }
88 |
89 | public static void showToast(String message, boolean longTime, long delay) {
90 | int duration = longTime ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT;
91 | new Handler(Looper.getMainLooper()).postDelayed(() -> Toast.makeText(getCurrentApplication(), message, duration).show(), delay);
92 | }
93 |
94 | @SuppressWarnings("all")
95 | public static Method findMethodByExactParameters(Class> clazz, String methodName, Class> returnType, Class>... parameterTypes) {
96 | for (Method method : clazz.getDeclaredMethods()) {
97 | if (!method.getName().equals(methodName) || method.getReturnType() != returnType)
98 | continue;
99 | Class>[] methodParameterTypes = method.getParameterTypes();
100 | if (methodParameterTypes.length != parameterTypes.length)
101 | continue;
102 | boolean match = true;
103 | for (int i = 0; i < parameterTypes.length; i++) {
104 | if (methodParameterTypes[i] != parameterTypes[i]) {
105 | match = false;
106 | break;
107 | }
108 | }
109 | if (match) {
110 | method.setAccessible(true);
111 | return method;
112 | }
113 | }
114 | throw new RuntimeException("can't find method " + methodName + " in class " + clazz.getName());
115 | }
116 |
117 | public static XC_MethodHook.Unhook findAndHookMethod(Class> clazz, String methodName, Object... parameterTypesAndCallback) {
118 | try {
119 | return XposedHelpers.findAndHookMethod(clazz, methodName, parameterTypesAndCallback);
120 | } catch (Throwable t) {
121 | log(t);
122 | return null;
123 | }
124 | }
125 |
126 | public static XC_MethodHook.Unhook findAndHookMethod(String clazzName, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) {
127 | try {
128 | return XposedHelpers.findAndHookMethod(clazzName, classLoader, methodName, parameterTypesAndCallback);
129 | } catch (Throwable t) {
130 | log(t);
131 | return null;
132 | }
133 | }
134 |
135 | public static XC_MethodHook.Unhook findAndHookConstructor(String clazzName, ClassLoader classLoader, Object... parameterTypesAndCallback) {
136 | try {
137 | return XposedHelpers.findAndHookConstructor(clazzName, classLoader, parameterTypesAndCallback);
138 | } catch (Throwable t) {
139 | log(t);
140 | return null;
141 | }
142 | }
143 |
144 | public static XC_MethodHook.Unhook findAndHookConstructor(Class> clazz, Object... parameterTypesAndCallback) {
145 | try {
146 | return XposedHelpers.findAndHookConstructor(clazz, parameterTypesAndCallback);
147 | } catch (Throwable t) {
148 | log(t);
149 | return null;
150 | }
151 | }
152 |
153 | public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
154 | try {
155 | return XposedBridge.hookMethod(hookMethod, callback);
156 | } catch (Throwable t) {
157 | log(t);
158 | return null;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/MainActivity.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.app.AlertDialog;
6 | import android.content.ComponentName;
7 | import android.content.ContentResolver;
8 | import android.content.Context;
9 | import android.content.Intent;
10 | import android.content.pm.PackageManager;
11 | import android.net.Uri;
12 | import android.os.Bundle;
13 | import android.os.Handler;
14 | import android.preference.Preference;
15 | import android.preference.PreferenceFragment;
16 | import android.preference.SwitchPreference;
17 | import android.util.Log;
18 | import android.widget.Toast;
19 |
20 | import java.io.File;
21 |
22 | /**
23 | * Created by YiTry on 2018/1/27
24 | */
25 |
26 | public class MainActivity extends Activity {
27 | private static final String KEY_HIDE_ICON = "hide_icon";
28 | private AlertDialog mActiveDialog = null;
29 |
30 | @Override
31 | public void onCreate(Bundle savedInstanceState) {
32 | super.onCreate(savedInstanceState);
33 | getFragmentManager().beginTransaction().replace(android.R.id.content, new PrefsFragment()).commit();
34 | }
35 |
36 | @Override
37 | protected void onResume() {
38 | super.onResume();
39 | // checkActiveState();
40 | }
41 |
42 | private void checkActiveState() {
43 | new Handler().postDelayed(() -> {
44 | if (!isModuleEnabled() && !isModuleInExposedEnabled(this)) {
45 | showActiveDialog();
46 | }
47 | }, 500L);
48 | }
49 |
50 | private boolean isModuleEnabled() {
51 | Log.i("YiTry", "My life is brilliant, My love is pure ...");
52 | return false;
53 | }
54 |
55 | private boolean isModuleInExposedEnabled(Context context) {
56 | ContentResolver contentResolver = context.getContentResolver();
57 | if (contentResolver == null) {
58 | return false;
59 | }
60 |
61 | Uri uri = Uri.parse("content://me.weishu.exposed.CP/");
62 | try {
63 | Bundle result = contentResolver.call(uri, "active", null, null);
64 | if (result == null) {
65 | return false;
66 | }
67 | return result.getBoolean("active", false);
68 | } catch (Exception ignored) {
69 | }
70 | return false;
71 | }
72 |
73 | private boolean isInXposedEnvironment() {
74 | boolean flag = false;
75 | try {
76 | ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedBridge");
77 | flag = true;
78 | } catch (ClassNotFoundException ignored) {
79 | }
80 | return flag;
81 | }
82 |
83 | private void showActiveDialog() {
84 | if (mActiveDialog == null) {
85 | mActiveDialog = new AlertDialog.Builder(this)
86 | .setCancelable(false)
87 | .setMessage(R.string.msg_module_not_active)
88 | .setPositiveButton(R.string.btn_active, (dialog, which) -> {
89 | if (isInXposedEnvironment()) {
90 | openXposedInstaller();
91 | } else {
92 | openExposed();
93 | }
94 | })
95 | .setNegativeButton(R.string.btn_ignore, null)
96 | .create();
97 | mActiveDialog.show();
98 | } else {
99 | if (!mActiveDialog.isShowing()) {
100 | mActiveDialog.show();
101 | }
102 | }
103 | }
104 |
105 | private void openXposedInstaller() {
106 | String xposed = "de.robv.android.xposed.installer";
107 | if (Utils.isPackageInstalled(this, xposed)) {
108 | Intent intent = new Intent("de.robv.android.xposed.installer.OPEN_SECTION");
109 | if (getPackageManager().queryIntentActivities(intent, 0).isEmpty()) {
110 | intent = getPackageManager().getLaunchIntentForPackage(xposed);
111 | }
112 | if (intent != null) {
113 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
114 | .putExtra("section", "modules")
115 | .putExtra("fragment", 1)
116 | .putExtra("module", Constants.MODULE_PACKAGE_NAME);
117 | startActivity(intent);
118 | }
119 | } else {
120 | Toast.makeText(this, R.string.toast_xposed_not_installed, Toast.LENGTH_SHORT).show();
121 | }
122 | }
123 |
124 | private void openExposed() {
125 | String exposed = "me.weishu.exp";
126 | if (Utils.isPackageInstalled(this, exposed)) {
127 | Intent intent = new Intent(exposed + ".ACTION_MODULE_MANAGE");
128 | intent.setData(Uri.parse("package:" + Constants.MODULE_PACKAGE_NAME));
129 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
130 | startActivity(intent);
131 | } else {
132 | Toast.makeText(this, R.string.toast_exposed_not_installed, Toast.LENGTH_SHORT).show();
133 | }
134 | }
135 |
136 | public static class PrefsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {
137 | @Override
138 | public void onCreate(Bundle savedInstanceState) {
139 | super.onCreate(savedInstanceState);
140 | addPreferencesFromResource(R.xml.xposed_prefs);
141 | setWorldReadable();
142 | SwitchPreference hideAppIcon = (SwitchPreference) getPreferenceScreen().findPreference("hide_icon");
143 | hideAppIcon.setOnPreferenceChangeListener(this);
144 | }
145 |
146 | @Override
147 | public boolean onPreferenceChange(Preference preference, Object object) {
148 | if (KEY_HIDE_ICON.equals(preference.getKey())) {
149 | changeIconStatus(!(Boolean) object);
150 | }
151 | return true;
152 | }
153 |
154 | private void changeIconStatus(boolean isShow) {
155 | final ComponentName aliasName = new ComponentName(getActivity(), MainActivity.class.getName() + "Alias");
156 | final PackageManager packageManager = getActivity().getPackageManager();
157 | int status = isShow ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
158 | if (packageManager.getComponentEnabledSetting(aliasName) != status) {
159 | packageManager.setComponentEnabledSetting(aliasName, status, PackageManager.DONT_KILL_APP);
160 | }
161 | }
162 |
163 | @SuppressWarnings("ResultOfMethodCallIgnored")
164 | @SuppressLint("SetWorldReadable")
165 | private void setWorldReadable() {
166 | File prefsDir = new File(getActivity().getApplicationInfo().dataDir, "shared_prefs");
167 | File prefsFile = new File(prefsDir, getPreferenceManager().getSharedPreferencesName() + ".xml");
168 | if (prefsFile.exists()) {
169 | prefsFile.setReadable(true, false);
170 | }
171 | }
172 |
173 | @Override
174 | public void onPause() {
175 | super.onPause();
176 | setWorldReadable();
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/app/src/main/java/me/zjns/lovecloudmusic/hooker/SideBarHook.java:
--------------------------------------------------------------------------------
1 | package me.zjns.lovecloudmusic.hooker;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.widget.LinearLayout;
6 | import android.widget.TextView;
7 |
8 | import de.robv.android.xposed.XC_MethodHook;
9 | import me.zjns.lovecloudmusic.Constants;
10 | import me.zjns.lovecloudmusic.Utils;
11 | import me.zjns.lovecloudmusic.hooker.base.BaseHook;
12 |
13 | import static de.robv.android.xposed.XposedHelpers.callMethod;
14 | import static de.robv.android.xposed.XposedHelpers.findClass;
15 | import static de.robv.android.xposed.XposedHelpers.getBooleanField;
16 | import static de.robv.android.xposed.XposedHelpers.getObjectField;
17 | import static me.zjns.lovecloudmusic.Utils.findAndHookConstructor;
18 | import static me.zjns.lovecloudmusic.Utils.findAndHookMethod;
19 |
20 | public class SideBarHook extends BaseHook {
21 |
22 | private DrawerItem drawerItem;
23 | private Object mainActivity;
24 |
25 | private boolean disableSignJump;
26 | private boolean autoSign;
27 | private boolean isFromAutoSign = false;
28 | private boolean hideDot;
29 | private boolean hideTicket;
30 | private boolean hideVip;
31 | private boolean hideStore;
32 | private boolean hideGame;
33 | private boolean hideFree;
34 | private boolean hideNearBy;
35 | private boolean hideChildMode;
36 | private boolean hideCoupon;
37 | private boolean hideViewer;
38 |
39 | @Override
40 | protected void hookMain() {
41 | drawerItem = new DrawerItem();
42 | drawerItem.initItems();
43 | hookDrawerItem();
44 | hideRedDot();
45 | disableSignJump();
46 | noToggleAfterAutoSign();
47 | }
48 |
49 | @Override
50 | protected void initPrefs() {
51 | disableSignJump = prefs.getBoolean("disable_sign_jump_to_small", false);
52 | autoSign = prefs.getBoolean("auto_sign", false);
53 | hideDot = prefs.getBoolean("hide_dot", false);
54 | hideTicket = prefs.getBoolean("hide_item_live_ticket", false);
55 | hideVip = prefs.getBoolean("hide_item_vip", false);
56 | hideStore = prefs.getBoolean("hide_item_shop", false);
57 | hideGame = prefs.getBoolean("hide_item_game", false);
58 | hideFree = prefs.getBoolean("hide_item_free_data", false);
59 | hideNearBy = prefs.getBoolean("hide_item_nearby", false);
60 | hideChildMode = prefs.getBoolean("hide_item_baby", false);
61 | hideCoupon = prefs.getBoolean("hide_item_ticket", false);
62 | hideViewer = prefs.getBoolean("hide_item_singer", false);
63 | }
64 |
65 | private void hookDrawerItem() {
66 | findAndHookMethod("com.netease.cloudmusic.ui.MainDrawer", loader,
67 | "refreshDrawer",
68 | new DrawerItemHook());
69 | }
70 |
71 | private void hideRedDot() {
72 | Class> MessageBubbleView = findClass("com.netease.cloudmusic.ui.MessageBubbleView", loader);
73 | findAndHookMethod(View.class, "setVisibility", Integer.TYPE, new XC_MethodHook() {
74 | @Override
75 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
76 | Object obj = param.thisObject;
77 | if (obj.getClass() == MessageBubbleView) {
78 | boolean hasText = getBooleanField(obj, "mShowBubbleWithText");
79 | if (!hasText) {
80 | loadPrefs();
81 | if (hideDot) {
82 | param.args[0] = View.GONE;
83 | }
84 | }
85 | }
86 | }
87 | });
88 | }
89 |
90 | private void disableSignJump() {
91 | findAndHookConstructor("com.netease.cloudmusic.ui.MainDrawer", loader,
92 | findClass("com.netease.cloudmusic.activity.MainActivity", loader),
93 | new XC_MethodHook() {
94 | @Override
95 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
96 | mainActivity = param.args[0];
97 | }
98 | });
99 | findAndHookMethod("com.netease.cloudmusic.activity.ReactNativeActivity", loader,
100 | "a", Context.class, boolean.class, String.class,
101 | new XC_MethodHook() {
102 | @Override
103 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
104 | Object context = param.args[0];
105 | boolean bool = (Boolean) param.args[1];
106 | if (context == mainActivity && bool) {
107 | loadPrefs();
108 | if (!disableSignJump) return;
109 | param.setResult(null);
110 | }
111 | }
112 | });
113 | findAndHookMethod("com.netease.cloudmusic.ui.MainDrawer", loader,
114 | "updateSignIn", Boolean.TYPE, Boolean.TYPE,
115 | new XC_MethodHook() {
116 | @Override
117 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
118 | loadPrefs();
119 | if (!disableSignJump) return;
120 | Object obj = param.thisObject;
121 | boolean bool1 = (Boolean) param.args[0];
122 | boolean bool2 = (Boolean) param.args[1];
123 | boolean running = (Boolean) callMethod(obj, "isDoingSinginTask");
124 | if ((!bool2 || !running) && bool1) {
125 | TextView drawerUserSignIn = (TextView) getObjectField(obj, "drawerUserSignIn");
126 | drawerUserSignIn.setOnClickListener(null);
127 | drawerUserSignIn.setEnabled(false);
128 | drawerUserSignIn.setText("已签到");
129 | drawerUserSignIn.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
130 | }
131 | }
132 | });
133 | }
134 |
135 | private void noToggleAfterAutoSign() {
136 | findAndHookMethod("com.netease.cloudmusic.ui.MainDrawer", loader,
137 | "toggleDrawerMenu",
138 | new XC_MethodHook() {
139 | @Override
140 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
141 | if (isFromAutoSign) {
142 | param.setResult(null);
143 | isFromAutoSign = false;
144 | }
145 | }
146 | });
147 | }
148 |
149 | private final class DrawerItemHook extends XC_MethodHook {
150 | @Override
151 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
152 | loadPrefs();
153 | removeUselessItem(param);
154 | autoSign(param);
155 | }
156 |
157 | private void autoSign(MethodHookParam param) {
158 | Object obj = param.thisObject;
159 | TextView drawerUserSignIn = (TextView) getObjectField(obj, "drawerUserSignIn");
160 | if (disableSignJump && autoSign) {
161 | String text = drawerUserSignIn.getText().toString();
162 | if ("签到".equals(text)) {
163 | isFromAutoSign = true;
164 | drawerUserSignIn.performClick();
165 | Utils.showToast("云村清洁工帮你自动签到了!", true, 500L);
166 | }
167 | }
168 | }
169 |
170 | private void removeUselessItem(MethodHookParam param) {
171 | LinearLayout drawerContainer;
172 | LinearLayout dynamicContainer = null;
173 | drawerContainer = (LinearLayout) getObjectField(param.thisObject, "mDrawerContainer");
174 | if (versionName.compareTo(Constants.CM_VERSION_600) >= 0) {
175 | dynamicContainer = (LinearLayout) getObjectField(param.thisObject, "mDynamicContainer");
176 | }
177 | removeItemInner(drawerContainer);
178 | removeItemInner(dynamicContainer);
179 | }
180 |
181 | private void removeItemInner(LinearLayout container) {
182 | if (container == null) return;
183 | for (int i = 0; i < container.getChildCount(); i++) {
184 | View v = container.getChildAt(i);
185 | Object tag = v.getTag();
186 | if (tag != null && shouldRemove(tag)) {
187 | v.setVisibility(View.GONE);
188 | }
189 | }
190 | }
191 |
192 | private boolean shouldRemove(Object tag) {
193 | return tag == drawerItem.TICKET && hideTicket
194 | || tag == drawerItem.VIP && hideVip
195 | || tag == drawerItem.STORE && hideStore
196 | || tag == drawerItem.GAME && hideGame
197 | || tag == drawerItem.FREE && hideFree
198 | || tag == drawerItem.NEARBY && hideNearBy
199 | || tag == drawerItem.CHILD_MODE && hideChildMode
200 | || tag == drawerItem.DISCOUNT_COUPON && hideCoupon
201 | || tag == drawerItem.MUSICIAN_VIEWER && hideViewer;
202 | }
203 | }
204 |
205 | @SuppressWarnings("unused")
206 | private final class DrawerItem {
207 | private Object PROFILE;
208 | private Object AVATAR;
209 | private Object MESSAGE;
210 | private Object MUSICIAN;
211 | private Object MUSICIAN_CREATOR_CENTER;
212 | private Object PROFIT;
213 | private Object VIP;
214 | private Object TICKET;
215 | private Object STORE;
216 | private Object GAME;
217 | private Object FREE;
218 | private Object COLOR_RING;
219 | private Object MY_FRIEND;
220 | private Object NEARBY;
221 | private Object THEME;
222 | private Object IDENTIFY;
223 | private Object CREATOR_CENTER;
224 | private Object MY_ORDER;
225 | private Object CLOCK_PLAY;
226 | private Object SCAN;
227 | private Object ALARM_CLOCK;
228 | private Object VEHICLE_PLAYER;
229 | private Object CLASSICAL;
230 | private Object CHILD_MODE;
231 | private Object PRIVATE_CLOUD;
232 | private Object DISCOUNT_COUPON;
233 | private Object MUSICIAN_VIEWER;
234 | private Object SMALL_ICE;
235 | private Object YOUTH_MODE;
236 | private Object VOIBOX;
237 | private Object SETTING;
238 | private Object DIV1;
239 | private Object DIV2;
240 | private Object DIV3;
241 | private Object DIV4;
242 | private Object DYNAMIC_CONTAINER;
243 | private Object DYNAMIC_ITEM;
244 |
245 | private Class classDrawerItemEnum;
246 |
247 | private void initItems() {
248 | String className = "com.netease.cloudmusic.ui.MainDrawer$DrawerItemEnum";
249 | classDrawerItemEnum = findClass(className, loader);
250 | PROFILE = getItem("PROFILE");
251 | AVATAR = getItem("AVATAR");
252 | MESSAGE = getItem("MESSAGE");
253 | MUSICIAN = getItem("MUSICIAN");
254 | MUSICIAN_CREATOR_CENTER = getItem("MUSICIAN_CREATOR_CENTER");
255 | PROFIT = getItem("PROFIT");
256 | VIP = getItem("VIP");
257 | TICKET = getItem("TICKET");
258 | STORE = getItem("STORE");
259 | GAME = getItem("GAME");
260 | FREE = getItem("FREE");
261 | COLOR_RING = getItem("COLOR_RING");
262 | MY_FRIEND = getItem("MY_FRIEND");
263 | NEARBY = getItem("NEARBY");
264 | THEME = getItem("THEME");
265 | IDENTIFY = getItem("IDENTIFY");
266 | CREATOR_CENTER = getItem("CREATOR_CENTER");
267 | MY_ORDER = getItem("MY_ORDER");
268 | CLOCK_PLAY = getItem("CLOCK_PLAY");
269 | SCAN = getItem("SCAN");
270 | ALARM_CLOCK = getItem("ALARM_CLOCK");
271 | VEHICLE_PLAYER = getItem("VEHICLE_PLAYER");
272 | CLASSICAL = getItem("CLASSICAL");
273 | CHILD_MODE = getItem("CHILD_MODE");
274 | PRIVATE_CLOUD = getItem("PRIVATE_CLOUD");
275 | DISCOUNT_COUPON = getItem("DISCOUNT_COUPON");
276 | MUSICIAN_VIEWER = getItem("MUSICIAN_VIEWER");
277 | SMALL_ICE = getItem("SMALL_ICE");
278 | YOUTH_MODE = getItem("YOUTH_MODE");
279 | VOIBOX = getItem("VOIBOX");
280 | SETTING = getItem("SETTING");
281 | DIV1 = getItem("DIV1");
282 | DIV2 = getItem("DIV2");
283 | DIV3 = getItem("DIV3");
284 | DIV4 = getItem("DIV4");
285 | DYNAMIC_CONTAINER = getItem("DYNAMIC_CONTAINER");
286 | DYNAMIC_ITEM = getItem("DYNAMIC_ITEM");
287 | }
288 |
289 | @SuppressWarnings("unchecked")
290 | private Object getItem(String name) {
291 | try {
292 | return Enum.valueOf(classDrawerItemEnum, name);
293 | } catch (Throwable t) {
294 | return null;
295 | }
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------