????????最近在玩一个手游,伊甸园的骄傲,日服客户端中的一些立绘稍微有些暴露,国服上线后不出意外的被和谐(添加布料)了
? ? ? ? 但nga的大神们总是有办法解决,一位大佬就提供了反和谐的方法:
?????????大致是将日服客户端的资源文件提取,替换掉国服客户端的资源文件,来完成角色立绘的反和谐。
????????但方法是有了,帖子下方仍然有不少玩家难以反和谐,因为目前的一些手机厂商,为了用户的安全,禁止了很多权限,包括用户访问data目录的权限,这使得用户必须root手机之后才能进入data目录进行操作,大大提高了反和谐的操作成本。
????????正好我最近也看到了一个使用SAF(Storage Access Framework)框架访问安卓data目录的文章(https://blog.csdn.net/qq_17827627/article/details/113931692),然后,我的兴趣就被提起来了。
制作一个一键反和谐工具,方便广大玩家。
Storage Access Framework是Android系统提供给用户和开发者的一个文件选择的辅助工具,高版本的安卓系统还允许通过SAF请求任意某个文件目录的权限(如Android/data,或任意应用程序的私有目录)。
官方文档:https://developer.android.com/training/data-storage/shared/documents-files
(文档中说到ACTION_OPEN_DOCUMENT_TREE这个action是5.0添加的,但是我实际测试,发现安卓6.0的mumu模拟器并不支持使用该action请求某一目录权限)
一、首先,申请所有文件管理权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
?二、然后需要一个Intent打开SAF文件管理界面,索取权限
//获取指定目录的权限
fun startFor(path: String, context: Activity, REQUEST_CODE_FOR_DIR: Int) {
val uri = changeToUri(path)
val parse: Uri = Uri.parse(uri)
val intent = Intent("android.intent.action.OPEN_DOCUMENT_TREE")
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse)
}
context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR)
}
索取权限之前提示用户,让用户进入下个界面后点击底部按钮
申请android/data的目录权限:
btn_antiHarmony.setOnClickListener {
AlertDialog.Builder(this)
.setTitle("游戏资源目录权限申请")
.setMessage("请在接下来弹出的界面中,直接点击底部“使用此文件夹”按钮,授予我们访问游戏资源目录的必要权限。")
.setPositiveButton("确定") { dialogInterface, i ->
FileUriUtils.startFor("android/data", this, REQUEST_CODE_FOR_DIR)
}
.setNegativeButton("取消", DialogInterface.OnClickListener { dialogInterface, i -> })
.show()
}
三、接收回调
//返回授权状态
override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
var uri: Uri?
if (data == null) {
return
}
uriTree = data.data
if (requestCode == REQUEST_CODE_FOR_DIR && data.data.also { uri = it } != null) {
contentResolver.takePersistableUriPermission(
uriTree!!, Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startAntiHarmony()
}
}
当从上个界面返回时,我们就知道是否已成功获得权限 使用自己的变量记录data.data的数据,这里面保存的是我们得到授权的文件夹路径
四、反和谐开始前的检测
var filter: List<DocumentFile> = mutableListOf()
fun startAntiHarmony() {
val root = DocumentFile.fromTreeUri(this, uriTree!!)
filter = root!!.listFiles().filter {
it.uri.path!!.endsWith(GUANFU_PACKAGE)
|| it.uri.path!!.endsWith(BLIBLI_PACKAGE)
}
if (filter.isNullOrEmpty()) {
ll_progress.visibility = View.GONE
btn_antiHarmony.isEnabled = true
btn_antiHarmony.setText("一键反和谐")
Snackbar.make(
this, ll_rootView, "你的手机上没有安装国服伊甸园的骄傲,无法进行反和谐。",
BaseTransientBottomBar.LENGTH_LONG
).setAction("确定", View.OnClickListener { }).show()
return
}
// 开始往游戏目录内复制文件
ll_progress.visibility = View.VISIBLE
progressBar.progress = 0
btn_antiHarmony.isEnabled = false
btn_antiHarmony.setText("正在进行中...")
startThread()
}
首先,DocumentFile.fromTreeUri()这个方法是系统的一个api,它可以将我们前面获取到的uri转为DocumentFile对象,因为我们采用SAF的方案,后续我们必须也只能通过DocumentFile来操作文件。
?这个时候获取到的root对象,其实就是android/data根目录的对象了。
此时我们需要查询当前文件夹内拥有的全部文件夹,并且找出游戏目录。
val GUANFU_PACKAGE = "com.eastgalaxy.ydydja.android"
val BLIBLI_PACKAGE = "com.bilibili.ydydja.bili"
这两个就是伊甸园的骄傲,国服游戏目录,因为国服有两个渠道,两个客户端,一个是taptap下载的官方服,一个是哔哩哔哩下载的B服,我们都可以支持。
如果两个都没有找到啊,那么只有三种可能:
- 用户没有安装国服客户端
- 用户安装了游戏,但从没有启动过(游戏只安装,不启动,是不会初始化资源的,不会创建这个目录)
- 我们第二步,弹出data目录向用户索取权限时,用户点击进入了下级文件夹,致使我们最终得到的uri不是android/data根目录,所以在此搜索不到游戏目录。
五:开始反和谐
?在这之前,肯定要先把日服的角色立绘资源文件导入到项目中。
首先要自我复制,把assets中的资源文件全部拷贝到用户的sd卡中。因为assets中的文件不能直接作为file对象使用。
复制完毕,就准备替换了,filter就是前面我们筛选查找出的游戏目录,因为游戏有两个渠道,所以filter可能有两个元素,需要考虑到。
fun startThread() {
Thread(Runnable {
// 先开始自我复制
copyAssetsFiles(this, "eden_jp", filesDir.path + "/eden_jp")
val sourceDF = DocumentFile.fromFile(File("file:///android_asset/eden_jp"))
Log.i(TAG, "sourceUri = ${sourceDF.uri}")
filter.forEachIndexed { index, documentFile ->
Log.i(TAG, "index = $index, filterUri = ${documentFile.uri} ")
}
Log.i(TAG, "isGrantAndroidData = ${isGrantAndroidData()}")
Log.i(TAG, "自我复制成功")
filter.forEach {
val targetDF = it.findFile("files")!!.findFile("Config")!!.findFile("res")!!
Log.i(TAG, "开始替换")
Log.i(TAG, "目标目录:${targetDF.uri}")
val copyDirectoryWithContent = FileManager(this).copyDirectoryWithContent(
RawFile(
Root.DirRoot(File(filesDir.path + "/eden_jp")),
BadPathSymbolResolutionStrategy.ThrowAnException
), ExternalFile(
this,
BadPathSymbolResolutionStrategy.ThrowAnException,
Root.DirRoot(CachingDocumentFile(this, targetDF))
), true, updateFunc = { x, y ->
handler.sendMessage(handler.obtainMessage(0, x, y))
Log.i(TAG, "updateFunc : x=$x, y=$y")
return@copyDirectoryWithContent false
})
Log.i(TAG, "替换完成, copyDirectoryWithContent = $copyDirectoryWithContent")
Snackbar.make(
this, ll_rootView, "由于你手机装有多个国服客户端,即将开始反和谐下个版本",
BaseTransientBottomBar.LENGTH_LONG
).setAction("确定", View.OnClickListener { }).show()
}
// 删除自我复制的资源
FileUtil.deleteFile(filesDir.path + "/eden_jp")
handler.sendEmptyMessage(1)
}).start()
}
?替换的源,就是刚才自我复制出来的文件,替换的目标,需要用findFile方法去一级一级找出来。
然后就可以开始替换了。
这里的文件操作,我使用了github上一个大佬的库,https://github.com/xbl3/Fuck-Storage-Access-Framework_K1rakishou
因为我们这里需要把原本的File文件体系,复制替换到SAF的DocumentFile文件体系,SAF提供的api不能说不好用,只能说非常难用。
使用这个库之后,就非常简单。
updateFunc 回调方法可以告诉我们一共有多少文件,当前处理了多少,使得我们可以很方便的使用进度条展示给用户。
六:刷新展示
val handler: Handler = object : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
0 -> {
progressBar.setProgress(msg.arg1)
progressBar.max = msg.arg2
tv_jindu.text = msg.arg1.toString() + "/" + msg.arg2.toString()
}
1 -> {
AlertDialog.Builder(this@MainActivity)
.setTitle("修改成功!")
.setMessage("伊甸园的骄傲已成功反和谐!请进入游戏查看!为防止后续游戏更新导致反和谐失效,请加入qq群享受永久免费更新!")
.setPositiveButton("确定") { dialogInterface, i ->
}
.show()
ll_progress.visibility = View.GONE
btn_antiHarmony.isEnabled = true
btn_antiHarmony.setText("一键反和谐")
}
}
}
}
最后,修改成功后,可以推广一波qq群,哈哈。
唯一缺点就是,文件替换速度比较慢。
软件效果:
?
虽然不赚钱,但是能帮到别人还是开心的~
目前我的这个群已经有一百多人了,不过,转念一想,如此轻松就可以让用户授予我android/data目录的权限,就可以随意操作任意应用的数据文件,是不是也有很大风险呢?如果让恶意应用获得这种权限,那真的是不敢想象鸭。。
|