Android Publisherによるストア管理の自動化

技術部開発基盤グループの id:gfx と申します。

Google I/O 2014で発表されたGoogle Play Developer API (Android Publisher)を調べてみました。

Android PublisherはPlayのアプリケーション情報やAPKバイナリの操作を行うAPIで、これがあればリリースエンジニアリングの自動化(Release Engineering as Code)や、アプリの説明文や更新情報のgit管理などができるようになります。リリースエンジニアリングは属人化しがちなので、その作業をコードに落としこむという点で大きな意味を持つAPIだと言えるでしょう。AndroidアプリケーションにおけるRelease Engineering as Codeの実現に一歩近づくことができます。

本エントリでは、Android Publisher API(以下、単に「API」といいます)について簡単に紹介します。

APIでできること

  • APKのアップロードやアプリの説明文、更新情報などを取得・更新ができる
  • IABの設定を行える(このエントリでは未検証)
  • APIリクエストの限界は 200,000 queries per day

サンプルコードについて

詳細はこのエントリでは省きますが、このエントリのコードはGradle+Groovyプロジェクトで試しました。依存ライブラリは以下のとおりです。

dependencies {
    compile 'com.google.apis:google-api-services-androidpublisher:v2-rev2-1.19.0'
    compile 'com.google.api-client:google-api-client:1.19.0'
    compile 'com.google.api-client:google-api-client-gson:1.19.0'
}

APIを使う準備

AndroidPublisher service clientの生成

認証方法は「OAuth2」と「service account」の二種類あります。今回はservice accountで認証しましょう。以下の「Getting Started」に従って、Developer Consoleでservice accountを作ってAPIの設定をしてください。

認証の流れとしては、認証情報を設定したGoogleCredentialを生成し、それをもとにAndroidPublisherを生成します。

認証情報としては、Google API Developer Consoleで生成した Google Play Android Developer-xxxx.json のなかの client_email と、 Google Play Android Developer-xxxx.p12 ファイルが必要です。

// Google Play Android Developer-xxx.json
public static class Account {

    public String private_key_id

    public String private_key

    public String client_email

    public String client_id

    public String type
}

// ...

// create AndroidPublisher instance
def account = new Gson().fromJson(credentialFile(jsonName).text, Account);
assert account.client_id;

def httpTransport = GoogleNetHttpTransport.newTrustedTransport()
def jsonFactory = JacksonFactory.getDefaultInstance()

def credential = new GoogleCredential.Builder()
        .setTransport(httpTransport)
        .setJsonFactory(jsonFactory)
        .setServiceAccountId(account.client_email)
        .setServiceAccountScopes(AndroidPublisherScopes.all())
        .setServiceAccountPrivateKeyFromP12File(credentialFile(p12Name))
        .build()

def publisher = new AndroidPublisher.Builder(httpTransport, jsonFactory,
        credential)
        .build()

このpublisherオブジェクトに対してリクエストを生成することで、Playのアプリケーション情報を操作します。この部分はメソッドにして分離しておくといいですね。

なお、認証情報などのクライアント生成のための情報が足りないとIllegalArgumentExceptionをただ投げるだけなので、これに悩まされたら思い切ってソースを読みましょう。

編集の準備を行う

AndroidPublisherを生成したら、APIを呼ぶために、リクエストのための準備をします。これはどのリクエストでも共通です。

// 操作は特定のpackage名(application ID)に対して行う
def packageName = "com.example.android.app"


// 編集操作の集合であるeditsを作る
// 最終的に `edits.commit()` することで
// editsにリクエストされた操作がコミットされる
def edits = publisher.edits()
def editRequest = edits.insert(packageName, null)
def appEdit = editRequest.execute()

// `appEdit.getId()` を使ってeditsに対して操作する
// ...

これでようやくAPIを呼ぶ準備が整いました。次に、実際にAPIを見てみましょう。

Android Publisher API

Edits.apks

APKを操作します。list()とupload()メソッドを持っています。

まずlist()で情報を取得してみます。取得できるのはversionCodeとapk binaryのSHA1値です:

// list
Apk apk = edits.apks()
        .list(packageName, appEdit.getId())
        .execute()
        .getApks()
        .first()
println("versionCode: ${apk.getVersionCode()}, SHA1: ${apk.getBinary().getSha1()}")

次にupload()ですが、これはpackage名とapkファイルがあればできます。最後にcommitするまで実際の更新は適用されません。なお、不正なファイルをアップロードするとエラーになります:

def mimeType = "application/vnd.android.package-archive"
def apkFile = new FileContent(mimeType,
        new File('./app/build/outputs/apk/app-release.apk'))

Apk apk = edits.apks(
        .upload(packageName, appEdit.getId(), apkFile)
        .execute()

println("uploaded: versionCode: ${apk.getVersionCode()}, sha1: ${apk.getBinary().getSha1()}")

edits.commit(packageName, appEdit.getId())
    .execute()

Edits.apklistings

その他のAPIについても同様に操作できます。たとえば、Edits.apklistingsは言語(ロケール)ごとの更新情報を取得・更新できます。操作にはバージョンコードが必要なので、必要に応じて Edits.apks から入手するとよいでしょう。

edits.apklistings()
        .list(packageName, appEdit.getId(), apk.getVersionCode())
        .execute()
        .getListings()
        .each { ApkListing listing ->
    println("recent changes: ${listing.getRecentChanges()} (${listing.getLanguage()})")
}

同様に、 管理者の連絡先用の Edits.details 、公開状態を操作する Edits.tracks 、タイトルや説明文を操作する Edits.listings などがあります。

さてここまでの前提知識があればオフィシャルのREST APIリファレンスとAndroidPublisherのjavadocを元にAPIを使えるようになると思います。

おまけ:API使用の注意点

API Usage Instructions に書いてあることのまとめです。

  • productionはもちろんのこと、alpha/betaリリースであっても、必要のない限り1日1度以上リリースすべきではない。頻繁なリリースはユーザーに負担をかける。
  • 外注先などの協力会社にアプリの作成や公開・配付を行わせてはいけない。それはGoogle Play Developer API Terms of Serviceに違反している
  • 協力会社にアプリの開発を任せる場合は、開発用アカウントを共有してはいけない。パーミッションを制限したアカウントを作ってそれを使わせること
  • 協力会社には作成したサービスアカウントを与えないことを奨励する
/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/