前阵子在思考怎么进步开发效率,以取得更多的摸鱼时刻,在App提测阶段,发现了这样一个现象:
Jenkins打包测验APK,上传到Fir,往钉钉群丢一个下载地址。测验崽看到消息后,点开下载地址,把APK装置到测验机上,有两种装置办法:
- ① 电脑直接下载,然后再想办法装置到手机上;
- ② 手机扫码下载,然后在手机上完结装置;
众所周知,一个测验崽一般都会随身配备多台手机,所以每次发新包,他们都要在覆盖装置APK上,浪费不少的时刻。
不得不夸测验崽有耐心啊,孜孜不倦做了这么久这种单调重复的装置作业,也没半点怨言。乐(xi)于(huan)助(zhuang)人(bi) 的杰哥知道了,肯定是要出手相助的,整点活,帮他们也进步下作业效率,究竟 独摸鱼,不如众摸鱼!
思路的话,分别从 电脑装置 和 手机装置 两个视点入手,直接开搞~
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等文件:
接着自己配个环境变量,能够键入:adb –version 来验证是否装备成功。
准备就绪,开始整脚本~
① 写一个拖拽装置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的bat脚本
由于没有fir的账号,就没去看有是否有供给下载API了,直接看接口恳求规矩,捣鼓出APK的下载URL。F12直接抓包:
触发APK下载前会调这个接口,302重定向,然后呼应头 Location 指向的便是APK的下载地址:
再往前走,看哪里能够凑齐这些参数,能够看到掉这个接口能够取得对应的参数:
http://download.appmeta.cn/短链?referer=下载域名
接着打两个包,看下载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脚本中,原生并不支撑运用正则表达式进行字符串提取。
试了好一会儿的findstr正则匹配和for循环,都没有把它给抠出来…
遇事不决ChatGPT,在它的建议下,尝试借助 VBScript来完结子串的提取:
然后就遇到 json里包括双引号,导致切割成多个参数的问题,运转一向报错:Microsoft VBScript 编译器错误: 短少语句
调了N久也没解决,索性直接把双引号干掉算了:
成功把release_id给抠出来了,接着拼接下载url又出问题了,由于url里有 &
,这玩意是 特别字符,需求加上一个 转义字符润饰,然后又由于我在前面设置 setlocal enabledelayedexpansion 敞开了变量推迟,所以字符串拼接变成这样:
然后便是 curl 恳求这个download_url,抠出恳求头 Location 指向的 apk的真实下载地址 了:
最终运用 call 指令调用下前面写的装置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的流程:手机浏览器扫码 → 点击Download按钮下载 → 等候下载完结点击APK装置。
优化思路:
- 1、浏览器扫码完全没必要,由于其实都是指向固定的URL,直接写死就好
- 2、能否完结 主动点击Download按钮 下载APK?
- 3、能否 监听APK下载进度,下载完结 主动触发装置?
2333,完全能够写一个 内置WebView浏览器的APP 来完结上面的操作啊,先搭个简略的UI:
十分简练,按需挑选环境,点击下载装置,能够看到包的版别信息,然后开始主动下载。
WebView加载URL就不用说了,调下 loadUrl() 就完事了,难点是怎么提取页面上的版别信息,以及点击Download按钮~
① 获取页面版别信息&点击Download按钮
经过 evaluateJavascript() 办法调用下js就好了,电脑翻开APK下载地址,F12看下页面源码:
能够经过css挑选器快速定位到版别信息相关的两个元素,直接在控制台履行下述代码:
两个span就被抠出来了,而这儿只关心它的innerText,转成Array调下map办法生成字符串数组:
能够,拿到需求的版别信息了,在 evaluateJavascript() 的回调里打个断点:
能够看到数据是String类型,而非字符串数组,这儿不用处理直接展示就好。
然后是点击Download按钮,也简略,css挑选定位到结点,然后调下click()办法就好,直接补全代码~
运转看看效果(顶部也能够看到下载进度~):
② 监听APK下载完结
监听下载完结的话简略,动态注册个下载完结的播送就好:
然后还需求调用WebView的 setDownloadListener()
对下载行为进行一些定制,比方设置APK的下载方位及名称,是否显现下载完结的告诉等:
别的,保存到Downloads目录,需求申请下读写权限,不然装置的时分部分手时机提示文件损坏:
运转后再次点击下载,能够监听到下载完结,文件下载到对应的方位,接着便是触发主动装置了~
③ 触发主动装置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还需求一个权限 REQUEST_INSTALL_PACKAGES,它是签名权限,不能在运用中恳求这个权限,只需在AndroidManifest.xml清单文件中声明。
装置时体系会弹出这样的授权对话框:
点击设置或持续会进入授权页:
授权后,后续就不会弹这个了,下载完直接触发体系装置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~