diff --git a/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.java b/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.java
deleted file mode 100644
index 60ebc71e9b..0000000000
--- a/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * QAuxiliary - An Xposed module for QQ/TIM
- * Copyright (C) 2019-2022 qwq233@qwq2333.top
- * https://github.com/cinit/QAuxiliary
- *
- * This software is non-free but opensource software: you can redistribute it
- * and/or modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation; either
- * version 3 of the License, or any later version and our eula as published
- * by QAuxiliary contributors.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * and eula along with this software. If not, see
- *
- * .
- */
-package cc.ioctl.hook.experimental;
-
-import android.app.Activity;
-import android.os.Environment;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import cc.ioctl.util.HookUtils;
-import cc.ioctl.util.HostInfo;
-import com.afollestad.materialdialogs.MaterialDialog;
-import de.robv.android.xposed.XposedHelpers;
-import io.github.qauxv.base.IUiItemAgent;
-import io.github.qauxv.base.annotation.FunctionHookEntry;
-import io.github.qauxv.base.annotation.UiItemAgentEntry;
-import io.github.qauxv.config.ConfigItems;
-import io.github.qauxv.config.ConfigManager;
-import io.github.qauxv.dsl.FunctionEntryRouter.Locations.Auxiliary;
-import io.github.qauxv.hook.CommonConfigFunctionHook;
-import io.github.qauxv.util.Initiator;
-import io.github.qauxv.util.Log;
-import io.github.qauxv.util.QQVersion;
-import io.github.qauxv.util.SyncUtils;
-import io.github.qauxv.util.Toasts;
-import io.github.qauxv.util.dexkit.CAppConstants;
-import io.github.qauxv.util.dexkit.DexKit;
-import io.github.qauxv.util.dexkit.DexKitTarget;
-import java.io.File;
-import java.lang.reflect.Field;
-import java.util.Locale;
-import kotlin.Unit;
-import kotlin.jvm.functions.Function3;
-import kotlinx.coroutines.flow.MutableStateFlow;
-
-@FunctionHookEntry
-@UiItemAgentEntry
-public class FileRecvRedirect extends CommonConfigFunctionHook {
-
- public static final FileRecvRedirect INSTANCE = new FileRecvRedirect();
- private boolean inited = false;
-
- private Field TARGET_FIELD = null;
-
- private FileRecvRedirect() {
- super(SyncUtils.PROC_ANY & ~(SyncUtils.PROC_MSF | SyncUtils.PROC_UNITY | SyncUtils.PROC_MINI),
- new DexKitTarget[]{CAppConstants.INSTANCE});
- }
-
- @NonNull
- @Override
- public String getName() {
- return "下载文件重定向";
- }
-
- @Nullable
- @Override
- public String[] getExtraSearchKeywords() {
- return new String[]{"下载重定向"};
- }
-
- @NonNull
- @Override
- public String[] getUiItemLocation() {
- return Auxiliary.EXPERIMENTAL_CATEGORY;
- }
-
- @Nullable
- @Override
- public MutableStateFlow getValueState() {
- return null;
- }
-
- @NonNull
- @Override
- public Function3 getOnUiItemClickListener() {
- return (agent, activity, view) -> {
- LinearLayout mRoot = new LinearLayout(activity);
- mRoot.setOrientation(LinearLayout.VERTICAL);
-
- CheckBox start = new CheckBox(activity);
- start.setText("开启下载重定向");
- start.setChecked(isEnabled());
-
- mRoot.addView(start);
-
- TextView tv = new TextView(activity);
- tv.setText("如果提示 目录可能无效 请检查是否已经给 " + HostInfo.getAppName() + " 授予了读写权限");
- mRoot.addView(tv);
-
- EditText PathSet = new EditText(activity);
- PathSet.setText(getRedirectPath());
- mRoot.addView(PathSet);
- PathSet.setVisibility(start.isChecked() ? View.VISIBLE :View.GONE);
-
- start.setOnCheckedChangeListener((buttonView, isChecked) -> PathSet.setVisibility(isChecked ? View.VISIBLE :View.GONE));
-
- MaterialDialog dialog = new MaterialDialog(activity,MaterialDialog.getDEFAULT_BEHAVIOR());
- dialog.title(null,"下载文件重定向");
- dialog.positiveButton(null, "保存", materialDialog -> {
- String Path = PathSet.getText().toString();
- if (!CheckPathIsAvailable(Path)){
- Toasts.show(activity,"目录可能无效");
- return null;
- }
- setEnabled(start.isChecked());
- if (start.isChecked()){
- setRedirectPathAndEnable(PathSet.getText().toString());
- }
- Toasts.show(activity,"已保存,请重启QQ");
- return null;
- });
- dialog.negativeButton(null, "取消", materialDialog -> null);
- dialog.getView().contentLayout.setCustomView(null);
- dialog.getView().contentLayout.addCustomView(null,mRoot,false,false,false);
-
- dialog.show();
-
- return Unit.INSTANCE;
- };
- }
- private static String CacheDefPath;
- @Override
- public boolean initOnce() throws Exception {
- CacheDefPath = getDefaultPath();
- String redirectPath = getRedirectPath();
- if (redirectPath != null) {
- inited = doSetPath(redirectPath);
- return inited;
- } else {
- return false;
- }
- }
-
- private boolean doSetPath(String str) {
-
- try {
- if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_2_8)){
- if(!inited){
- HookUtils.hookAfterIfEnabled(this, XposedHelpers.findMethodBestMatch(Initiator.load("com.tencent.mobileqq.vfs.VFSAssistantUtils"), "getSDKPrivatePath", String.class), param -> {
- String getResult = (String) param.getResult();
- File checkAvailable = new File(getResult);
- if (checkAvailable.exists() && checkAvailable.isFile())return;//如果文件存在则不处理,防止已下载的文件出现异常
- if (getResult.startsWith(CacheDefPath)){
- //在QQ尝试获取文件目录时进行替换
- param.setResult(getRedirectPath()+getResult.substring(CacheDefPath.length()));
- }
- });
- try {
- HookUtils.hookAfterIfEnabled(this,XposedHelpers.findMethodBestMatch(Initiator.load("com.tencent.guild.api.msg.impl.GuildMsgApiImpl"),"getNTKernelExtDataPath"),param -> {
- param.setResult(HostInfo.getApplication().getExternalCacheDir().getParentFile().getAbsolutePath()+"/Tencent/QQfile_recv/");
- });
- }catch (Exception ignored){
-
- }
-
- }
- }else {
- Field[] fields = DexKit.requireClassFromCache(CAppConstants.INSTANCE).getFields();
- if (TARGET_FIELD == null) {
- for (Field field : fields) {
- field.setAccessible(true);
- Object value = field.get(null);
- String path = String.valueOf(value);
- if (path.toLowerCase(Locale.ROOT).endsWith("file_recv/")) {
- TARGET_FIELD = field;
- break;
- }
- }
- }
- TARGET_FIELD.setAccessible(true);
- TARGET_FIELD.set(null, str);
- }
- return true;
- } catch (Exception e) {
- Log.e(e);
- return false;
- }
- }
- public boolean CheckPathIsAvailable(String Path){
- File f = new File(Path);
- f = f.getParentFile();
- if (f != null){
- f.mkdirs();
- return f.listFiles() != null;
- }
- return false;
- }
-
- public String getDefaultPath() {
- if (HostInfo.isTim()) {
- return Environment.getExternalStorageDirectory().getAbsolutePath()
- + "/Tencent/TIMfile_recv/";
- } else {
- if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_2_8)) {
- return HostInfo.getApplication()
- .getExternalFilesDir(null).getParent() + "/Tencent/QQfile_recv";
- } else {
- return Environment.getExternalStorageDirectory().getAbsolutePath()
- + "/Tencent/QQfile_recv";
- }
- }
- }
-
- @Nullable
- public String getRedirectPath() {
- return ConfigManager.getDefaultConfig().getString(ConfigItems.qn_file_recv_redirect_path);
- }
-
- public void setRedirectPathAndEnable(String path) {
- try {
- ConfigManager cfg = ConfigManager.getDefaultConfig();
- cfg.putString(ConfigItems.qn_file_recv_redirect_path, path);
- cfg.putBoolean(ConfigItems.qn_file_recv_redirect_enable, true);
- cfg.save();
- inited = doSetPath(path);
- } catch (Exception e) {
- Log.e(e);
- }
- }
-
- @Override
- public boolean isInitialized() {
- return inited;
- }
-
- @Override
- public boolean isInitializationSuccessful() {
- return isInitialized();
- }
-
- @Override
- public boolean isEnabled() {
- return ConfigManager.getDefaultConfig().getBooleanOrFalse(ConfigItems.qn_file_recv_redirect_enable);
- }
-
- /**
- * Still follow the rule only apply if it is already inited.
- *
- * @param enabled if true set to config value, otherwise restore to default value
- */
- @Override
- public void setEnabled(boolean enabled) {
- ConfigManager cfg = ConfigManager.getDefaultConfig();
- cfg.putBoolean(ConfigItems.qn_file_recv_redirect_enable, enabled);
- cfg.save();
- if (inited) {
- if (enabled) {
- String path = getRedirectPath();
- if (path != null) {
- inited = doSetPath(path);
- }
- } else {
- doSetPath(getDefaultPath());
- }
- }
- }
-}
diff --git a/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.kt b/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.kt
new file mode 100644
index 0000000000..9dd12d6e9f
--- /dev/null
+++ b/app/src/main/java/cc/ioctl/hook/experimental/FileRecvRedirect.kt
@@ -0,0 +1,255 @@
+/*
+ * QAuxiliary - An Xposed module for QQ/TIM
+ * Copyright (C) 2019-2023 QAuxiliary developers
+ * https://github.com/cinit/QAuxiliary
+ *
+ * This software is non-free but opensource software: you can redistribute it
+ * and/or modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version and our eula as published
+ * by QAuxiliary contributors.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * and eula along with this software. If not, see
+ *
+ * .
+ */
+
+package cc.ioctl.hook.experimental
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.os.Environment
+import android.view.View
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.setPadding
+import cc.ioctl.util.HostInfo
+import cc.ioctl.util.hookAfterIfEnabled
+import com.github.kyuubiran.ezxhelper.utils.Log
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import de.robv.android.xposed.XposedHelpers
+import io.github.qauxv.base.IUiItemAgent
+import io.github.qauxv.base.annotation.FunctionHookEntry
+import io.github.qauxv.base.annotation.UiItemAgentEntry
+import io.github.qauxv.config.ConfigItems
+import io.github.qauxv.config.ConfigManager
+import io.github.qauxv.dsl.FunctionEntryRouter
+import io.github.qauxv.hook.CommonConfigFunctionHook
+import io.github.qauxv.util.Initiator
+import io.github.qauxv.util.QQVersion
+import io.github.qauxv.util.SyncUtils
+import io.github.qauxv.util.Toasts
+import io.github.qauxv.util.dexkit.CAppConstants
+import io.github.qauxv.util.dexkit.DexKit
+import kotlinx.coroutines.flow.StateFlow
+import java.io.File
+import java.lang.reflect.Field
+import java.util.Locale
+
+@FunctionHookEntry
+@UiItemAgentEntry
+object FileRecvRedirect : CommonConfigFunctionHook(SyncUtils.PROC_ANY and (SyncUtils.PROC_MSF or SyncUtils.PROC_UNITY or SyncUtils.PROC_MINI).inv(), arrayOf(CAppConstants)) {
+ private var inited = false
+ private lateinit var defaultCachePath: String
+ private var targetField: Field? = null
+ override val name: String
+ get() = "下载文件重定向"
+ override val valueState: StateFlow?
+ get() = null
+ override val onUiItemClickListener: (IUiItemAgent, Activity, View) -> Unit
+ @SuppressLint("SetTextI18n")
+ get() = { _, activity, _ ->
+ val root = LinearLayout(activity)
+ root.orientation = LinearLayout.VERTICAL
+
+ val cb_enable = CheckBox(activity)
+ cb_enable.text = "开启下载重定向"
+ cb_enable.isChecked = isEnabled
+ root.addView(cb_enable)
+
+ val tv_note = TextView(activity)
+ tv_note.text = "如果提示 目录无效 请检查是否已经给 ${HostInfo.getAppName()} 授予了读写权限"
+ root.addView(tv_note)
+
+ val ll_path = LinearLayout(activity)
+ ll_path.orientation = LinearLayout.HORIZONTAL
+ ll_path.visibility = if (cb_enable.isChecked) View.VISIBLE else View.GONE
+ root.addView(ll_path)
+
+ val et_path = EditText(activity)
+ et_path.setText(getRedirectPath())
+ ll_path.addView(et_path, LinearLayout.LayoutParams(0, WRAP_CONTENT, 1F))
+
+ val btn_select = Button(activity)
+ btn_select.text = "..."
+ ll_path.addView(btn_select, WRAP_CONTENT, WRAP_CONTENT)
+
+ cb_enable.setOnCheckedChangeListener { _, isChecked ->
+ ll_path.visibility = if (isChecked) View.VISIBLE else View.GONE
+ }
+ btn_select.setOnClickListener {
+ doSelectPath(activity, et_path.text.toString()) { path ->
+ et_path.setText(path)
+ }
+ }
+
+ MaterialAlertDialogBuilder(activity)
+ .setTitle("下载文件重定向")
+ .setPositiveButton("保存") { _, _ ->
+ val path = et_path.text.toString()
+ if (checkPathAvailable(path).not()) {
+ Toasts.show(activity, "目录无效")
+ return@setPositiveButton
+ }
+ isEnabled = cb_enable.isChecked
+ if (isEnabled) {
+ setRedirectPathAndEnable(path)
+ }
+ Toasts.show(activity, "已保存,请重启 ${HostInfo.getAppName()}")
+ }.setNegativeButton(android.R.string.cancel, null)
+ .setView(root)
+ .show()
+ }
+
+ override fun initOnce(): Boolean {
+ defaultCachePath = getDefaultPath()
+ val redirectPath = getRedirectPath()
+ return if (redirectPath != null) {
+ inited = doSetPath(redirectPath)
+ inited
+ } else {
+ false
+ }
+ }
+
+ override val uiItemLocation: Array
+ get() = FunctionEntryRouter.Locations.Auxiliary.EXPERIMENTAL_CATEGORY
+
+ override var isEnabled: Boolean
+ get() = ConfigManager.getDefaultConfig().getBooleanOrFalse(ConfigItems.qn_file_recv_redirect_enable)
+ set(enabled) {
+ val cfg = ConfigManager.getDefaultConfig()
+ cfg.putBoolean(ConfigItems.qn_file_recv_redirect_enable, enabled)
+ cfg.save()
+ if (inited) {
+ if (enabled) {
+ getRedirectPath()?.let { inited = doSetPath(it) }
+ } else {
+ doSetPath(getDefaultPath())
+ }
+ }
+ }
+
+ private fun setRedirectPathAndEnable(path: String) {
+ try {
+ val cfg = ConfigManager.getDefaultConfig()
+ cfg.putString(ConfigItems.qn_file_recv_redirect_path, path)
+ cfg.putBoolean(ConfigItems.qn_file_recv_redirect_enable, true)
+ cfg.save()
+ inited = doSetPath(path)
+ } catch (e: Exception) {
+ Log.e(e)
+ }
+ }
+
+ override val isInitializationSuccessful: Boolean
+ get() = isInitialized
+
+ override val isInitialized: Boolean
+ get() = inited
+
+ private fun getRedirectPath() =
+ ConfigManager.getDefaultConfig().getString(ConfigItems.qn_file_recv_redirect_path)
+
+ private fun doSetPath(path: String): Boolean {
+ try {
+ if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_2_8)) {
+ if (inited.not()) {
+ hookAfterIfEnabled(XposedHelpers.findMethodBestMatch(Initiator.load("com.tencent.mobileqq.vfs.VFSAssistantUtils"), "getSDKPrivatePath", String::class.java)) { param ->
+ val result = param.result as String
+ val file = File(result)
+ if (file.exists() && file.isFile) return@hookAfterIfEnabled // 如果文件存在则不处理,防止已下载的文件出现异常
+ if (result.startsWith(defaultCachePath))
+ param.result = getRedirectPath() + result.substring(defaultCachePath.length)
+ }
+ try {
+ hookAfterIfEnabled(XposedHelpers.findMethodBestMatch(Initiator.load("com.tencent.guild.api.msg.impl.GuildMsgApiImpl"), "getNTKernelExtDataPath", *arrayOf())) { param ->
+ param.result = HostInfo.getApplication().externalCacheDir!!.parentFile!!.absolutePath + "/Tencent/QQfile_recv/"
+ }
+ } catch (ignored: Exception) {
+ }
+ }
+ } else {
+ val fields = DexKit.requireClassFromCache(CAppConstants).fields
+ if (targetField == null) {
+ for (field in fields) {
+ field.isAccessible = true
+ val value = field.get(null)
+ val path = value!!.toString()
+ if (path.lowercase(Locale.ROOT).endsWith("file_recv/")) {
+ targetField = field
+ break
+ }
+ }
+ }
+ targetField?.isAccessible = true
+ targetField?.set(null, path)
+ }
+ return true
+ } catch (e: Exception) {
+ Log.e(e)
+ return false
+ }
+ }
+
+ private fun getDefaultPath() =
+ when {
+ HostInfo.isTim() ->
+ Environment.getExternalStorageDirectory().absolutePath + "/Tencent/TIMfile_recv/"
+
+ HostInfo.requireMinQQVersion(QQVersion.QQ_8_2_8) ->
+ HostInfo.getApplication().getExternalFilesDir(null)!!.parent!! + "/Tencent/QQfile_recv"
+
+ else ->
+ Environment.getExternalStorageDirectory().absolutePath + "/Tencent/QQfile_recv"
+ }
+
+ private fun checkPathAvailable(path: String): Boolean {
+ val file = File(path)
+ return file.exists() && file.isDirectory && file.canWrite()
+ }
+
+ private fun doSelectPath(context: Context, path: String, onSelect: (String) -> Unit) {
+ val file = File(path)
+ val dirs = file.listFiles()?.filter { it.isDirectory }
+ val items = (listOf("..") + (dirs?.map { it.name } ?: listOf(""))).toTypedArray()
+ MaterialAlertDialogBuilder(context)
+ .setCustomTitle(TextView(context).apply {
+ text = path
+ setPadding(40)
+ })
+ .setItems(items) { _, index ->
+ if (index == 0 ) {
+ doSelectPath(context, file.parent ?: "/", onSelect)
+ } else if (dirs != null) {
+ doSelectPath(context, dirs[index - 1].absolutePath, onSelect)
+ } else {
+ doSelectPath(context, path, onSelect)
+ }
+ }.setPositiveButton(android.R.string.ok) {_, _ ->
+ onSelect(path)
+ }.setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+}
\ No newline at end of file