工作中打包好的APK文件经常需要签名才能安装,公司没有提供签名APK所需的私钥,每次签名都需要先上传到签名服务器上,再下载签名后的APK文件,这样手动操作很繁琐,遂萌生出写个一键签名的脚本,这里考虑用gradle脚本来实现,在APK打包好之后可以自动上传至服务器签名然后自动下载。
签名服务器接口分析
编写gradle插件需要groovy,与java是兼容的,这样我们可以直接用OkHttp框架更方便地实现各种网络请求。由于没有现成的签名服务API接口,只能自己通过fiddler抓包来一步步分析请求参数。
首先是模拟登录
在签名服务器登录页面输入用户名和密码,观察抓包的情况。可以看到请求头以及表单信息
请求头信息 表单信息
有了这些参数,接下来我们就可以手动模拟登录。
FormBody formBody = new FormBody.Builder()
.add("return", "index.php")
.add("username", username)
.add("password", password)
.build();
Request request = new Request.Builder()
.post(formBody)
.url(url)
.addHeader("User-Agent", USER_AGENT)
.build();
Response response = mOkHttpClient.newCall(request).execute();
模拟登录成功后就可以获取到包含登录信息的Cookie,后面的上传APK文件请求中需要携带这个Cookie。OkHttp3提供了cookieJar的方法来实现Cookie的缓存。
OkHttpClient.Builder builder = new OkHttpClient.Builder()
builder.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.cookieJar(new CookieJar() {
//使用Map缓存Cookie
private final Map<String, List<Cookie>> cookieStore = new HashMap<>()
@Override
void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url.host(), cookies)
}
@Override
List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url.host())
return cookies != null ? cookies : new ArrayList<Cookie>()
}
})
mOkHttpClient = builder.build();
模拟网页上传文件
浏览器是用Multipart/form-data上传文件的,下图是抓包网页上传文件的请求头和请求体信息。请求头中包含Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFFpuNGUcr8mOaedw
这一行,其中multipart/form-data
是表单方式上传文件,----WebKitFormBoundaryFFpuNGUcr8mOaedw
是随机生成的分隔符boundary,在下面详细的请求体数据中可以看到这个。
请求体中包含了两处数据,一个是自定义名称APC_UPLOAD_PROGRESS
以及它的值,再就是名称为APK文件二进制数据。
用OkHttp模拟相同的请求信息,调用APIsetType(MultipartBody.FORM)
设置请求头Content-Type,就不需要我们手动设置boundary了。此外OkHttp提供了addFormDataPart
的API可以方便的构造表单数据。
MediaType mediaType = MediaType.parse("application/vnd.android.package-archive");
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("APC_UPLOAD_PROGRESS", String.valueOf(key))
.addFormDataPart("upfile", file.getName(), RequestBody.create(mediaType, file))
.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.addHeader("User-Agent", USER_AGENT)
.addHeader("Connection", "keep-alive")
.build();
Response response = mOkHttpClient.newCall(request).execute()
下载文件
后面根据返回信息生成的url就可以下载签名后的APK文件了。
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", USER_AGENT)
.addHeader("Accept-Encoding", "identity") // 服务器下发的的资源不要做gzip, 否则没有content-length,报ProtocolException
.build();
Response response = mOkHttpClient.newCall(request).execute()
gradle插件
关于gradle插件的编写已有很多不错的文章,大家可以参考下面罗列出来的。
配置参数类
外部配置参数
class ConfigExt {
String username
String password
String loginUrl
String baseSignUrl
String signType
String suffix
boolean debugged = false
boolean autoSign = true
boolean deleteSourceFile = true
}
嵌套配置参数,可以设置每个buildType是否签名
class VariantConfigExt {
boolean sign
}
创建SignPlugin
这里根据variant生成不同的签名task,通过设置签名tasksignXXX
和打包taskassembleXXX
不同的依赖关系以实现自动或手动签名。
class SignPlugin implements Plugin<Project> {
private Project mProject = null
@Override
void apply(Project project) {
this.mProject = project
GLog.i("apply")
project.extensions.create('remoteSign', ConfigExt)
ConfigExt mConfig = project.remoteSign
// 根据不同的buildType创建VariantConfigExt
if (project.android.hasProperty("buildTypes")) {
project.android.buildTypes.all { type ->
project.remoteSign.extensions.create("${type.name}", VariantConfigExt)
}
// release默认自动签名
mConfig.release.sign = true
}
if (project.android.hasProperty("applicationVariants")) {
project.android.applicationVariants.all { variant ->
Task assembleTask
DefaultTask signTask = createSignTask(variant)
GLog.debug = (mConfig != null && mConfig.debugged)
variant.outputs.each { output ->
assembleTask = output.assemble
GLog.d(" config: " + (mConfig == null ? "null" : mConfig.toString()))
GLog.d(" assembleTask: " + assembleTask.name)
signTask.assembleTaskName = assembleTask.name
signTask.dependsOn assembleTask
if (project.remoteSign != null && project.remoteSign.autoSign) {
def buildType = variant.buildType.name
boolean hasBuildTypePro = mConfig.hasProperty(buildType)
boolean sign = true
if (hasBuildTypePro) {
sign = mConfig.getProperty(buildType).properties.get("sign").asBoolean()
GLog.d(buildType + ": " + mConfig.getProperty(buildType).toString())
}
if (sign) {
assembleTask.doLast { tk ->
if (!tk.state.failure) {
signTask.execute()
}
}
}
}
}
}
}
}
private DefaultTask createSignTask(Object variant) {
String variantName = variant.name.capitalize()
GLog.i("create sign${variantName} task")
DefaultTask signTask = mProject.task("sign${variantName}", type: RemoteSignTask)
signTask.group = 'signature'
signTask.description = 'Upload apk to sign server'
return signTask
}
创建DoSignTask
如果在gradle中修改过输出的apk名称,在配置阶段(在SignPlugin的apply方法中)就查找文件名会是修改之前的名称,所以这里我们在task执行阶段再根据前面生成的assembleTask
名称查找对应生成的apk文件名,
class DoSignTask extends DefaultTask {
String assembleTaskName
...
...
private ApkInfo getApkInfo(Project project) {
glogd("getApkInfo:")
ApkInfo apkInfo = new ApkInfo()
project.android.applicationVariants.all { variant ->
variant.outputs.each { output ->
GLog.d(" assembleTask: ${output.assemble.name}")
if (output.assemble.name == assembleTaskName) {
File apkFile = output.outputFile
apkInfo.apkFile = apkFile
apkInfo.apkName = apkFile.name
apkInfo.parentDir = apkFile.parent
GLog.d(" output: ${apkFile.name}")
}
}
}
return apkInfo
}
...
...
}
发布插件到maven私服
在项目根目录下新建一个uploadToMaven.gradle文件,还在开发调试阶段可以发布SNAPSHOT版本 (理解Maven中的SNAPSHOT版本和正式版本)。
apply plugin: 'maven'
def mavenReleaseUrl = 'http://xx.xx.x.xx/nexus/content/repositories/gradle-plugin/'
def mavenSnapshotUrl = 'http://xx.xx.x.xx/nexus/content/repositories/android-snapshots/'
uploadArchives {
configuration = configurations.archives
repositories {
mavenDeployer {
repository(url: uri(mavenReleaseUrl)) {
authentication(userName: MAVEN_USERNAME, password: MAVEN_PASSWORD)
}
snapshotRepository(url: uri(mavenSnapshotUrl)) {
authentication(userName: MAVEN_USERNAME, password: MAVEN_PASSWORD)
}
}
}
}
在module的build.gradle文件中添加
group='com.ahgx.gradleplugin'
version='1.1.0'
//version='1.0.0-SNAPSHOT'
//apply from: '../uploadToLocalRepo.gradle'
apply from: '../uploadToMaven.gradle'
集成使用
在项目根目录下的build.gradle文件中添加
buildscript {
repositories {
maven {
url uri('http://xx.xx.x.xx/nexus/content/groups/public/')
}
}
dependencies {
classpath 'com.ahgx.gradleplugin:sign-plugin:1.1.0'
}
}
在mudule下的build.gradle文件中添加
apply plugin: 'com.ahgx.sign-plugin'
可配置参数有
remoteSign {
username 'xxxxx' //用户名,必填
password 'xxxxx' //密码,必填
loginUrl 'http://xx.xx.x.xx/mantis/login.php' //登录地址,必填
baseSignUrl 'http://xx.xx.x.xx/mantis' //地址,必填
signType 'app' //签名类型(app, apk, sys),必填
debugged false //调试模式,默认false
autoSign true //打包APK之后自动签名,默认true
suffix '-signed' //签名后文件命名后缀,默认签名服务器自动添加
deleteSourceFile true //是否删除原APK文件,默认true
release {
sign true //是否自动签名,默认为true
}
debug {
sign false //是否自动签名,默认为false
}
最后在AndroidStudio右侧的gradle面板上可以看到signature分组下的签名task,双击执行即可手动签名。
网友评论