eijenson Con

主に作業中にはまったことの作業ログを書いていきます。

Androidのスクリーンショットを10秒ごとに撮る

一瞬だけ出るPush通知をスクショにしたかったが、ずっと画面を見ておくのも大変なのでスクショを定期的にとるshellを作成した

#!/bin/bash
set -eux

# スクリーンキャプチャを撮る
script_dir=$(cd $(dirname ${BASH_SOURCE:-$0}); pwd)
cd ${script_dir}

while true; do
  adb shell screencap -p /sdcard/screen.png
  adb pull /sdcard/screen.png
  mv screen.png `date +%Y%m%d_%H-%M-%S`.png
  adb shell rm /sdcard/screen.png
  sleep 10
done

各処理の説明

ループ処理

while true; do
~~~
~~~
  sleep 10
done

trueにしてるので止めるまで無限ループしている

sleepで処理を10秒間隔で実行するようにしている

スクショを撮る

  adb shell screencap -p /sdcard/screen.png

画像は端末内に保存される

-p で端末内のどこに保存するか指定する

端末内の画像をPCに持ってくる

  adb pull /sdcard/screen.png

shellが置かれている場所にコピーされる

今の時間にリネームする

  mv screen.png `date +%Y%m%d_%H-%M-%S`.png

いつのスクショかわかりやすくするのとサイド実行したときに上書きされないようにするため

端末内のスクショを削除する

  adb shell rm /sdcard/screen.png

処理の後始末

実行結果

PCにAndroid端末をつなげた状態で実行する

./auto_screencap.sh
# 1分程待つ
# 別コンソールで開く
ls 
20200128_11-36-13.png 20200128_11-36-34.png
20200128_11-36-23.png auto_screencap.sh

止め忘れるとディスクを圧迫するので忘れないようにする

保存先の枚数が100枚を超えたら~とかで止めたほうが良いかもしれない

クライアント側の文字数制限を書ける時に悩むこと

Android/iOSのネイティブアプリやフロントエンドでテキストフィールドを設置する際、文字数にバリデーションをかける事がある。

その扱いについて少し悩んでいるので考えを列挙する

色んな所で言われていることをまとめてるだけ。

何に悩んでいるのか

・ネイティブ側でこのバリデーションをかける必要があるのか?

・テキストフィールドにその文字数以上入力できないようにするのか、送信時にチェックをしてエラーにするのがいいのか

ネイティブ側でこのバリデーションをかける必要があるのか?

この文字数制限は、無制限に送れるようにしてしまうとサーバやDBの負荷になるため書けることがほとんどだと思う。

そのため、必ずサーバ側で受け取った時にも文字数にバリデーションはかける。(クライアント側しかないならそれは不具合と言っていい)

そうなると、ネイティブ側はなにもしなくてもエラーハンドリングさえしていればいいことになる。

これに関しては、わざわざ通信しないとエラー結果が出ないことが気になるなら文字数チェックをかけたほうがいい。

ユーザの利便性を考えていくと、通信しなくてもエラーがわかるならわかったほうがいい。

文字数の数をころころ変えて仕様の調整をしたいようならサーバ側のみじゃないとだめだけど。

テキストフィールドにその文字数以上入力できないようにするのか、送信時にチェックをしてエラーにするのがいいのか

文字数制限をネイティブ側で掛ける場合、maxlength等でテキストフィールドにその文字数以上入力させない方法がある。

一方で、送信ボタン押した時にjavascriptやネイティブコード上で文字数をチェックして、超えるならエラーにするという方法がある。

これに関しては悩みが多くて、

maxlengthを使う方法は、ユーザがコピペでテキストを入力した場合、ペーストする文字列の数が多い場合勝手に切られちゃうため(100文字の制限がかかってるテキストフィールドに101文字をペーストすると最後の文字は切られて無くなるため)不具合というか、ユーザの意図に反する動きになる。

maxlengthをパスワードにかけた結果、意図したパスワードとは違うパスワードが設定されてトラブルになったケースをブログで見たことがあるのであまり使わないほうが良さそう。

送信ボタン押した時にチェックするのはわりと普通のやり方でいい。仕様としてそこの文字数制限が変わらないなら問題ない。

ちょっとエラーにするためのラベル欄の出し分けとかがめんどくさいくらい。

他には10/100的な感じで入力文字数/文字数制限を可視化して超えるなら赤くするのもあって、リッチでいい。

まとめとしては

・サーバに文字数制限はかならず書け、クライアントだけにしない

・クライアントでは利便性を良くするのを目的にする

になる

Firefoxのヘッドレスブラウザ+Seleniumでログイン後ページのスクリーンショットを撮る

目的

サービスの計測値を日時で確認必要があり、それを自動化したかった

APIが提供されていないので直接見るしかなく、

ログインが必要なためnokogiriによるスクレイピングでは難しかった

そのためSeleniumを使ってログインし、スクリーンショットを定期的に撮るようにした

コード

screen_shot.rb

    require 'selenium-webdriver'
    require 'time'

    url = "...ログインが必要なサービスのログインページ" #適宜変更
    options = Selenium::WebDriver::Firefox::Options.new
    options.add_argument('-headless')

    driver = Selenium::WebDriver.for :firefox, options: options
    driver.navigate.to url

    sleep 3 #seleniumはブラウザ表示が間に合わなくてエラーになるため3秒待つ
    email = driver.find_element(id: 'email')#DevToolでID入力欄を指定できるID/Classを探す
    email.send_keys "ID"#IDを入力
    password = driver.find_element(id: 'password')#DevToolでパスワードを指定できるID/Classを探す
    password.send_keys "パスワード"#パスワードを入力
    submit = driver.find_element(class: "sign-in")#DevToolでログインボタンを指定できるID/Classを探す
    submit.submit
    sleep 7 #遷移時の処理が遅い場合があるため待つ
    date = Time.now.strftime("%Y%m%d_%H%M%S")
    driver.save_screenshot('log_' + date + '.png')

    driver.quit

Gemfile

source 'https://rubygems.org'
gem 'selenium-webdriver'

コンソールコマンド

bundle install
bundle exec ruby screen_shot.rb

実行したディレクトリ内にlog_{現在時刻}.png というファイルが表示されます。 例えばGmailみたいに、「未ログイン時に遷移した時に、ログインページに飛び、ログイン後にもともと遷移したかったページが表示される」サービスの場合は表示したいページを直接指定すればそのスクリーンショットが取れる

shellでフォルダ内の全てのファイルに対してfor処理をする

目的

screenshot
├── スクリーンショット10月
│   └── 01.png
│   ├── 02.png
│   ├── 03.png
│   ├── 04.png
│   ├── 05.png
│   ├── 06.png
│   ├── 07.png
│   └── 08.png
└── スクリーンショット11月
    └── 01.png
    ├── 02.png
    ├── 03.png
    ├── 04.png
    ├── 05.png
    ├── 06.png
    ├── 07.png
    └── 08.png

こんな感じのディレクトリ構成になっていて、各フォルダ内の全ての画像をjpgに変えたかった

作成したshell

#!/bin/bash
#set -eux

while read -d $'\0' d; do
    dir=`basename "${d}"`
    echo dir="${dir}" 
    while read -d $'\0' file; do
        echo file="${file}" 
        convert "${file}" -quality 100 "${file}"
    done < <(find "$dir" -mindepth 1 -maxdepth 1 -print0)
done < <(find . -type d -mindepth 1 -maxdepth 1 -print0)

説明

while read -d $'\0' d; do
  #今いる場所のフォルダ分loopを回す
 # ${d}でフォルダのpathが取れる
done < <(find . -type d -mindepth 1 -maxdepth 1 -print0)

while read -d $'\0' d; do は正直良くわかっていないが、いろいろ試した結果ディレクトリ内のすべてのファイルをループする - Qiitaの記事で書かれているやり方が一番良く動いたので使っている

done < <(find . -type d -mindepth 1 -maxdepth 1 -print0)

find . で今いる場所を検索する

-type d でディレクトリのみに検索範囲を指定

-mindepth 1 -maxdepth 1 で今いるディレクトリのみを検索範囲にする(深く探さない)

-print0 で空白文字を含むディレクトリも検索できるようにする参考URL→findとxargsコマンドで-print0オプションを使う理由(改) - Qiita

中身のloop

 dir=`basename "${d}"`
    echo dir="${dir}" 
    while read -d $'\0' file; do
        echo file="${file}" 
        convert "${file}" -quality 100 "${file}"
    done < <(find "$dir" -mindepth 1 -maxdepth 1 -print0)

dir=`basename "${d}"` ディレクトリパスからディレクトリ名を取得している

done < <(find "$dir" -mindepth 1 -maxdepth 1 -print0) で-type d をなくしてloopを回しているため、ファイルも検索対象になっている

ファイルは画像のみなので、convert "${file}" -quality 100 "${file}"ImageMagickのconvertコマンドを叩いて変換ができる

Android+Jacocoでテストカバレッジを出力する(Android Studio3.2対応)

しばらく放置していたJacocoでのカバレッジ取得を更新しようとしたらハマった。

参考にしたサイト: http://phicdy.hatenablog.com/entry/jacoco-code-coverage

上の記事が一年前に書かれているが、Gradleの更新によって動かなくなったらしい

タスク自体は成功するが、出力結果が0件となっていた

    classDirectories = files(
                fileTree(
-                        dir: "${buildDir}/intermediates/classes/uiTest/debug",
+                        dir: "${buildDir}/intermediates/javac/uiTestdebug",
                        exclude: coverageExcludeFiles))
    }

jacocoのカバレッジ計測に使っている、classファイル群が生成されるフォルダを指定してあげたら動いた

RxJava2へ移行時のUnitTest

RxJava1 から2へ移行すると、TestSubscriberがうまく使えなくなった。

代わりにObservar.test()というメソッドが追加された。

RxJava1

public void test(){
//...

        TestSubscriber<UserResult> testSubscriber = TestSubscriber.create();
        repository.getUser().subscribe(testSubscriber);
        testSubscriber.awaitTerminalEvent();
        testSubscriber.assertCompleted();
        UserResult result = testSubscriber.getOnNextEvents().get(0);

        assertEquals(result.getName(), "田中");
        assertEquals(result.getAge(), 13);
//...
}

RxJava2

public void test(){
//...

        UserResult result = repository
                .getUser()
                .test()
                .assertComplete()
                .assertValueCount(1)
                .values().get(0);

        assertEquals(result.getName(), "田中");
        assertEquals(result.getAge(), 13);
//...
}

メソッドチェーンで記述できることで、型指定の煩わしさが少なくなる。

JobSchedulerで◯時間後からXX時間ごとに定期実行する方法

概要

Android Oreo対応の時に、定期実行処理をJobSchedulerに設定したら1回目が即時実行されて困ったので対応した。

コード

アプリ起動のActivity

class MainActivity : AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        // 略
        startJob()
    }

    override fun startJob() {
        val componentName = ComponentName(this, FirstRunJobService::class.java)

        val periodic = 15 * 60 * 1000L // 15分
        val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        val jobInfo = JobInfo.Builder(1, componentName)
                .setMinimumLatency(periodic) // サービス起動遅延時間を設定する
                .build()

        scheduler.schedule(jobInfo)
    }
}

Activityから起動するService

class FirstRunJobService : JobService() {
    override fun onStartJob(p0: JobParameters?): Boolean {
        startJob()
        return false
    }

    override fun onStopJob(p0: JobParameters?): Boolean {
        return false
    }

    private fun startJob() {
        val componentName = ComponentName(this, MyJobService::class.java)

        // デバッグ時は15分,リリース版は6時間ごと
        val periodic = if (BuildConfig.DEBUG) 15 * 60 * 1000L else 6 * 60 * 60 * 1000L
        val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        val jobInfo = JobInfo.Builder(2, componentName)
                .setPeriodic(periodic) // サービス実行間隔を設定する
                .build()

        scheduler.schedule(jobInfo)
    }
}

定期実行するService

class MyJobService : JobService() {

    override fun onStartJob(p0: JobParameters?): Boolean {
        // 定期実行したい処理
        return true
    }

    override fun onStopJob(p0: JobParameters?): Boolean {
        return true
    }
}

説明

ActivityでFirstRunJobServiceを15分後に起動するように処理を書く。 setMinimumLatencyメソッドで遅延時間を設定する(コードでは15分後に実行する)

FirstRunJobServiceでは、MyJobServiceを起動するための設定だけを行なっている setPeriodicメソッドで実行間隔を設定する(コードでは15分か6時間後に実行する)

setMinimumLatencyとsetPeriodicは同時には設定できない(例外が吐かれる)

参考URL

公式のリファレンス

https://developer.android.com/reference/android/app/job/JobInfo.Builder