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

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

        但nga的大神们总是有办法解决,一位大佬就提供了反和谐的方法:

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

         大致是将日服客户端的资源文件提取,替换掉国服客户端的资源文件,来完成角色立绘的反和谐。

        但方法是有了,帖子下方仍然有不少玩家难以反和谐,因为目前的一些手机厂商,为了用户的安全,禁止了很多权限,包括用户访问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()
        }

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

三、接收回调

    //返回授权状态
    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服,我们都可以支持。

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

  1. 用户没有安装国服客户端
  2. 用户安装了游戏,但从没有启动过(游戏只安装,不启动,是不会初始化资源的,不会创建这个目录)
  3. 我们第二步,弹出data目录向用户索取权限时,用户点击进入了下级文件夹,致使我们最终得到的uri不是android/data根目录,所以在此搜索不到游戏目录。

五:开始反和谐

《利用Android SAF(存储访问框架)进行游戏反和谐(伊甸园的骄傲)/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 SAF(存储访问框架)进行游戏反和谐(伊甸园的骄傲)/Android data目录的访问限制》

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

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

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

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

    原文作者:kaixuan_dashen
    原文地址: https://blog.csdn.net/kaixuan_dashen/article/details/119783260
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞