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