Picture

Hi, I'm ogaclejapan.

Android Software Engineer at AbemaTV

Gradleを使ったWin/Mac対応のapkインストールツール

Androidアプリ開発de継続的インテグレーション
をプロジェクトに導入してみたら、社内無線LANに繋げるテスト端末よりも個人端末など直接繋げない端末のほうが多くて非常に困った(´・ω・`)

「AndroidSDKぐらい全員入れろよ!」…とは言えないので、
セットアップ不要のPC経由でapkを半自動でインストールするツールをGradleで作成してみた。
一応、開発者以外にもチームに関わる色々な人が導入できるよう最低限の機能と操作にしたつもり。

このツールで可能なことは以下の3つ

  1. showタスク

    特定のWebDAVディレクトリに存在するapk一覧を表示する
    →不要なファイルが見えても困るので、apkのみを表示するようにした

  2. downloadタスク

    特定のWebDAVディレクトリに存在するapkをダウンロードする
    →デフォルト(apkファイル指定なし)だと一番使うであろう開発最新版がダウンロードされるようにした

  3. installタスク

    特定のWebDAVディレクトリに存在するapkをPCに接続された端末にadb経由で直接インストールする
    →デフォルト(apkファイル指定なし)だと一番使うであろう開発最新版がインストールされるようにした
    →ローカルにapkが存在しない場合は自動でダウンロードされるようにした

記事上、WebDAVは以下の仕様であることを前提とする

  • apkが格納されたWebDAVのURLはhttp://192.168.0.1/apks/とする
  • apkのファイル形式はsample-(env)-(version).apkでUPされているものとする
  • プロジェクト環境はdebug,staging,releaseの3つあるものとする
  • 最新開発中のものはSNAPSHOTというversionを意味するものとする

最終的なツールの構成はこんな感じ↓

.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── tool
    ├── forMac
    │   └── adb
    └── forWindows
        ├── AdbWinApi.dll
        ├── AdbWinUsbApi.dll
        └── adb.exe

tool配下はMac/WinのAndroidSDKが入った端末からadbに必要なファイルを事前に抜き出したものをセットし、
GradleWrapperは以前書いた「
Gradle環境要らずのGradleチームビルド
」のやり方で生成する

最終的なbuild.gradleはこんな感じ↓

buildscript {
    repositories {
        mavenCentral()
        maven {
        	url 'http://sardine.googlecode.com/svn/maven/'
        }
    }
    dependencies {
        classpath 'com.googlecode.sardine:sardine:314'
    }
}

project.ext {
	baseUrl = 'http://192.168.0.1/apks/'
	sardine = SardineFactory.begin();
	defaultApk = 'sample-debug-SNAPSHOT.apk'
}

import com.googlecode.sardine.SardineFactory
import org.apache.tools.ant.taskdefs.condition.Os

task wrapper(type: Wrapper) {
  gradleVersion = '1.2'
}

//特定のWebDAVディレクトリに存在するapk一覧を表示する
task show(description: 'gradle[w] show') << {
	showApks()
}

//特定のWebDAVディレクトリに存在するapkをダウンロードする
task download(description: 'gradle[w] download -[Ptarget=(filename)]') << {
	def apk = project.defaultApk
	if (project.hasProperty('target')) {
		apk = target
	}
	downloadApk(apk)
}

//特定のWebDAVディレクトリに存在するapkをPCに接続された端末にadb経由で直接インストールする
task install(description: 'gradle[w] install [-Ptarget=(filename)] -Pforce=true') << {
	def apk = project.defaultApk
	if (project.hasProperty('target')) {
		apk = target
	}
	def isForce = false
	if (project.hasProperty('force')) {
		isForce = force
	}
	installApk(apk, isForce)
}

boolean isApk(mime) {
	mime.equals('application/vnd.android.package-archive')
}

void showApks() {
	def dav = project.sardine
	dav.list(project.baseUrl).collect {
		if (isApk(it.contentType)) {
			println "[${it.modified}] ${it.name}"
		}
	}
}

void downloadApk(name) {
	downloadApk(name, true)
}

void downloadApk(name, overwrite) {
	def out = new File(projectDir, name)
	if (out.exists()) {
		if (!overwrite) return
		out.delete()
	}

	def apk = "${project.baseUrl}${name}"
	def dav = project.sardine
	if (!dav.exists(apk)) {
		logger.error("file not found. ${apk}")
		throw new StopActionException()
	}

	logger.lifecycle("download.. ${apk}")

	out.withOutputStream { stream ->
		dav.get(apk).eachByte { b ->
        	stream.write(b as int)
    	}
	}
}

void installApk(name, isForce) {
	downloadApk(name, isForce)
	if (Os.isFamily(Os.FAMILY_WINDOWS)) {
		installApkForWindows(name, isForce)
		return
	}
	if (Os.isFamily(Os.FAMILY_MAC)) {
		installApkForMac(name, isForce)
		return
	}
	logger.error("not supported os. must be windows or mac")
	throw new StopActionException()
}

void installApkForWindows(name, isForce) {
	adbInstall("${projectDir}\\tool\\forWindows\\adb", name, isForce)
}

void installApkForMac(name, isForce) {
	adbInstall("${projectDir}/tool/forMac/adb", name, isForce)
}

void adbInstall(adb, apk, isForce) {
	logger.lifecycle("adb install ${apk}")

	def stdout = new StringBuffer()
	def stderr = new StringBuffer()
	def cmd = "${adb} install"
	if (isForce) {
		cmd = "${cmd} -r" //reinstall option
	}

	def proc = "${cmd} ${apk}".execute()
	proc.consumeProcessOutput(stdout, stderr)
	proc.waitForOrKill(1000 * 60) //wait for 1min

	if (stdout.length() > 0) {
		logger.lifecycle(stdout.toString())
	}
	if (stderr.length() > 0) {
		logger.error(stderr.toString())
	}
}

ちなみに今回はWebDAVクライアントにserdineを使ってみた。
Antタスクとしても提供しているので、簡潔で良い感じ。

Back to Top