AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

ErgoDox EZを買いました

最近流行ってることもあり買いました。 https://www.indiegogo.com/projects/ergodox-ez-an-incredible-mechanical-keyboard-computers-health#/

f:id:phicdy:20170625110121j:plain

以前使っていたキーボードはテンキー付きで横に広いものでした。そのためキーボードの横にあるマウスを使うたびに肩と腕に負担がかかっていました。通常のキーボードは中央にキーが集まっているため、肩が内側に寄り身体への負担が大きいです。ErgoDoxはセパレートキーボードで、肩を開いて打てるというのが魅力でした。 ErgoDoxはキーボード配列を自由に変更できるのも大きな魅力で、色んな人が自分の考えた最強のキーマップを公開していて面白そうだなと思ったのもあります。

キーマップ

現在のキーマップです。

f:id:phicdy:20170625110204p:plain

方針としては、まずは通常のキー配列にして作業をしました。その中で自分が押す頻度の高いキーを親指に集め、よく使う記号を押しやすい位置に変更していきました。

親指周辺

私は普段MacAndroid StudioにIdea Vimを入れて作業しています。そのためVimでよく使うEscキーを押しやすい親指の位置に持ってきました。 親指シフトというのは色んなところでいいと見かけていたので、左手親指に設定しました。確かにかなり楽に大文字が打てます。

英数⇔かな変換

英数⇔かな変換はKarabinerを使い、左右commandキーで行うようにしました。最初はGoogle日本語入力のショートカットで行っていましたが、Vim操作時に余計なキーが押されてうまく変換が行えないということがありました。

括弧

コードを書くときにでよく使う括弧が押しやすい位置にあったらいいなと思い、左キーボードに左括弧、右キーボードに右括弧を置く配置にしました。Shiftを押しながらここだっけな?と押して逆向きの括弧が入力されることがなくなってかなり快適です。

ショートカットキー

どの記事か忘れましたが、コピー&ペーストをキーに割り当てている人がいて便利そうだったので真似しました。AlfredユーザなのでAlfredをすぐ呼べるようにAlt+Spaceをキーに割り当てました

レイヤー

ErgoDoxはレイヤー機能があり、キーを押すだけで切り替えができます。私は基本1レイヤーで済まそうとしていますが、他に2レイヤー追加しています。

L1: 足りなかったキー用レイヤー

f:id:phicdy:20170625110302p:plain

普段使いで足りなかったキーだけ配置してます。ファンクションキーとハット(^)だけです。もっとうまく使えるような気もしてます。

L2: Windows, Ubuntu

f:id:phicdy:20170625110407p:plain

リモートデスクトップWindows, Ubuntuに繋ぐとき用です。キーの割り当て対応とCmdをCtrlに置き換えています。なぜか_と|が割り当てできず困っています…ひとまずは辞書に「あ」の変換で登録しておきました。もしくはどこかからかコピーしてきます。基本こっちではコードを書かないようにしています。

おわりに

使い始めて2ヶ月程度ですが、明らかに腕や肩が楽になりました。よりよいタイピングができるようにキーマップを改善していきたいと思います。 私のキーマップはこちらです。

【Android】UI Automator 2.0でUIテストをする

UI AutomatorGoogleが開発しているAndroid向け自動UIテストフレームワークです。 2015年3月にバージョンが2.0になりGradleに対応しました。 有名なテストフレームワークのAppiumも中ではUI Automatorを実行しています。

Espressoとの違い

GoogleはテストフレームワークとしてEspressoも開発しています。EspressoとUI Automatorの違いとして最も大きいのは、UI Automatorは複数のアプリをテストできるということです。Espressoはアプリのソースコードに紐付いており、アプリのプロジェクトの中に入れる必要があります。それに対してUI Automatorは自分のアプリはもちろんのこと、設定アプリや自分で作ったツールアプリや他社のアプリ、通知バーなど自由に操作することができます。ソースコードと紐付いていないので、別プロジェクトで管理することもできますし、アプリと同じプロジェクトに含めることもできます。またUI Automatorは2.0からEspressoと併用できるようになったため、自分のアプリはEspresso、他のアプリを操作するときはUI Automatorといった書き方が可能です。

UI AutomatorAndroid 4.3以上のみをサポートしています。まだまだAndroidでは最新のOSのみを対象とはできない状況なので、通常はAndroid 4.3以上のみを対象とていないことがほとんどかと思います。ここはProductFlavorを分けるかプロジェクトごと分けることで対応します。なおEspressoはAndroid 2.2からサポートしており、設定アプリなどを使わないテストのみであれば、Espressoのほうが幅広くテストを行えます。

設定

UI Automatorは2.0からAndroid Support Libraryの一部となり、build.gradleで簡単に使うことができるようになりました。 まずAndroid SDK ManagerからAndroid Support Repositoryをインストールします。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "26.0.0"

    defaultConfig {
        applicationId "com.phicdy.uiautomator2sample"
        minSdkVersion 14
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
        production {
            minSdkVersion 14
        }
        uiTest {
            minSdkVersion 18
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:design:25.3.1'
    uiTestCompile 'com.android.support.test:runner:0.5'
    uiTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}

testInstrumentationRunnerをandroid.support.test.runner.AndroidJUnitRunnerに指定します。これでJUnit4形式でテストが書けるようになります。productFlavorをアプリ用とUI Automator用で分け、UI Automator用のminSdkVersionを18(Android 4.3)に設定します。今回はuiTestとしました。

最後に実行に必要なライブラリであるcom.android.support.test:runner:0.5とcom.android.support.test.uiautomator:uiautomator-v18:2.1.2をuiTestCompileで読み込みます。

テストを書く時や実行時は、Android Studio上のBuild VariantsをuiTestDebugに変更します。

f:id:phicdy:20170625105424p:plain

これでUI Automatorの設定は終わりです。

テストを書く

テストはsrc/androidTest以下にJUnit4の書き方で追加していきます。 デフォルトでApplicationTestが入っていますが必要ないので消し、新たにテストを追加します。

package com.phicdy.uiautomator2sample;

import android.content.Context;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;

import org.junit.Test;
import org.junit.runner.RunWith;

import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.fail;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class MainUiTest {

    @Test
    public void floatingButtonTest() {
        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Launch MainActivity
        Context context = InstrumentationRegistry.getContext();
        Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);

        // Click floating button
        UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000);
        if (btn == null) fail("Floating button was not found");
        btn.click();

        UiObject2 snakeBar = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000);
        assertNotNull(snakeBar);
        assertThat(snakeBar.getText(), is("Replace with your own action"));
    }
}

今回はプロジェクト作成するときにTabbed Activityで作成したデフォルトのアプリのテストを作ります。 テストステップは以下の通りです。

  1. MainActiivtyを起動
  2. FloatingButtonを押す
  3. SnakeBarが出るので文言が"Replace with your own action"であることを確認

テストの初めにUiDeviceのインスタンスを取得します。 UiDeviceは端末の操作をしたり、UiObject2を取得したり、様々な場面で使います。 UiDeviceを管理するクラスを用意してもいいと思います。

UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

MainActivity起動部分です。UI Automator 2.0ではContextが使えるようになったのでstartActivity()で起動します。 MainActivityはアプリ起動時に起動されるActivityなので、context.getPackageManager().getLaunchIntentForPackage()で対象のパッケージ名を指定して起動しています。

// Launch MainActivity
Context context = InstrumentationRegistry.getContext();
Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

FloatingButtonをクリックしてSnackBarのUiObject2を取得する部分です。

UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000);

UI Automator 2.0では各ViewをUiObject2として取得します。主に取得には、UiDevice#findObject()かUiDevice#wait()を使います。 UiDevice#wait()を使うとViewが描画されるまで待ってくれるのでテストの成功率が上がります。

UiDevice#wait()にはSearchConditionとtimeoutを指定します。 SearchConditionはどの条件で待つかを指定します。Untilという便利なクラスがあるので、これを使ってSearchConditionを作ります。今回は特定のUiObject2が出るまで待つので、Unitl.findObject()を使います。Unitl.findObject()にはBySelectorを指定します。BySelectorはByクラスから生成してUiObject2を特定する条件を指定します。条件はresoruse IDやテキスト、クラス名などで指定します。resource IDが確実なので、アプリ側できるだけresource IDを設定します。Android SDKのtoolsにuiautomatorviewerというツールがあるのでこれで簡単にIDやViewの階層構造を確認できます。Android Studio 2.2からはこういった機能がAndroid Studio自体に追加されるようです。

f:id:phicdy:20170625105113p:plain

UiObject2を取得したらnullチェックをしてエラーハンドリングをした後、クリックします。

if (btn == null) fail("Floating button was not found");
btn.click();

クリックするとSnackBarが出るので、テキスト部分を同様に取得します。 最後にテキスト部分のnullチェックと文言を確認します。

UiObject2 snakeBarText = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000);
assertNotNull(snakeBarText);
assertThat(snakeBarText.getText(), is("Replace with your own action"));

最後に

UI Automator 2.0を使うことで、UIテストが書けました。 今回は1つのアプリだけを対象としたテストでしたが、例えば設定アプリを起動して機内モードをオンにし、エラーダイアログが出るか確認するといったテストもUI Autoamtor 2.0では可能です。 UIテストを書くことでCIでのリグレッションテストや手動テストの削減ができるので、少しずつ書いていきたいです。

今回のサンプルプロジェクトはこちらです。 以上です。

【Android】MaterialShowcaseViewライブラリでチュートリアルを実装する

MyCurationの1.2.1でチュートリアルを追加しました。というのもインストールされてもすぐアンインストールされているようで、説明が足りなかったかな・・・と思い始めたためです。 実装を自分でやってもよかったのですが、デザイン的なセンスがないのと、ある程度テンプレート化されたものがあるのではということでライブラリを探しました。 今回はMaterialShowcaseViewというライブラリを使ってチュートリアルを実装してみました。

f:id:phicdy:20170625104753g:plain

設定

GitHubのページ通りにbuild.gradleを編集

プロジェクトのbuild.gradle

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

アプリのbuild.gradle

dependencies {
  compile 'com.github.deano2390:MaterialShowcaseView:1.1.0@aar'
}

使い方

MaterialShowcaseViewでは特定のViewにハイライトを当ててチュートリアルを行います。 実装できるパターンとしては、(1)単純に追加ボタンなど1つだけを説明するパターンと、(2)ボタン1を押して次にボタン2を押して・・・といったように連続して説明をするパターンがあります。

MyCurationでは最初のRSS購読のチュートリアルを入れました。 RSS購読の流れとしては

  1. メイン画面からToolbarの「+」ボタンを押し、検索画面に行く(メイン画面)
  2. RSSを購読するサイト検索する(検索画面)
  3. サイトを開いたら追加ボタンを押す(検索画面)

です。

まずメイン画面で実装した単純に追加ボタンなど1つだけを説明するパターンです。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Menu settings...

    // Start tutorial at first time
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            View view = findViewById(R.id.add);
            new MaterialShowcaseView.Builder(TopActivity.this)
                    .setTarget(view)
                    .setContentText(R.string.tutorial_go_to_search_rss_description)
                    .setDismissText(R.string.tutorial_next)
                    .singleUse(SHOWCASE_ID)
                    .setListener(new IShowcaseListener() {
                        @Override
                        public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {

                        }

                        @Override
                        public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
                            goToFeedSearch();
                        }
                    })
                    .show();
        }
    });
    return true;
}

Builderパターンを使ってMaterialShowcaseView.Builderを作成していきます。今回はToolbar上の「+」ボタン(R.id.add)にハイライトを当てたかったのでonCreateOptionsMenu()の中でチュートリアルを作成します。 まずsetTarget()でハイライトするViewを指定します。次にチュートリアルの説明文と、チュートリアルを消す部分のテキストを設定します。singleUse()でIDを指定することで初回のみチュートリアルを表示し、同じIDのチュートリアルは今後表示しないように設定できます。今回はチュートリアルが閉じたときに次の画面(検索画面)に移りたかったので、setListener()でIShowcaseListenerをセットし、チュートリアルが閉じられたときに呼ばれるonShowcaseDismissed()で次の画面に飛んでいます。設定が終わったら最後にshow()でチュートリアルを表示します。

使ったメソッドのまとめです。

メソッド 説明
setTarget(View view) ハイライトするViewを指定
setContentText(int resource) 説明文を指定
setDismissText(int resource) タップして閉じる部分の文を指定
singleUse(String showCaseId) 1度だけの表示を管理するためのID。これを設定するとアンインストールしない限りは同じIDのチュートリアルは表示されない
setListener(IShowcaseListener listener) リスナの設定。表示時、消えた時のハンドリング
show() 表示する

次に検索画面で実装した連続して説明をするパターンです。

@Override
public boolean onCreateOptionsMenu(Menu menu) { 
    // Menu settings...

    // Start tutorial at first time
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            View view = findViewById(R.id.search_rss);
            ShowcaseConfig config = new ShowcaseConfig();
            config.setDelay(500); // half second between each showcase view

            MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(FeedSearchActivity.this, SHOWCASE_ID);
            sequence.setConfig(config);

            // Search tutorial
            sequence.addSequenceItem(
                    new MaterialShowcaseView.Builder(FeedSearchActivity.this)
                            .setTarget(view)
                            .setContentText(R.string.tutorial_search_rss_description)
                            .setDismissText(R.string.tutorial_next)
                            .build()
            );

            // Add button tutorial
            sequence.addSequenceItem(
                    new MaterialShowcaseView.Builder(FeedSearchActivity.this)
                            .setTarget(fab)
                            .setContentText(R.string.tutorial_add_rss_description)
                            .setDismissText(R.string.tutorial_close)
                            .setDismissOnTouch(true)
                            .build()
            );

            // Open software keyboard if tutorial already finished
            if (sequence.hasFired()) {
                searchView.setIconified(false);
            }

            sequence.start();
        }
    });
    return true;
}

今度はMaterialShowcaseSequenceというクラスのインスタンスを作り、MaterialShowcaseViewのインスタンスを追加していってチュートリアルの流れを作ります。

さきほどと同様にBuilderパターンでMaterialShowcaseView.Builderを作っていき、今回は最後にbuild()を呼ぶことでMaterialShowcaseViewを作り、MaterialShowcaseSequenceに追加します。これを繰り返すことでチュートリアルを表示→閉じる→チュートリアルを表示→閉じる→・・・と流れを作れます。作り終わったらstart()でチュートリアルを始めます。

注意した点

Toolbar上のViewがNullになる

onCreateOptionsMenu()の中でViewをfindViewById()で取るとNullが返ってきて落ちるという現象がありました。Handler#post()を使うことで遅延を発生させてこれを回避しました。

参考

How To Get Action View Of Menu Item?

チュートリアル中にキーボードが開かないようにする

検索画面では開いたときにすぐキーボードが開くようにsearchView.setIconified(false)を呼んでいたのですが、チュートリアル中は邪魔です。MaterialShowcaseSequence#hasFired()でチュートリアルが既に終わったかを判定できるので、チュートリアルでないときのみキーボードを開くようにしました。

// Open software keyboard if tutorial already finished
if (sequence.hasFired()) {
    searchView.setIconified(false);
}

終わりに

MaterialShowcaseViewを使うことでかなり簡単にチュートリアルを作成しました。さくっと作れるので、凝ったチュートリアルが必要でなければこれで十分かなと思います。以上です。

2週間毎日GitHubにコードを上げ続けて思ったこと

最近私にしては珍しく2週間毎日GitHubにコードを上げ続けています。 (2週間ごときで何を言ってるだと思う方もいらっしゃるとは思いますが・・・)

f:id:phicdy:20160424132021p:plain


なんとなくで始めたことですが、自分にとっては結構よかったです。

よかった点としては

  • 定期的なアウトプットが行える
  • 毎日何かしらのコードを上げる必要があるので、機能の粒度を下げることを考える
  • 機能の粒度を下げるにはどうすればいいかという設計を考えるようになる
  • 今日は何を作ろうかと考えるようになる
  • 毎日勉強が行える
  • 隙間時間を見つけてコードを書くようになる
  • 草が生えるのがなんか嬉しい

などです。

毎日コードを上げるといっても、毎日何十コミットも上げる訳ではなく、疲れた日や時間のない日は.gitignoreを更新するだけにしたり、テストを書いて終わりにしたりと、自分の生活には影響ない範囲でできているのかなと思います。


一方でアウトプットを増やすとインプットが減るという話もありますので、コードを書きつつインプットの時間も取れるようにしていければいいのかなと思います。 (もちろん自分の自由な時間を削ることなく・・・)

ksss9.hatenablog.com

【Android】App Standbyがわからない

App StatndbyはAndroid 6.0になって追加された省電力の機能である。

Doze と App Standby 用に最適化する | Android Developers

アプリが以下の場合でないとき、そのアプリは"Idle"状態になり、"Idle"状態が長時間続くとバックグラウンドでの通信を1日1回しか行うことができなくなる。

  • ユーザがアプリを明示的に起動する
  • アプリのプロセスがフォアグラウンドにある (アクティビティまたはサービスがフォアグラウンドにある、または他のアクティビティやフォアグラウンドサービスからアプリが使わている時)
  • アプリが生成した通知をユーザがロック画面または通知領域で見る

また、端末が充電中の時は"Idle"状態から解放される。

以上である。


全然わからない。


例えば以下のようなことである。


  • “Idle"状態にはどの程度の時間でなるのか
  • 常駐Serivceが動いている場合は"Idle"状態に移らないのか
  • “Idle"状態が何時間続けばバックグラウンド通信を1日1回しかできなくなるのか
  • 1日1回とは1リクエストのことなのか
  • 1日でバックグラウンド通信規制→解放→バックグラウンド通信規制となった場合、2回目の規制は1回目から通信が規制されるのか


この辺りがわからないせいでどうテストを行えばいいのかがさっぱりわからない。 特に気になるのはバックグラウンド通信の規制に入る方法である。 一応GoogleにはApp Standbyをシュミレートする方法が載っているが、肝心のバックグラウンド通信規制にする方法は載っていない。


$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <packageName> true


テスト終了後に戻す。


$ adb shell am set-inactive <packageName> false
$ adb shell am get-inactive <packageName>  ← falseならOK


アプリが"Idle"状態かどうかはPowerManager#isDeviceIdleMode()で取得できるので、実機にて1時間ごとにAlarmでこの状態をログに出力し、2~3時間放置して様子を見てみたが、常にfalseだった(充電できないし、AlarmがDozeモードで規制されないようにしないといけないのでクソ面倒)。

PowerManager | Android Developers


その他海外含め色々調査してみたが、上記のApp Standbyの条件とGoogleが出しているシュミレート方法を載せている記事がほとんどで、App Standbyの詳細がわかる記事は見つからなかった(もっと探せばあるのかもしれない)。


この辺りをはっきりさせるのなら自分で全部調査するしかないのかな・・・ ただそこまでやる必要は無い気はする。


皆様はどう対応しているんでしょう。。。

【Android】 リモートリポジトリに繋がらない環境でJUnitを実行する

ネットワーク環境に問題がないときは、Googleのページのやり方で設定すればJUnitの環境設定は簡単だけど、Proxyなどでmaven centralに繋がらない場合の対処法


jarファイルのダウンロード

リモートリポジトリに繋がらないので、jarファイルをlibsフォルダに置いてコンパイルする

<dependencies>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>1.3</version>
    </dependency>
</dependencies>
  • ダウンロードしたjarファイルをlibsフォルダにコピー


build.gradle

compile fileTree(dir: 'libs', include: '*.jar', excludes: ['junit-4.12.jar', 'hamcrest-core-1.3.jar'])
testCompile files('libs/junit-4.12.jar')
testCompile files('libs/hamcrest-core-1.3.jar')


テストを書く

  • src/test/javaフォルダ以下にJUnit4のテストを書いていく
  • 実行は実行したいクラス、フォルダ、メソッドの上で右クリック -> Runで行う

【Android】自分でキュレーションを作るフィードリーダー「MyCuration」を公開しました

f:id:phicdy:20151011164400p:plain

MyCuration -キュレーションを作るフィードリーダー - Google Play の Android アプリ

だらだらと自分用に作ってたアプリがある程度まとまったので公開しました。

Android4.0以上対応です。

背景

開発の背景としては以下のような背景がありました。

  • feedlyが起動時に毎回ネットワーク通信をして嫌だった
  • RSSリーダーだと読みたくない記事も入ってきて毎回既読にするのが面倒だった
  • キュレーションアプリは不必要な情報が多いし、必要な情報が削られる

要はfeedlyもキュレーションアプリも使いたくないけど、既存のでいいのがなかったから作ったって感じです。

機能

  • RSSリーダー機能(記事の取得、未読管理など)
  • キュレーション機能(特定の単語が入っている記事をまとめる)
  • フィルター機能(特定の単語またはURLが入った記事を既読にする)

キュレーションの条件とフィルターの条件は今後増やしていきたいところ。考えてるのは、時間とかはてなブックマーク数、つぶやき数などなど

使っているライブラリ

Android-PullToRefreshはもうメンテナンスしていなくて、デフォルトでも提供していたと思うので、時間ができたらそちらに乗り換えたい。

開発環境

テストはJUnit単体テストで実機テストしかやってないけどできればUIAutomatorを使った自動UIテストとかも徐々にやっていきたい。

レポジトリ

github.com

終わりに

とりあえずリリースできたので、機能追加を徐々にやっていこうと思う