[摸鱼篇]记录一次Hook差劲的英语app拦截听力音频URL并重构播放操作
41
此app疑似完全外包开发,logcat不堪入目,程序员被自己写的逻辑跳转搞破防:
虽然看起来草率,实际用起来也是苦不堪言,作文输入框的长按菜单被block了,竟是要在一个自定义键盘一个个字母手敲作文。完美的防止粘贴方案其实是使用TextWatcher,起码还能获得单词补全提高效率,也不用去敲那个shit。虽然通过无障碍服务去检索屏幕上的EditText控件并使用设置文本的办法可以免注入绕过此限制(TextWatcher也能够阻止此方法),但一般没人想到,只能去敲那个{{github第10亿个仓库名}}。
在一套CET6任务里,作文写完了,接下来就是听力了。虽然高考英语考出了130+,分全扣在了作文上,但面对一套“一镜到底”的六级音频前期还是会变得很菜......于是在力所能及的范围内,去进行优化,允许读的慢一些,最好还能往回拉进度照顾一下年迈的大脑。
核心思路:劫持音频URL,ai生成并嵌入一个自定义的view去负责交互,为了提高开发效率,选择xposed模块,分发时,使用lspatch打包。
package com.sunight.bluedovehook;
import android.content.Context;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import java.lang.reflect.Field;
import java.util.Arrays;
public class HookInit implements IXposedHookLoadPackage {
public String globalAudioUrl;
private android.media.MediaPlayer[] mediaPlayer = {null};
private android.widget.Button playButton;
private android.widget.SeekBar seekBar;
private android.widget.TextView currentTimeView;
private android.widget.TextView totalTimeView;
private android.widget.TextView titleView;
private android.os.Handler handler;
private android.widget.Spinner speedSpinner; // 速度选择器
private java.util.List speedOptions = Arrays.asList(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f);
private float currentSpeed = 1.0f; // 当前播放速度
private int lastProgress = 0; // 保存进度
private android.app.Activity activity;
private android.content.Context context;
// 格式化时间方法
private String formatTime(int milliseconds) {
int seconds = milliseconds / 1000;
int minutes = seconds / 60;
seconds = seconds % 60;
return String.format("%02d:%02d", minutes, seconds);
}
private void initializeMediaPlayer(Context context) {
try {
mediaPlayer[0] = new android.media.MediaPlayer();
mediaPlayer[0].setDataSource(globalAudioUrl);
mediaPlayer[0].setAudioStreamType(android.media.AudioManager.STREAM_MUSIC);
mediaPlayer[0].prepareAsync();
mediaPlayer[0].setOnPreparedListener(new android.media.MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(android.media.MediaPlayer mp) {
// 设置播放速度
setPlaybackSpeed(currentSpeed);
// 恢复上次进度(如果有)
if (lastProgress > 0) {
mp.seekTo(lastProgress);
}
mp.start();
playButton.setText("暂停");
totalTimeView.setText(formatTime(mp.getDuration()));
seekBar.setMax(mp.getDuration());
titleView.setText("播放中...");
}
});
mediaPlayer[0].setOnCompletionListener(new android.media.MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(android.media.MediaPlayer mp) {
playButton.setText("播放");
seekBar.setProgress(0);
currentTimeView.setText("00:00");
titleView.setText("Listening Optimization");
lastProgress = 0; // 重置进度
}
});
} catch (Exception e) {
android.widget.Toast.makeText(context, "播放失败: " + e.getMessage(), android.widget.Toast.LENGTH_SHORT).show();
}
}
// 设置播放速度
private void setPlaybackSpeed(float speed) {
if (mediaPlayer[0] != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
try {
android.media.PlaybackParams params = new android.media.PlaybackParams();
params.setSpeed(speed);
mediaPlayer[0].setPlaybackParams(params);
currentSpeed = speed;
} catch (Exception e) {
XposedBridge.log("设置播放速度失败: " + e.getMessage());
}
}
}
public void resetPlayer() {
// 保存当前进度
if (mediaPlayer[0] != null) {
lastProgress = mediaPlayer[0].getCurrentPosition();
if (mediaPlayer[0].isPlaying()) {
mediaPlayer[0].stop();
}
mediaPlayer[0].release();
mediaPlayer[0] = null;
}
lastProgress = 0;
playButton.setText("播放");
seekBar.setProgress(0);
currentTimeView.setText("00:00");
totalTimeView.setText("00:00");
titleView.setText("Listening Optimization");
// 如果全局URL已更新,可以在这里开始新的播放
if (globalAudioUrl != null && !globalAudioUrl.isEmpty()) {
initializeMediaPlayer(context);
}
}
@Override
public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
if (BuildConfig.APPLICATION_ID.equals(lpparam.packageName)) {
XposedHelpers.findAndHookMethod(
MainActivity.class.getName(),
lpparam.classLoader,
"isModuleActivated",
XC_MethodReplacement.returnConstant(true));
}
XposedHelpers.findAndHookMethod("android.app.Activity", lpparam.classLoader,
"onCreate", android.os.Bundle.class, new XC_MethodHook() {
private android.widget.PopupWindow popupWindow;
private java.lang.Runnable updateSeekBar;
private int[] lastX = {500};
private int[] lastY = {1000};
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
activity = (android.app.Activity) param.thisObject;
context = activity;
final android.view.View rootView = activity.getWindow().getDecorView();
// 创建主容器
android.widget.LinearLayout container = createPlayerContainer(context);
// 创建播放器UI组件
createPlayerComponents(context, container);
// 初始化PopupWindow
initPopupWindow(context, container);
// 初始化Handler和Runnable
initHandler();
// 设置拖拽功能
setupDragListener(container);
// 显示播放器
showPlayer(rootView);
}
private android.widget.LinearLayout createPlayerContainer(Context context) {
android.widget.LinearLayout container = new android.widget.LinearLayout(context);
container.setOrientation(android.widget.LinearLayout.VERTICAL);
container.setPadding(dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16));
// 设置现代背景
android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable();
drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
drawable.setCornerRadius(dpToPx(context, 16));
drawable.setColor(0xFF2D3748); // 深蓝色背景
drawable.setStroke(dpToPx(context, 1), 0xFF4A5568); // 边框
container.setBackground(drawable);
return container;
}
private void createPlayerComponents(Context context, android.widget.LinearLayout container) {
// 标题栏
createTitleBar(context, container);
// 进度条区域
createProgressArea(context, container);
// 控制按钮区域
createControlArea(context, container);
// 速度控制区域
createSpeedControlArea(context, container);
}
private void createTitleBar(Context context, android.widget.LinearLayout container) {
// 标题容器
android.widget.LinearLayout titleContainer = new android.widget.LinearLayout(context);
titleContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
titleContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);
// 图标
android.widget.TextView iconView = new android.widget.TextView(context);
iconView.setText("😜");
iconView.setTextSize(18);
iconView.setPadding(0, 0, dpToPx(context, 8), 0);
// 标题
titleView = new android.widget.TextView(context);
titleView.setText("Listening Optimization");
titleView.setTextSize(16);
titleView.setTextColor(0xFFFFFFFF);
titleView.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);
titleContainer.addView(iconView);
titleContainer.addView(titleView);
container.addView(titleContainer);
// 分隔线
android.view.View divider = new android.view.View(context);
divider.setBackgroundColor(0xFF4A5568);
android.widget.LinearLayout.LayoutParams dividerParams = new android.widget.LinearLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(context, 1)
);
dividerParams.setMargins(0, dpToPx(context, 8), 0, dpToPx(context, 12));
divider.setLayoutParams(dividerParams);
container.addView(divider);
}
private void createProgressArea(Context context, android.widget.LinearLayout container) {
// 时间显示容器
android.widget.LinearLayout timeContainer = new android.widget.LinearLayout(context);
timeContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
timeContainer.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
));
// 当前时间
currentTimeView = new android.widget.TextView(context);
currentTimeView.setText("00:00");
currentTimeView.setTextSize(12);
currentTimeView.setTextColor(0xFFA0AEC0);
// 时间分隔符
android.widget.TextView timeSeparator = new android.widget.TextView(context);
timeSeparator.setText(" / ");
timeSeparator.setTextSize(12);
timeSeparator.setTextColor(0xFFA0AEC0);
// 总时间
totalTimeView = new android.widget.TextView(context);
totalTimeView.setText("00:00");
totalTimeView.setTextSize(12);
totalTimeView.setTextColor(0xFFA0AEC0);
timeContainer.addView(currentTimeView);
timeContainer.addView(timeSeparator);
timeContainer.addView(totalTimeView);
container.addView(timeContainer);
// 进度条
seekBar = new android.widget.SeekBar(context);
seekBar.setMax(100);
seekBar.setProgress(0);
android.widget.LinearLayout.LayoutParams seekBarParams = new android.widget.LinearLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
);
seekBarParams.setMargins(0, dpToPx(context, 4), 0, dpToPx(context, 12));
seekBar.setLayoutParams(seekBarParams);
container.addView(seekBar);
}
private void createControlArea(Context context, android.widget.LinearLayout container) {
android.widget.LinearLayout controlContainer = new android.widget.LinearLayout(context);
controlContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
controlContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);
// 播放按钮
playButton = createModernButton(context, "播放", 0xFF48BB78); // 绿色
android.widget.LinearLayout.LayoutParams playButtonParams = new android.widget.LinearLayout.LayoutParams(
0, android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1
);
playButtonParams.setMargins(0, 0, dpToPx(context, 8), 0);
playButton.setLayoutParams(playButtonParams);
// 重置按钮
android.widget.Button resetButton = createModernButton(context, "重置", 0xFFED8936); // 橙色
android.widget.LinearLayout.LayoutParams resetButtonParams = new android.widget.LinearLayout.LayoutParams(
0, android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1
);
resetButton.setLayoutParams(resetButtonParams);
controlContainer.addView(playButton);
controlContainer.addView(resetButton);
container.addView(controlContainer);
// 设置按钮点击事件
setupButtonListeners(context, resetButton);
}
// 新增速度控制区域
private void createSpeedControlArea(Context context, android.widget.LinearLayout container) {
android.widget.LinearLayout speedContainer = new android.widget.LinearLayout(context);
speedContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
speedContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);
speedContainer.setPadding(0, dpToPx(context, 8), 0, 0);
// 速度标签
android.widget.TextView speedLabel = new android.widget.TextView(context);
speedLabel.setText("播放速度: ");
speedLabel.setTextSize(12);
speedLabel.setTextColor(0xFFA0AEC0);
speedLabel.setPadding(0, 0, dpToPx(context, 8), 0);
// 速度选择器
speedSpinner = new android.widget.Spinner(context);
android.widget.ArrayAdapter<Float> adapter = new android.widget.ArrayAdapter<Float>(
context, android.R.layout.simple_spinner_item, speedOptions);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
speedSpinner.setAdapter(adapter);
speedSpinner.setSelection(2); // 默认1.0x
// 设置速度选择监听
speedSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, android.view.View view, int position, long id) {
float selectedSpeed = speedOptions.get(position);
setPlaybackSpeed(selectedSpeed);
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
speedContainer.addView(speedLabel);
speedContainer.addView(speedSpinner);
container.addView(speedContainer);
}
private android.widget.Button createModernButton(Context context, String text, int color) {
android.widget.Button button = new android.widget.Button(context);
button.setText(text);
button.setTextColor(0xFFFFFFFF);
button.setAllCaps(false);
button.setTextSize(14);
button.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);
// 设置现代背景
android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable();
drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
drawable.setCornerRadius(dpToPx(context, 8));
drawable.setColor(color);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
button.setElevation(dpToPx(context, 2));
}
button.setBackground(drawable);
// 设置点击效果
android.content.res.ColorStateList colorStateList = android.content.res.ColorStateList.valueOf(color);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
button.setBackgroundTintList(colorStateList);
}
return button;
}
private void setupButtonListeners(final Context context, android.widget.Button resetButton) {
// 播放按钮点击事件
playButton.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
handlePlayButtonClick(context);
}
});
// 重置按钮点击事件
resetButton.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
resetPlayer();
}
});
// 进度条变化监听
seekBar.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(android.widget.SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && mediaPlayer[0] != null) {
mediaPlayer[0].seekTo(progress);
currentTimeView.setText(formatTime(progress));
lastProgress = progress; // 保存进度
}
}
@Override
public void onStartTrackingTouch(android.widget.SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(android.widget.SeekBar seekBar) {}
});
}
private void handlePlayButtonClick(Context context) {
if (globalAudioUrl == null || globalAudioUrl.isEmpty()) {
android.widget.Toast.makeText(context, "请点击应用内音频播放按钮", android.widget.Toast.LENGTH_SHORT).show();
return;
}
if (mediaPlayer[0] == null) {
initializeMediaPlayer(context);
} else if (mediaPlayer[0].isPlaying()) {
// 暂停时保存当前进度
lastProgress = mediaPlayer[0].getCurrentPosition();
mediaPlayer[0].pause();
playButton.setText("播放");
titleView.setText("Listening Optimization");
} else {
// 继续播放时恢复进度
mediaPlayer[0].start();
playButton.setText("暂停");
titleView.setText("播放中...");
}
}
private void initPopupWindow(Context context, android.widget.LinearLayout container) {
popupWindow = new android.widget.PopupWindow(
container,
dpToPx(context, 280),
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
);
// 关键设置:禁用所有自动关闭行为
popupWindow.setOutsideTouchable(false);
popupWindow.setFocusable(false);
popupWindow.setTouchable(true);
popupWindow.setBackgroundDrawable(null);
popupWindow.setAnimationStyle(0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
popupWindow.setElevation(dpToPx(context, 8));
}
}
private void initHandler() {
handler = new android.os.Handler();
updateSeekBar = new java.lang.Runnable() {
@Override
public void run() {
if (mediaPlayer[0] != null && mediaPlayer[0].isPlaying()) {
int currentPosition = mediaPlayer[0].getCurrentPosition();
seekBar.setProgress(currentPosition);
currentTimeView.setText(formatTime(currentPosition));
lastProgress = currentPosition; // 持续保存进度
}
handler.postDelayed(this, 1000);
}
};
handler.post(updateSeekBar);
}
private void setupDragListener(android.widget.LinearLayout container) {
android.view.View.OnTouchListener dragListener = new android.view.View.OnTouchListener() {
private float startRawX;
private float startRawY;
private int initialX;
private int initialY;
@Override
public boolean onTouch(android.view.View v, android.view.MotionEvent event) {
int action = event.getAction();
if (action == android.view.MotionEvent.ACTION_DOWN) {
startRawX = event.getRawX();
startRawY = event.getRawY();
initialX = lastX[0];
initialY = lastY[0];
return true;
} else if (action == android.view.MotionEvent.ACTION_MOVE) {
float deltaX = event.getRawX() - startRawX;
float deltaY = event.getRawY() - startRawY;
lastX[0] = initialX + (int) deltaX;
lastY[0] = initialY + (int) deltaY;
popupWindow.update(lastX[0], lastY[0], -1, -1);
return true;
} else if (action == android.view.MotionEvent.ACTION_UP) {
return true;
}
return false;
}
};
container.setOnTouchListener(dragListener);
titleView.setOnTouchListener(dragListener);
}
private void showPlayer(final android.view.View rootView) {
rootView.post(new java.lang.Runnable() {
@Override
public void run() {
if (popupWindow != null && !popupWindow.isShowing()) {
popupWindow.showAtLocation(
rootView,
android.view.Gravity.TOP | android.view.Gravity.START,
lastX[0],
lastY[0]
);
}
}
});
}
// dp转px方法
private int dpToPx(Context context, int dp) {
float density = context.getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
});
XposedHelpers.findAndHookMethod("com.lancoo.answer.widget.audioPlayView.TopicAudioView$Config",
lpparam.classLoader,
"access$700",
lpparam.classLoader.loadClass("com.lancoo.answer.widget.audioPlayView.TopicAudioView$Config"),
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Object configInstance = param.args[0];
if (configInstance != null) {
XposedBridge.log("=== access$700 方法调用 ===");
XposedBridge.log("Config实例: " + configInstance.toString());
// 获取所有字段
Field[] fields = configInstance.getClass().getDeclaredFields();
XposedBridge.log("字段总数: " + fields.length);
// 遍历所有字段并打印String类型的值
for (Field field : fields) {
field.setAccessible(true);
try {
Object value = field.get(configInstance);
if (value instanceof String) {
XposedBridge.log("字段: " + field.getName() + " = " + value);
} else if (value != null) {
XposedBridge.log("字段: " + field.getName() + " = " + value + " (类型: " + value.getClass().getSimpleName() + ")");
}
if (field.getName().equals("audioUrl")) {
globalAudioUrl = (String)value;
}
} catch (Exception e) {
XposedBridge.log("无法读取字段: " + field.getName());
}
}
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
XposedBridge.log("access$700 返回值: " + param.getResult());
}
});
XposedHelpers.findAndHookMethod("com.lancoo.answer.widget.audioPlayView.TopicAudioView", lpparam. classLoader, "play", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
android.widget.Toast.makeText(context, "successfully hijacked✓", android.widget.Toast.LENGTH_SHORT).show();
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 获取被hook的对象实例
Object topicAudioView = param.thisObject;
XposedHelpers.callMethod(topicAudioView, "pause");
resetPlayer();
}
});
}
}
代码基本ai生成,费时间的是方案设计与逆向的过程、定位logcat,activity记录等找出要hook的参数。这么难用的APP必然是没什么混淆加密的,因此整个过程十分顺利没有压力,最后的效果也非常不错,成功将听力音频劫持到自己的播放器上。