AWS Device farm を使って、Google Playストアでの見え方を確認してみようかともがいた話。


私達の会社では、Playストア掲載用の画像はデザインの担当に作ってもらっています。
で、「特定の端末でだけ、見にくいよ〜」などのフィードバックが必要であればしたいところです。
ただ、そのためだけに確認用端末を買うわけにはいきませんね。

ということで、最近でたAWS Device farmを使って、撮ってみましょう、と思い立ったのです。

個人的に、へんなDSL使うのは好きではないのと、
そもそもテスト対象はアプリではないので、
uiautomatorを使います。

## ざっくりいうと

* Google Playで、スクリーンショットを撮りたいターゲットのページにはちゃんと遷移できているっぽい
* スクリーンショットを撮って、保存すること自体はうまくいっているっぽい
* AWS Device Farmのドキュメントに書いてあるパスにスクリーンショットを配置してみたが、AWS Device Farmのシステムがそれを拾ってくれない。
* ドキュメントに書いてあるやり方でダメなら、自前でDropboxにあげてやろうじゃないの！　としたが、 HTTP POSTのさいに勝手にコネクションリセットされてしまい、アップロードできなかった
* **Keep-Aliveをやめたら、アップロードできるようになった！**
* Google アカウントが設定されていない！( ﾟдﾟ)　←イマココ


## テストプロジェクトを作成
Android Studioで適当な新規プロジェクトを作ります。

本体（はろーわーるど）は、特に用事はないので、何もいじりません。

Dropboxにファイルはアップロードしようかなと思い、INTERNETパーミッションをつけるところだけはいじりました。

```xml:app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.co.crowdworks.awssample"
    android:sharedUserId="jp.co.crowdworks.awssample.uid">

    <uses-permission android:name="android.permission.INTERNET"/>

	<application
```

uiautomator 2.0を使います。
あと、DropboxにあげるにはHTTPクライアントとして、今回はOkHttp使いました。

```groovy:app/build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'

    // Set this dependency to use JUnit 4 rules
    androidTestCompile 'com.android.support.test:rules:0.3'
    // Set this dependency to build and run Espresso tests
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
    // Set this dependency to build and run UI Automator tests
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'

    // for uploading screenshots into Dropbox.
    androidTestCompile 'com.squareup.okhttp:okhttp:2.4.0'

}

```

テストを書きます。
注意点としては、uiautomator 2.0は InstrumentationTestだということです。

```xml:app/src/androidTest/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.co.crowdworks.awssample.test"
    android:sharedUserId="jp.co.crowdworks.awssample.uid">

    <uses-permission android:name="android.permission.INTERNET"/>

</manifest>
```

```java:ApplicationTest.java
public class ApplicationTest extends InstrumentationTestCase {
    private UiDevice mDevice;
    private Context mContext;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        // Initialize UiDevice instance
        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        mContext = getInstrumentation().getTargetContext();
    }

    private File ensureDir(File d){
        if(!d.exists()){
            d.mkdir();
        }
        return d;
    }

    public void test1PlayStoreScreenshot() {
        // Start from the home screen
        mDevice.pressHome();

        Intent intent = new Intent(Intent.ACTION_VIEW,
        	Uri.parse("market://details?id=jp.co.crowdworks.androidapp"));
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);

        try{Thread.sleep(10*1000);}catch(Exception e){}


        File screenshotDir = ensureDir(new File(mContext.getFilesDir().getParentFile(),"app_spoon-screenshots"));
        File screenshotDirCls = ensureDir(new File(screenshotDir,ApplicationTest.class.getName()));
        File screenshotDirMethod = ensureDir(new File(screenshotDirCls,"test1PlayStoreScreenshot"));

        File fileA = new File(screenshotDirMethod,"/a.png");
        File fileB = new File(screenshotDirMethod,"/b.png");

        mDevice.takeScreenshot(fileA);

        try{Thread.sleep(5*1000);}catch(Exception e){}
        
        try {
            mDevice.setOrientationLeft();
        } catch(Exception e){}

        try{Thread.sleep(5*1000);}catch(Exception e){}

        mDevice.takeScreenshot(fileB);

        try{Thread.sleep(5*1000);}catch(Exception e){}

        assertTrue(fileA.exists());
        assertTrue(fileB.exists());
    }

    public void test2UploadScreenshotFile() {
        File screenshotDir = ensureDir(new File(mContext.getFilesDir().getParentFile(),"app_spoon-screenshots"));
        File screenshotDirCls = ensureDir(new File(screenshotDir,ApplicationTest.class.getName()));
        File screenshotDirMethod = ensureDir(new File(screenshotDirCls,"test1PlayStoreScreenshot"));

        File fileA = new File(screenshotDirMethod,"/a.png");
        File fileB = new File(screenshotDirMethod,"/b.png");

        assertTrue(fileA.exists());
        assertTrue(fileB.exists());

        upload(fileA);
        upload(fileB);
    }

    private void upload(File file){
        final OkHttpClient client = new OkHttpClient();
        Request req = new Request.Builder()
                .url("https://api-content.dropbox.com/1/files_put/auto/"+ Build.MODEL+"_"+file.getName())
                .addHeader("Authorization", "Bearer XXXXXXXXXXXXXX(DropboxのAPIトークン)XXXXXXXXXXXXXX")
                .post(RequestBody.create(MediaType.parse("image/png"),file))
                .build();

        try {
            Response response = client.newCall(req).execute();
            assertTrue(response.isSuccessful());
        }
        catch(IOException e){
            Log.e("hoge","error",e);
            assertTrue(false);
        }
    }

    @Override
    protected void tearDown() throws Exception {
        mDevice.unfreezeRotation();
        super.tearDown();
    }
}
```

test1PlayStoreScreenshot()のなかで、やたらと深い階層にスクリーンショットを保存していますが、なぜこんなところに置いているかというと、
![スクリーンショット_2015-08-06_16_27_20.png](https://qiita-image-store.s3.amazonaws.com/0/74571/62daedfc-c44c-22d6-dcdc-f7d19ef8dc48.png "スクリーンショット_2015-08-06_16_27_20.png")
こんなかんじでAWSのマニュアルに書いてあるからです。

AWSとしては、Spoonというライブラリで保存することを期待しているんだと思います。

```java:Spoon
    public static File screenshot(Activity activity, String tag) {
        StackTraceElement testClass = findTestClassTraceElement(Thread.currentThread().getStackTrace());
        String className = testClass.getClassName().replaceAll("[^A-Za-z0-9._-]", "_");
        String methodName = testClass.getMethodName();
```
Spoon-Clientをみてみると、↑のような実装になっているので、
`/data/data/アプリのパッケージ名/app_spoon-screenshots/テストパッケージ名.テストクラス名/テストメソッド名/` 配下にスクリーンショットを保存すれば良い、というわけです。


## テストプロジェクトをビルド
```bash:ビルド
./gradlew clean assembleDebug assembleDebugAndroidTest
```

すると、成果物が、

* app/build/outputs/apk/app-debug.apk
* app/build/outputs/apk/app-debug-androidTest-unaligned.apk

みたいなかんじでできるかと思います。こいつをAWS Device Farmにアップロードして、テストを走らせます。

## ローカルで試験が通ることを確認する
実機をUSB接続し、

```bash:ローカルで動作確認
adb install -r app/build/outputs/apk/app-debug.apk
adb install -r app/build/outputs/apk/app-debug-androidTest-unaligned.apk
 
adb shell am instrument -w jp.co.crowdworks.awssample.test/android.support.test.runner.AndroidJUnitRunner
```

を実行すると、スクリーンショットが縦・横あわせてDropboxにアップロードされることを確認。


## AWS Device FarmにAPKを上げて実行する

ローカル確認がうまくいったところで、AWSに上げます。

![スクリーンショット 2015-08-06 12.59.17.png](https://qiita-image-store.s3.amazonaws.com/0/74571/2ee156e4-0290-731d-9994-5f1854fba8e4.png "スクリーンショット 2015-08-06 12.59.17.png")
app-debug.apkをアップします。
　　　↓
![スクリーンショット 2015-08-06 12.59.52.png](https://qiita-image-store.s3.amazonaws.com/0/74571/98a0bd41-0845-2d01-1198-1ffc723ac886.png "スクリーンショット 2015-08-06 12.59.52.png")
uiautomator 2.0はInstrumentationTestなので、
Instrumentationを選択してapp-debug-androidTest-unaligned.apkをアップします。
　　　↓
![スクリーンショット 2015-08-06 12.59.58.png](https://qiita-image-store.s3.amazonaws.com/0/74571/c37928a1-8668-3335-ba0c-a43990370416.png "スクリーンショット 2015-08-06 12.59.58.png")
デバイスを選択する画面ですが、面倒なのでデフォルトのままとします。
　　　↓
あとは、「Next step」を連打。
　
　
　
さて、テストが始まります。

![スクリーンショット 2015-08-06 13.01.19.png](https://qiita-image-store.s3.amazonaws.com/0/74571/77b50ae3-0085-960e-4921-52a10262bf73.png "スクリーンショット 2015-08-06 13.01.19.png")
　
　
→ test1PlayStoreScreenshot はテストが通る = スクリーンショットはちゃんととれている
→ test2UploadScreenshotFile はテストにFailする。logcatを見てみると、

```log:logcat
08-06 00:29:47.445 23838 23852 E hoge    : error
08-06 00:29:47.445 23838 23852 E hoge    : java.io.IOException: unexpected end of stream on Connection{api-content.dropbox.com:443, proxy=DIRECT@ hostAddress=184.72.246.165 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} (recycle count=0)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpConnection.readResponse(HttpConnection.java:211)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpTransport.readResponseHeaders(HttpTransport.java:80)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:917)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpEngine.access$300(HttpEngine.java:95)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpEngine$NetworkInterceptorChain.proceed(HttpEngine.java:902)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:760)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.Call.getResponse(Call.java:274)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.Call$ApplicationInterceptorChain.proceed(Call.java:230)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.Call.getResponseWithInterceptorChain(Call.java:201)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.Call.execute(Call.java:81)
08-06 00:29:47.445 23838 23852 E hoge    : 	at jp.co.crowdworks.flavorsample.ApplicationTest.upload(ApplicationTest.java:113)
08-06 00:29:47.445 23838 23852 E hoge    : 	at jp.co.crowdworks.flavorsample.ApplicationTest.test2UploadScreenshotFile(ApplicationTest.java:100)
08-06 00:29:47.445 23838 23852 E hoge    : 	at java.lang.reflect.Method.invokeNative(Native Method)
08-06 00:29:47.445 23838 23852 E hoge    : 	at java.lang.reflect.Method.invoke(Method.java:515)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestCase.runBare(TestCase.java:134)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestResult$1.protect(TestResult.java:115)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestResult.runProtected(TestResult.java:133)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.DelegatingTestResult.runProtected(DelegatingTestResult.java:90)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestResult.run(TestResult.java:118)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.AndroidTestResult.run(AndroidTestResult.java:49)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestCase.run(TestCase.java:124)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.NonLeakyTestSuite$NonLeakyTest.run(NonLeakyTestSuite.java:63)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestSuite.runTest(TestSuite.java:243)
08-06 00:29:47.445 23838 23852 E hoge    : 	at junit.framework.TestSuite.run(TestSuite.java:238)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.DelegatingTestSuite.run(DelegatingTestSuite.java:103)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.AndroidTestSuite.run(AndroidTestSuite.java:63)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.junit3.JUnit38ClassRunner.run(JUnit38ClassRunner.java:90)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.Suite.runChild(Suite.java:128)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.Suite.runChild(Suite.java:27)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
08-06 00:29:47.445 23838 23852 E hoge    : 	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:54)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:228)
08-06 00:29:47.445 23838 23852 E hoge    : 	at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1729)
08-06 00:29:47.445 23838 23852 E hoge    : Caused by: java.io.EOFException: \n not found: size=0 content=...
08-06 00:29:47.445 23838 23852 E hoge    : 	at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:200)
08-06 00:29:47.445 23838 23852 E hoge    : 	at com.squareup.okhttp.internal.http.HttpConnection.readResponse(HttpConnection.java:191)
08-06 00:29:47.445 23838 23852 E hoge    : 	... 41 more
```

UnexpectedなEOSと。

ぐぐってみると、こんな記事が引っかかりました。
http://d.hatena.ne.jp/knight_9999/20141119/1416365367

```
connection: close
```

ヘッダを付けてやれば治る。（もしくは、http.keepAlive=falseにすれば確実に治る？）と。


## ということで

```Java:修正
    private void upload(File file){
        final OkHttpClient client = new OkHttpClient();
        Request req = new Request.Builder()
                .url("https://api-content.dropbox.com/1/files_put/auto/"+ Build.MODEL+"_"+file.getName())
                .addHeader("Authorization", "Bearer XXXXXXXXXXXXXX(DropboxのAPIトークン)XXXXXXXXXXXXXX"))
                .addHeader("connection", "close") //★追加
                .post(RequestBody.create(MediaType.parse("image/png"), file))
                .build();
```

とりあえずconnection:closeを追加してみました。
すると・・・

![スクリーンショット 2015-08-26 14.16.15.png](https://qiita-image-store.s3.amazonaws.com/0/74571/321e0229-721c-ca0f-ebec-8606723f9856.png)

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!

となりました！

ただ・・・
![SM-G900T_SM-G900T_a.png](https://qiita-image-store.s3.amazonaws.com/0/74571/3bff1cab-72de-16ba-f5da-71b1fcfa9c0b.png)

∑(ﾟДﾟ)ｶﾞｰﾝ

これはいろいろと大変そうだ。

## P.S.
いちおう、AWSのフォーラムにも類似のissue報告はあげてみましたが、8/26現在、報告はないですね・・・。
https://forums.aws.amazon.com/thread.jspa?messageID=667834&#667834