前阵子在思考怎么进步开发效率,以取得更多的摸鱼时刻,在App提测阶段,发现了这样一个现象:

Jenkins打包测验APK,上传到Fir,往钉钉群丢一个下载地址。测验崽看到消息后,点开下载地址,把APK装置到测验机上,有两种装置办法:

  • 电脑直接下载,然后再想办法装置到手机上;
  • 手机扫码下载,然后在手机上完结装置;

众所周知,一个测验崽一般都会随身配备多台手机,所以每次发新包,他们都要在覆盖装置APK上,浪费不少的时刻。

不得不夸测验崽有耐心啊,孜孜不倦做了这么久这种单调重复的装置作业,也没半点怨言。乐(xi)于(huan)助(zhuang)人(bi) 的杰哥知道了,肯定是要出手相助的,整点活,帮他们也进步下作业效率,究竟 独摸鱼,不如众摸鱼

【效率提升】快速安装APK的两个

思路的话,分别从 电脑装置手机装置 两个视点入手,直接开搞~


0x1、电脑快速装置

拆解电脑装置APK的流程:浏览器翻开下载URL → 点击Download → 经过下述办法的一种装置到手机上:

  • 手机登录微信、QQ或其它带文件传送的APP,发送APK到手机上装置。
  • 手机接电脑,翻开 文件传输,把文件复制到手机上,再在手机上点装置。
  • 手机接电脑,电脑装置XX手机助手,右键APK直接装置。

上述是普通人的装置办法,安卓崽有自己独特的装置办法 → 运用adb指令来装置

adb install -r xxx.apk

运用adb指令前要敞开 USB调试方式,敞开办法如下(部分机型可能有差异,可善用搜索引擎):

  • 翻开手机设置;
  • 找到关于手机中的版别号;
  • 连续点击5次,进入开发者方式;
  • 找到 开发者选项,翻开USB调试开关即可;

安卓崽一般都会配Android SDK的环境变量,就不需求别的整个ADB了,没有的童鞋能够到官网下个:platform-tools工具包,解压后能够看到adb.exe等文件:

【效率提升】快速安装APK的两个

接着自己配个环境变量,能够键入:adb –version 来验证是否装备成功。

【效率提升】快速安装APK的两个

准备就绪,开始整脚本~

① 写一个拖拽装置APK的bat脚本

这儿没啥难度,直接给出可用脚本代码,注释写的很翔实,应该能看懂,看不懂的部分能够评论区留言~

:: 让当前批处理窗口支撑UTF-8,便是防止中文乱码
chcp 65001
:: 从下一行开始封闭回显
@echo off
:: 1、拖拽的apk途径
set apk_path=%1
:: 2、判断APK途径是否有传入,比方用户直接运转脚本就不会传这个参数
:check_apk 
if not exist "%apk_path%" (
    echo "APK文件不存在,请把APK文件拖拽到这个脚本上!"
    goto :error
)
:: 3、检查adb指令是否可用,2>&1 代表将stderr重定向到stdout(标准输出流)
:check_adb
adb --version >nul 2>&1
:: 假如指令履行成功会回来0,neq代表:不等于
if %ERRORLEVEL% neq 0 (
    echo "adb.exe不存在,请先增加此文件到该目录下,或许装备PATH环境变量!"
    goto :error 
)
:: 4、装置APK
:install
echo "装置 %apk_path%..."
adb install -r "%apk_path%"
if %ERRORLEVEL% neq 0 ( 
    echo "装置失败..."
    goto :error
) 
echo "装置成功,你能够在安卓设备上翻开此APP啦~"
:error
pause

脚本运用办法:(把apk直接拖到bat脚本上等候装置成功即可~)

【效率提升】快速安装APK的两个


② 写一个主动下载APK的bat脚本

由于没有fir的账号,就没去看有是否有供给下载API了,直接看接口恳求规矩,捣鼓出APK的下载URL。F12直接抓包:

【效率提升】快速安装APK的两个

触发APK下载前会调这个接口,302重定向,然后呼应头 Location 指向的便是APK的下载地址:

【效率提升】快速安装APK的两个

再往前走,看哪里能够凑齐这些参数,能够看到掉这个接口能够取得对应的参数:

http://download.appmeta.cn/短链?referer=下载域名

【效率提升】快速安装APK的两个

接着打两个包,看下载url的差异:

http://download.appmeta.cn/apps/xxx/install?short=xxx&download_token=xxx&release_id=6478xxx
http://download.appmeta.cn/apps/xxx/install?short=xxx&download_token=xxx&release_id=6476xxx

不难看出只有release_id是改变的,其它都是写死的,那要做的事情其实就两步:

  • 访问下载参数的URL,然后解析json提取release_id;
  • 拼接下载url,访问并下载APK

这个解析提取release_id可难倒我了,由于BAT脚本中,原生并不支撑运用正则表达式进行字符串提取

【效率提升】快速安装APK的两个

试了好一会儿的findstr正则匹配和for循环,都没有把它给抠出来…

【效率提升】快速安装APK的两个

遇事不决ChatGPT,在它的建议下,尝试借助 VBScript来完结子串的提取:

【效率提升】快速安装APK的两个

然后就遇到 json里包括双引号,导致切割成多个参数的问题,运转一向报错:Microsoft VBScript 编译器错误: 短少语句

【效率提升】快速安装APK的两个

调了N久也没解决,索性直接把双引号干掉算了:

【效率提升】快速安装APK的两个

成功把release_id给抠出来了,接着拼接下载url又出问题了,由于url里有 &,这玩意是 特别字符,需求加上一个 转义字符润饰,然后又由于我在前面设置 setlocal enabledelayedexpansion 敞开了变量推迟,所以字符串拼接变成这样:

【效率提升】快速安装APK的两个

然后便是 curl 恳求这个download_url,抠出恳求头 Location 指向的 apk的真实下载地址 了:

【效率提升】快速安装APK的两个

最终运用 call 指令调用下前面写的装置APK的脚本,传入apk文件名,然后做一些文件整理作业~

【效率提升】快速安装APK的两个

运转看下实际效果:

【效率提升】快速安装APK的两个

一键完结下载装置,简直不要太爽,此处能够有掌声~

【效率提升】快速安装APK的两个

别的,假如需求一起装置到多个手机上的需求,能够改下装置部分的脚本,adb devices 取得连接设备的序列号,然后循环调用adb install 就好啦。顺带给出脚本文件,咱们可根据自己的实际情况改着玩:

chcp 65001
@echo off
:: 敞开变量推迟
setlocal enabledelayedexpansion
:: 下载装备文件
set config_file=response.json
:: VBScript暂时文件
set vb_script=vbscript.vbs
:: 提取released_id的正则
set regex=master:\{id:(\w+),
:: apk文件名
set apk_file=test.apk
:: 1、取得恳求装备信息
set config_url=http://download.appmeta.cn/短链?referer=下载域名
curl %config_url% > %config_file%
::2、读取json内容
set "content="
for /f "usebackq delims=" %%i in (%config_file%) do (
    set "content=!content!!%%i"
)
:: 将双引号干掉,不然下述传参会有问题
set "content=%content:"=%"
::3、创建VBScript暂时文件运用正则提取released_id
echo Set objRegEx = New RegExp > %vb_script%
echo objRegEx.Pattern = "%regex%" >> %vb_script%
echo Set matches = objRegEx.Execute("%content%") >> %vb_script%
echo For Each match in matches >> %vb_script%
echo     Wscript.Echo match.SubMatches(0) >> %vb_script%
echo Next >> %vb_script%
:: 运转VBScript并捕获输出结果
for /f "delims=" %%i in ('cscript //nologo %vb_script%') do (
    set "release_id=%%i"
)
::4、拼接下载url,这儿&是特别字符,需求在它的前面加上^转义字符
set base_url=http://download.appmeta.cn/apps/xxx/install?short=xxx^&download_token=xxx^&release_id=
set download_url=!base_url!!release_id!
echo !download_url!
::5、恳求下载地址,提取恳求头Location中的下载地址
for /f "delims=" %%a in ('curl -I "!download_url!" ^| find "Location"') do set location=%%a
::
echo !location:~10!
::6、保存apk文件,这儿~10用于截断前面的Location:得到正确的下载地址
curl !location:~10! -o %apk_file%
::7、下载结束,调用装置脚本
call adb_install.bat %apk_file%
::8、整理产生的文件
echo 装置结束,整理生成的垃圾文件...
echo %config_file% 
del %config_file%
del %vb_script%
del %apk_file%
endlocal

Tips:Bat脚本最大的长处便是不需求别的装置环境,windows直接履行,缺点便是有些语法单调难懂,写起来比较繁琐,远不如Python这类脚本语言写起来舒服。比方这儿的代码,我折腾了将近一天,并且仍是在ChatGPT的加持下,假如换成Python,10分钟不用就给你肝出来了~


0x2、手机快速装置

虽然上面供给了一键下载装置的脚本,但关于普通人来说,并不算友爱,得:下载adb装备环境变量 + 手机敞开USB调试,并且手机还得接着数据线。帮人帮到底,接着看下 手机装置 这个视点怎么搞。

【效率提升】快速安装APK的两个

拆解手机装置APK的流程:手机浏览器扫码 → 点击Download按钮下载 → 等候下载完结点击APK装置。

优化思路

  • 1、浏览器扫码完全没必要,由于其实都是指向固定的URL,直接写死就好
  • 2、能否完结 主动点击Download按钮 下载APK?
  • 3、能否 监听APK下载进度,下载完结 主动触发装置

2333,完全能够写一个 内置WebView浏览器的APP 来完结上面的操作啊,先搭个简略的UI:

【效率提升】快速安装APK的两个

十分简练,按需挑选环境点击下载装置能够看到包的版别信息然后开始主动下载

WebView加载URL就不用说了,调下 loadUrl() 就完事了,难点是怎么提取页面上的版别信息,以及点击Download按钮~


① 获取页面版别信息&点击Download按钮

经过 evaluateJavascript() 办法调用下js就好了,电脑翻开APK下载地址,F12看下页面源码:

【效率提升】快速安装APK的两个

能够经过css挑选器快速定位到版别信息相关的两个元素,直接在控制台履行下述代码:

【效率提升】快速安装APK的两个

两个span就被抠出来了,而这儿只关心它的innerText,转成Array调下map办法生成字符串数组:

【效率提升】快速安装APK的两个

能够,拿到需求的版别信息了,在 evaluateJavascript() 的回调里打个断点:

【效率提升】快速安装APK的两个

能够看到数据是String类型,而非字符串数组,这儿不用处理直接展示就好。

然后是点击Download按钮,也简略,css挑选定位到结点,然后调下click()办法就好,直接补全代码~

【效率提升】快速安装APK的两个

运转看看效果(顶部也能够看到下载进度~):

【效率提升】快速安装APK的两个


② 监听APK下载完结

监听下载完结的话简略,动态注册个下载完结的播送就好:

【效率提升】快速安装APK的两个

然后还需求调用WebView的 setDownloadListener() 对下载行为进行一些定制,比方设置APK的下载方位及名称,是否显现下载完结的告诉等:

【效率提升】快速安装APK的两个

别的,保存到Downloads目录,需求申请下读写权限,不然装置的时分部分手时机提示文件损坏:

【效率提升】快速安装APK的两个

【效率提升】快速安装APK的两个

运转后再次点击下载,能够监听到下载完结,文件下载到对应的方位,接着便是触发主动装置了~


③ 触发主动装置APK

其实便是发起 装置APK的Intent,顺带把APK的途径也传过去,这儿有个 版别适配的小坑

Android 7.0(API 24) 后强制启用了所谓的StrictMode的策略,咱们的APP无法直接对外露出 file:// 类型的Uri,假如仍是经过 Uri.fromFile(file) 办法来露出的话就会引发 FileUriExposedException。官方供给了 FileProvider,它生成的Uri会以 content:// 的方式分享给其它APP运用,这种方式的Uri能够让其它APP暂时取得读取(Read)和写入(Write)权限文件的权限。

这个不是什么新东西了,网上材料一堆,感兴趣的自己搜下,这儿直接一笔带过~

1)装备访问途径信息的xml → 在res/xml目录下直接新建xml文件,如:file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!-- 外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录途径 -->
    <external-path name="apk_file_path" path="." />
</paths>

2)AndroidManifest.xml增加FileProvider → 定义一个authorities,增加一个meta-data指向上面的xml文件:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="cp.provider.authority"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

接着在下载完结播送的onReceive()补全跳转装置APK的代码:

【效率提升】快速安装APK的两个

然后,运用内装置APK还需求一个权限 REQUEST_INSTALL_PACKAGES,它是签名权限,不能在运用中恳求这个权限,只需在AndroidManifest.xml清单文件中声明。

【效率提升】快速安装APK的两个

装置时体系会弹出这样的授权对话框:

【效率提升】快速安装APK的两个

点击设置或持续会进入授权页:

【效率提升】快速安装APK的两个

授权后,后续就不会弹这个了,下载完直接触发体系装置APK的页面:

【效率提升】快速安装APK的两个

同样是 一键完结下载装置,并且不用手机连着电脑,更舒服了,此处能够有掌声~

【效率提升】快速安装APK的两个

当然,还需求点一下更新,要连这一步都省去的话,能够再写一个 AccessibilityService 无障碍服务,监听履行体系装置包名,识别到更新、确认等按钮时,触发主动点击,这是偷懒到极致了啊!!!

最终给出完整代码,感兴趣的朋友能够没事的时分自己着手改着玩,感觉也能算个KPI?哈哈哈~

class MainActivity : AppCompatActivity() {
    companion object {
        private const val TEST_APK_URL = "测验环境APK的下载地址"
        private const val RELEASE_APK_URL = "正式环境APK的下载地址"
    }
    private lateinit var mTypeRg: RadioGroup
    private lateinit var mDownloadBt: Button
    private lateinit var mContentWv: WebView
    private lateinit var mApkInfoTv: TextView
    private var mApkUrl = TEST_APK_URL
    private var mApkFile: String? = null
    private lateinit var dm: DownloadManager
    private val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
    private val mDownloadCompleteReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            mDownloadBt.text = "下载"
            mDownloadBt.isEnabled = true
            val installIntent: Intent
            val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), mApkFile)
            // 兼容
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 7.0+以上版别
                installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
                val apkUri = FileProvider.getUriForFile(this@MainActivity, "cp.provider.authority", file)
                installIntent.data = apkUri
                installIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION // 暂时权限
            } else {
                installIntent = Intent(Intent.ACTION_VIEW)
                installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                installIntent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
            }
            startActivity(installIntent)
        }
    }
    private val mPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { callback ->
            var isGrantAll = true
            callback.entries.forEach {
                isGrantAll = isGrantAll and it.value
            }
            if (isGrantAll) {
                Toast.makeText(this.applicationContext, "文件读写权限授权成功", Toast.LENGTH_SHORT).show()
                mDownloadBt.text = "下载中..."
                mDownloadBt.isEnabled = false
                mContentWv.loadUrl(mApkUrl)
            } else {
                Toast.makeText(this.applicationContext, "为了软件正常运用,请给予相关权限", Toast.LENGTH_SHORT).show()
            }
        }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        registerReceiver(mDownloadCompleteReceiver, filter)
        dm = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        mTypeRg = findViewById<RadioGroup?>(R.id.rg_type).apply {
            setOnCheckedChangeListener { _, checkedId ->
                mApkUrl = if (checkedId == R.id.rb_test) TEST_APK_URL else RELEASE_APK_URL
            }
        }
        mDownloadBt = findViewById<Button?>(R.id.bt_download_install).apply {
            setOnClickListener {
                mPermissionLauncher.launch(arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE))
            }
        }
        mApkInfoTv = findViewById(R.id.tv_apk_info)
        mContentWv = findViewById<WebView?>(R.id.wv_content).apply {
            settings.apply {
                javaScriptEnabled = true
                javaScriptCanOpenWindowsAutomatically = true
                allowFileAccess = true
                setSupportZoom(true)
            }
            webViewClient = object : WebViewClient() {
                override fun onPageFinished(view: WebView?, url: String?) {
                    mContentWv.evaluateJavascript("Array.from(document.querySelectorAll('div.release-info > p > span')).map(element => element.innerText)") {
                        mApkInfoTv.text = it.replace("\\n", "")
                        mContentWv.evaluateJavascript("document.querySelector('#actions > button').click()", null)
                    }
                }
            }
            setDownloadListener { url, _, _, mimetype, _ ->
                mApkFile = "${if(mApkUrl == TEST_APK_URL) "Test_" else "Release_"}${SimpleDateFormat("yyyyMMdd_hh_mm_ss").format(Date())}.apk"
                dm.enqueue(DownloadManager.Request(Uri.parse(url)).apply {
                    setMimeType(mimetype)
                    setVisibleInDownloadsUi(true)   // 显现下载UI
                    setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)  // 下载完结后会显现相应的告诉
                    allowScanningByMediaScanner()   // 答应被体系扫描到
                    setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, mApkFile)    // 设置文件保存途径
                })
            }
        }
    }
}

Tips:其实在电脑装置那里咱们已经把下载接口给抠出来了,这儿能够直接用OkHttp等库来模仿恳求下载APK。而笔者还故意用 WebView主动化 的方案来完结下载,是由于许多时分官方纷歧定会供给API接口,这就需求咱们自行逆向API接口,一般都是费时吃力的,投入产出比较低,究竟花那么多时刻破解一个接口又能怎样。还不如直接主动化,两三下就搞完,比方适配 蒲公英 只需求修改下js~

【效率提升】快速安装APK的两个