最近在玩一个手游,伊甸园的骄傲,日服客户端中的一些立绘稍微有些暴露,国服上线后不出意外的被和谐(添加布料)了

但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-permissionandroid: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_PERMISSIONor Intent.FLAG_GRANT_WRITE_URI_PERMISSIONor Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSIONor 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.dataif (requestCode == REQUEST_CODE_FOR_DIR && data.data.also { uri = it } != null) {contentResolver.takePersistableUriPermission(uriTree!!, Intent.FLAG_GRANT_READ_URI_PERMISSIONor 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.GONEbtn_antiHarmony.isEnabled = truebtn_antiHarmony.setText("一键反和谐")Snackbar.make(this, ll_rootView, "你的手机上没有安装国服伊甸园的骄傲,无法进行反和谐。",BaseTransientBottomBar.LENGTH_LONG).setAction("确定", View.OnClickListener { }).show()return}// 开始往游戏目录内复制文件ll_progress.visibility = View.VISIBLEprogressBar.progress = 0btn_antiHarmony.isEnabled = falsebtn_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服,我们都可以支持。

如果两个都没有找到啊,那么只有三种可能:

  1. 用户没有安装国服客户端
  2. 用户安装了游戏,但从没有启动过(游戏只安装,不启动,是不会初始化资源的,不会创建这个目录)
  3. 我们第二步,弹出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.arg2tv_jindu.text = msg.arg1.toString() + "/" + msg.arg2.toString()}1 -> {AlertDialog.Builder(this@MainActivity).setTitle("修改成功!").setMessage("伊甸园的骄傲已成功反和谐!请进入游戏查看!为防止后续游戏更新导致反和谐失效,请加入qq群享受永久免费更新!").setPositiveButton("确定") { dialogInterface, i ->}.show()ll_progress.visibility = View.GONEbtn_antiHarmony.isEnabled = truebtn_antiHarmony.setText("一键反和谐")}}}}

最后,修改成功后,可以推广一波qq群,哈哈。

唯一缺点就是,文件替换速度比较慢。

软件效果:

虽然不赚钱,但是能帮到别人还是开心的~

目前我的这个群已经有一百多人了,不过,转念一想,如此轻松就可以让用户授予我android/data目录的权限,就可以随意操作任意应用的数据文件,是不是也有很大风险呢?如果让恶意应用获得这种权限,那真的是不敢想象鸭。。

利用Android SAF(存储访问框架)进行游戏反和谐(伊甸园的骄傲)/Android data目录的访问限制相关推荐

  1. android SAF存储访问框架

    Android 4.4(API 级别 19)引入了Storage Access Framework存储访问框架 (SAF),SAF 让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档.图像以 ...

  2. Android11 无Root 访问data目录实现、Android11访问data目录、Android11解除data目录限制、Android11 data空白解决

    Android11 无Root 访问data目录 实现 正文开始 关于Android11权限变化 作为普通安卓用户该如何方便快速地访问Android/data目录 开发者该如何实现无ROOT访问Dat ...

  3. Android 进阶——Framework 核心之Android Storage Access Framework(SAF)存储访问框架机制详解(二)

    文章大纲 引言 一.DirectFragment 1.当选中DirectoryFragment中RecyclerView的Item时 2.选中DirectoryFragment中RecyclerVie ...

  4. Android 进阶——Framework 核心之Android Storage Access Framework(SAF)存储访问框架机制详解(一)

    文章大纲 引言 一.Android Storage Access Framework 二.Storage Access Framework 的主要角色成员 1.Document Provider 文件 ...

  5. Android存储访问框架的使用

    存储访问框架,简称:SAF, 就是系统文件选择器+文件操作API.先选择文件,在用文件操作API处理文件.系统文件选择器,就和Windows的文件选择框一样. 其实绝大多数app,都不会使用这个东西, ...

  6. Android11(30)/Android10(29)分区存储-存储访问框架(SAF)

    概述 存储访问框架(SAF)是在Android 4.4(API 级别 19)引入的.借助 SAF,用户可轻松打开文档.图像及其他文件. 存储访问框架包含三部分: 文档提供程序 - 文档提供程序以 Do ...

  7. android文件存储框架,Android 存储访问框架|undefined

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? Android 4.4(API 级别 19)引入了存储访问框架 (SAF).SAF 让用户能够在其所有首选文档存储提供程 ...

  8. android内存修改 跳一跳,Android版微信跳一跳小游戏利用技术手段达到高分的操作方法...

    本文主要来讲个个好玩的东西,近来微信刚出的跳一跳微信小程序的游戏很火,看到很多人都达到了二三百分就各种刷朋友圈了. 甩手一个表情 最终我们达到的分数却是这样的: 羡慕吧 一定会有人拍手叫好," ...

  9. Android 数据存储 利用SQLiteDatabase实现简单的学生管理

    转载请注明出处:明桑Android 这是作为上一篇Android 数据存储 如何搞定SQLite Database的实例练习,之所以单独列出来是因为除了数据库方面的知识,还涉及其它方面的知识,所以就写 ...

  10. 【错误记录】Android 分区存储下的 SD 卡应用专属外部存储空间目录访问 ( 需手动创建应用专属外部存储空间目录 )

    文章目录 一.报错信息 二.解决方案 一.报错信息 开发时 , 需要向外置 SD 卡中拷贝一些文件 , 应用读取这些文件 , 进行相关配置 ; 但是 Android 系统 , 并不会主动为应用创建文件 ...

最新文章

  1. retrofit2 发送json数据_SQLmap JSON 格式的数据注入
  2. php中curl的详解
  3. VS2010下安装配置OpenCV2.4.4
  4. Python_XlrdXlwt
  5. (二十二)深入浅出TCPIP之实战篇—用c++开发一个http服务器
  6. 服务化改造的云上利器 | 阿里云 EDAS 重大升级发布
  7. 在hdfs文件系统中创建目录连接失败_分布式文件系统HDFS
  8. xcode cocos2dx 3.x mac工程 当assert(cond)触发断点,但cond却为0
  9. 数据从机房迁移到阿里ECS弹性云
  10. 回归分析常数项t值没有显著异于零怎么办_洋蜜蜂统计辅导专题:回归分析关键词统计量须知...
  11. 闲来无事研究一下酷狗缓存文件kgtemp的加密方式
  12. 记录解决Win10底部任务栏转圈圈问题的过程(Windows假死)
  13. 网页认证上网服务器无响应,portal认证失败,网络故障或者portal服务器没有响应排查方法...
  14. javaweb羽毛球教练场地预约管理系统ssm
  15. WIFI基础入门--802.11--成帧细节(管理帧)--5
  16. android代码混淆个人总结及踩坑
  17. 功能强大的发卡网源码+支付接口超多
  18. 神经网络算法用什么软件,神经网络算法的基本原理
  19. MySQL增量备份实战
  20. 计算机应用飞机自动驾驶,计算机应用技术论文自动驾驶空中失效的影响分析与决策...

热门文章

  1. SLA是什么意思 ?
  2. 谈谈扫码支付的实现流程
  3. 关于报错:There is already ‘什么Controller‘ bean method的解决方法
  4. 关于java的文件操作
  5. 自定义ViewGroup——自定义布局
  6. 2016-2017 ACM-ICPC, South Pacific Regional Contest (SPPC 16)
  7. 马云卸任演讲全文:青山不改 绿水长流 后会有期
  8. python求斜边上的高_直角三角形斜边上的高如何求?
  9. c语言中eof的作用,C语言中EOF是什么意思?
  10. 有学生表Student, 课程表Course, 学生选课表StudentCourse