Android通知のバージョンごとのUIの違い

自分用にメモ。以下のコードを実行したときのAndroid OSバージョンごとのUIを調べた。

Intent intent = new Intent(context, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(context)
    .setAutoCancel(true) // Delete notification when user taps
    .setContentTitle("Test")
    .setTicker("ticker") // Message when notification shows for ~4.4
    .setContentInfo("content info")
    .setContentText("content text")
    .setSmallIcon(R.mipmap.ic_launcher)
    .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
    .setContentIntent(pi)
    .build();
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(ID, notification);
  • Android 4.4までは通知が表示されるときにTickerの文字が表示される。5.0以上では設定しても無視される。
  • Android 5.0からはSmall IconがBig iconの右下に表示される。
  • Android 7.0からはContent Infoが表示されない。
Notification Ticker
4.1
4.2
4.3
4.4
5.0
5.1
6.0
7.0
LINEで送る
Pocket

CircleCIでAndroid SDKとGradleをキャッシュする

CircleCIで普通にビルドしていると毎回Android SDKとGradleのダウンロードが行われてビルド時間が長くなってくる。CircleCIにはデフォルトのキャッシュ以外にも自分でキャッシュの設定ができるので、Android SDKとGradleをキャッシュすることでビルドを短くできる。~/.gradleはデフォルトでキャッシュされるようになっているようなので設定は不要。

dependencies:
  override:
   ...
  cache_directories:
    - /usr/local/android-sdk-linux/tools
    - /usr/local/android-sdk-linux/platforms/android-25
    - /usr/local/android-sdk-linux/platforms/android-23
    - /usr/local/android-sdk-linux/platforms/android-16
    - /usr/local/android-sdk-linux/build-tools/25.0.2

キャッシュを使うかダウンロードするかの判定

tools, platforms/android-xxにはsource.propertiesというファイルがある。これにバージョンがPkg.Revision=25.2.5のように書かれているので、この値を見ることでキャッシュを使うかダウンロードするかを決める。build-toolsとGradleはフォルダがあるかどうかで判定する。

dependencies:
  override:
    - if ! $(grep -q "Pkg.Revision=25.2.5" $ANDROID_HOME/tools/source.properties); then echo y | android update sdk --no-ui --all --filter "tools"; fi
    - if [ ! -e $ANDROID_HOME/build-tools/25.0.2 ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"; fi
    - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-25/source.properties); then echo y | android update sdk --no-ui --all --filter "android-25"; fi
    - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-23/source.properties); then echo y | android update sdk --no-ui --all --filter "android-23"; fi
    - if ! $(grep -q "Pkg.Revision=5" $ANDROID_HOME/platforms/android-16/source.properties); then echo y | android update sdk --no-ui --all --filter "android-16"; fi
    - if [ ! -e ~/.gradle/wrapper/dists/gradle-3.5-all ]; then ./gradlew init; fi
  cache_directories:
    - /usr/local/android-sdk-linux/tools
    - /usr/local/android-sdk-linux/platforms/android-25
    - /usr/local/android-sdk-linux/platforms/android-23
    - /usr/local/android-sdk-linux/platforms/android-16
    - /usr/local/android-sdk-linux/build-tools/25.0.2

注意点として先にAndroid SDKのアップデートをしないとGradleの初期化中にAndroid SDKの使用許諾が取れていなくて失敗する。

circle.yml

machine:
  java:
    version: openjdk8
  environment:
    ANDROID_HOME: /usr/local/android-sdk-linux
dependencies:
  override:
    - if ! $(grep -q "Pkg.Revision=25.2.5" $ANDROID_HOME/tools/source.properties); then echo y | android update sdk --no-ui --all --filter "tools"; fi
    - if [ ! -e $ANDROID_HOME/build-tools/25.0.2 ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"; fi
    - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-25/source.properties); then echo y | android update sdk --no-ui --all --filter "android-25"; fi
    - if ! $(grep -q "Pkg.Revision=3" $ANDROID_HOME/platforms/android-23/source.properties); then echo y | android update sdk --no-ui --all --filter "android-23"; fi
    - if ! $(grep -q "Pkg.Revision=5" $ANDROID_HOME/platforms/android-16/source.properties); then echo y | android update sdk --no-ui --all --filter "android-16"; fi
    - if [ ! -e ~/.gradle/wrapper/dists/gradle-3.5-all ]; then ./gradlew init; fi
  cache_directories:
    - /usr/local/android-sdk-linux/tools
    - /usr/local/android-sdk-linux/platforms/android-25
    - /usr/local/android-sdk-linux/platforms/android-23
    - /usr/local/android-sdk-linux/platforms/android-16
    - /usr/local/android-sdk-linux/build-tools/25.0.2
test:
  override:
    - ./gradlew assembleDebug
    - cp -r ~/$CIRCLE_PROJECT_REPONAME/app/build/outputs/apk/* $CIRCLE_ARTIFACTS
      # unit test
    - ./gradlew testDebugUnitTest 
    - cp -r ~/$CIRCLE_PROJECT_REPONAME/app/build/test-results/testDebugUnitTest/* $CIRCLE_TEST_REPORTS
deployment:
  master:
    branch: master
    commands:
      - ./gradlew assembleRelease

参考

https://circleci.com/docs/1.0/how-cache-works/https://discuss.circleci.com/t/installing-android-build-tools-23-0-2/924/6

LINEで送る
Pocket

Python3でSlackのステータスを変える

最近Slackにステータスの機能が実装された。ミーティング中や帰宅済みなど名前の横を見ればステータスがわかるようになって非常に便利になった。
https://get.slack.help/hc/ja/articles/201864558-Slack-%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B
APIが公開されていてトークンがあればSlackの外から変更できる。
https://api.slack.com/methods/users.profile.set
トークンの取得はこちらから
https://api.slack.com/custom-integrations/legacy-tokens
以下はPython3でのコードになります。

環境

  • python 3.6.0
  • requests 2.13.0

コード

import requests
import json
import sys

arguments = sys.argv
text = arguments[1]
emoji = arguments[2]
url = "https://slack.com/api/users.profile.set"
params = {
    "token": "your_token",
    "profile": json.dumps(
        {
            "status_text": text,
            "status_emoji": emoji
        }
    )
}
headers = {"Content-Type": "application/json"}
r = requests.get(url, params=params, headers=headers)
print(r.url)
print(r.text)

実行

python3 change_slack_status.py "In meeting" ":spiral_calendar_pad:"
LINEで送る
Pocket

SwipeRefreshLayout内のListViewにEmptyViewを設定する時の注意点

SwipeRefreshLayout内のListViewにEmptyViewを設定しようとしたら詰まったのでメモ。

ListViewと同じ階層にEmptyView用のTextViewを置いていたのが最初のミス。SwipeRefreshLayoutは最初のViewしか認識してくれず、2つ目のViewは特にエラーも出ずに非表示になる。

<LinearLayout android:layout_height="match_parent" 
    android:layout_width="match_parent" 
    android:orientation="vertical" 
    xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:id="@+id/ll_xxx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <!-- 他のView -->
    </LinearLayout>
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/your_list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:drawSelectorOnTop="false"/>
        <TextView
            android:id="@+id/empty_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@string/no_data_message" />
    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

次にTextViewの階層をSwipeRefreshLayoutと同じにしたが、表示されなかった。これはLinearLayoutで縦にViewを並べているのが原因。SwipeRefreshLayoutを表示した段階でViewが全画面に描画されてしまっていた。Previewに表示されていない時点で気付こう・・・

<LinearLayout android:layout_height="match_parent" 
    android:layout_width="match_parent" 
    android:orientation="vertical" 
    xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:id="@+id/ll_xxx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <!-- 他のView -->
    </LinearLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/your_list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:drawSelectorOnTop="false"/>
    </android.support.v4.widget.SwipeRefreshLayout>
    <TextView
        android:id="@+id/empty_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/no_data_message" />
</LinearLayout>

最終的にSwipeRefreshLayoutとTextViewをRelativeLayoutに入れることで無事表示されるようになった。

<LinearLayout android:layout_height="match_parent" 
    android:layout_width="match_parent" 
    android:orientation="vertical" 
    xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:id="@+id/ll_xxx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <!-- 他のView -->
    </LinearLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/srl_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ListView
                android:id="@+id/your_list"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:drawSelectorOnTop="false"/>
        </android.support.v4.widget.SwipeRefreshLayout>
        <TextView
            android:id="@+id/empty_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@string/no_data_message" />
    </RelativeLayout>
</LinearLayout>
LINEで送る
Pocket

【Android】jacocoでコードカバレッジを取る

最近UTを書くようになってきたのでカバレッジを取ってみました。Gradleにjacocoのプラグインがあるのでそれを使います。

build.gradleはDroidkaigi2017のbuild.gradleを参考にしました。

apply 'jacoco'
apply plugin: 'com.android.application'

android {
    // Settings for Android...
}

jacoco {
    toolVersion = "0.7.7.201606060606"
}

// A list of directories which should be included in coverage report
def coverageSourceDirs = ['src/main/java']
// A list of files which should be excluded from coverage report since they are generated and/or framework code
def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*',
                            '**/BuildConfig.class', '**/*Activity*.class',
                            '**/*Fragment*.class', '**/*Receiver.class',
                            '**/*Manifest*.class', '**/*Application*.class']
task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
        xml.destination "${buildDir}/reports/jacoco/jacocoTestReport.xml"
        html.destination "${buildDir}/reports/jacoco/html"
        classDirectories = files(
                fileTree(
                        dir: "${buildDir}/intermediates/classes/uiTest/debug",
                        exclude: coverageExcludeFiles))
    }
    sourceDirectories = files(coverageSourceDirs)
    executionData = files "${buildDir}/jacoco/testUiTestDebugUnitTest.exec"

    doLast {
        println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/jacocoTestReport.xml"
        println "jacoco html report has been generated to file://${reports.html.destination}/index.html"
    }
}

dependencies {
    // For dependencies...
}

まずapply ‘jacoco’でプラグインを適用します。そしてjacocoプラグインのバージョンを指定します。

jacoco {
    toolVersion = "0.7.7.201606060606"
}

バージョンの一覧はここにあります。

次にコードカバレッジのレポートを作るタスクを作ります。
jacocoプラグインにはJacocoReportというレポートを作成するタスクがあります。jacocoの解析対象はバイトコードのため、UTのタスク(test(ProductFlavor)DebugUnitTest)と同時に実行させる必要があるので依存させます。私の場合はuiTestというProductFlavorでUTを実行しているので依存させるタスク名はtestUiTestDebugUnitTestになります。タスク名がわからなかったら./gradlew tasksで調べます。

task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) {
}

あとはJacocoReportのページを参考に設定します。私の場合はActivityやFragment等のクラスはカバレッジに含めないようにしました。先ほど書いたようにjacocoの解析対象はバイトコードのため、reports.classDirectoriesにはビルド後のパスを設定します。executionDataのパスは”${buildDir}/jacoco/test(ProductFlavor)DebugUnitTest.exec”となるようです。

// A list of directories which should be included in coverage report
def coverageSourceDirs = ['src/main/java']
// A list of files which should be excluded from coverage report since they are generated and/or framework code
def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*',
                            '**/BuildConfig.class', '**/*Activity*.class',
                            '**/*Fragment*.class', '**/*Receiver.class',
                            '**/*Manifest*.class', '**/*Application*.class']
task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
        xml.destination "${buildDir}/reports/jacoco/jacocoTestReport.xml"
        html.destination "${buildDir}/reports/jacoco/html"
        classDirectories = files(
                fileTree(
                        dir: "${buildDir}/intermediates/classes/uiTest/debug",
                        exclude: coverageExcludeFiles))
    }
    sourceDirectories = files(coverageSourceDirs)
    executionData = files "${buildDir}/jacoco/testUiTestDebugUnitTest.exec"
}

結果は↓のような感じ

これでjacocoによるコードカバレッジが取れるようになりました。
次はCircleCIとCodecovを連携させてコードカバレッジをGitHubのREADMEに表示する方法について書きたいと思います。

LINEで送る
Pocket

[UI Automator 2.0] UiObject2#longClick()が効かない問題の対策

UI Automator 2.0を使ってListViewのContextMenuのテストを書きたかったのですが、UiObject2#longClick()ではどうもまくいきませんでした。

動きを見る感じロングクリックの時間が短いですね・・・
調べるとUiDevice#swipe()でロングクリックしたい場所の座標をスワイプすることで解決できるようです。

参考:http://stackoverflow.com/questions/21432561/how-to-achieve-long-click-in-uiautomator

UiDevice#swipe()の説明を見ると

Performs a swipe from one coordinate to another using the number of steps to determine smoothness and speed. Each step execution is throttled to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete.

とあるので最後のstepの値でロングクリックの時間を制御できそうです。
以下テストコードです。

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

    // アプリ起動等
    // ...
    
    // ロングクリック
    UiObject2 filterList = device.wait(Until.findObject(
            By.clazz(ListView.class)), 5000);
    if (filterList == null) fail("Filter list was not found");
    List<UiObject2> filters = filterList.findObjects(
            By.clazz(LinearLayout.class).depth(2));
    if (filters == null) fail("Filter item was not found");
    assertThat(filters.size(), is(1));
    // 動かない
    // filters.get(0).longClick();
    Rect filterRect = filters.get(0).getVisibleBounds();
    device.swipe(filterRect.centerX(), filterRect.centerY(),
            filterRect.centerX(), filterRect.centerY(), 100);

    // 削除をクリック
    UiObject2 dialogContentList = device.wait(Until.findObject(
            By.res("android", "select_dialog_listview")), 5000);
    if (dialogContentList == null) fail("Dialog was not found");
    List<UiObject2> contents = dialogContentList.findObjects(
            By.clazz(RelativeLayout.class).depth(2));
    for (UiObject2 content : contents) {
        UiObject2 contentText = content.findObject(
                By.clazz(TextView.class));
        if (contentText != null && contentText.getText().equals("フィルター削除")) {
            content.click();
            break;
        }
    }

    // 結果確認
    UiObject2 emptyView = device.wait(Until.findObject(
            By.res(BuildConfig.APPLICATION_ID, "filter_emptyView")), 5000);
    assertNotNull(emptyView);
}

うまくいきました!

LINEで送る
Pocket

HTTPSに移行しました

XserverがLet’s Encryptに対応したということで今更対応しました。
やったことは以下になります。

  • XserverパネルからSSL設定->独自SSL設定の追加(適用まで30-60分かかる)
  • WordPressから設定->一般でWordPressアドレスとサイトアドレスをhttpsに変更
  • public_html/blog/.htaccessに以下を追加し、httpsにリダイレクト
RewriteEngine on
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://phicdy.com/blog/$1 [R=301,L]
  • サイドバーにある画像(リンクがhttpのもののみ)をメディアに追加し直し、リンクをhttpsに変更

簡単だ・・・!

LINEで送る
Pocket

【Android】SQLiteのテーブルからカラムを削除する

ALTER TABLE mytable DROP COLOMN mycolomnを実行すれば完了・・・と思いきや、SQLiteはDROP COLOMNをサポートしていないらしい。

http://www.sqlite.org/faq.html#q11

そのため、以下の手順でカラムを削除した

  1. テーブルのデータを全て取得
  2. DROP TABLEでテーブル削除
  3. カラムを消したSQL文を使ってCREATE TABLEでテーブル作成
  4. テーブルのデータを復元

下のコードはfilterテーブルからfeedIdカラムを削除したコードになる。

public class DatabaseHelper extends SQLiteOpenHelper{
  
    public static final String DATABASE_NAME = "rss_manage";
    private static final int DATABASE_VERSION = 3;
    private static final int DATABASE_VERSION_ADD_FILTER_FEED_REGISTRATION = 3;

    private String createFiltersTableSQL =
            "create table " + Filter.TABLE_NAME + "(" +
                    Filter.ID + " integer primary key autoincrement,"+
                    Filter.KEYWORD + " text,"+
                    Filter.URL + " text," +
                    Filter.TITLE + " text,"+
                    Filter.ENABLED + " integer)";

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    //onCreate() is called when database is created
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(createFiltersTableSQL);
    }
      
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (newVersion < oldVersion) return;
        if ((oldVersion < DATABASE_VERSION_ADD_FILTER_FEED_REGISTRATION)) {
            // Drop feed ID column in filter table, but Androd does not support drop column.
            // Copy and drop table and insert.
            ArrayList<Filter> filters = getAllFilters(db);
            String sql = "DROP TABLE " + Filter.TABLE_NAME;
            db.execSQL(sql);
            db.execSQL(createFiltersTableSQL);

            // Insert all of the filters
            insertFilters(db, filters);
        }
    }

    private void insertFilters(@NonNull SQLiteDatabase db, @NonNull ArrayList<Filter> filters) {
        try {
            db.beginTransaction();
            boolean result = true;
            for (Filter filter : filters) {
                ContentValues filterVal = new ContentValues();
                filterVal.put(Filter.TITLE, filter.getTitle());
                filterVal.put(Filter.KEYWORD, filter.getKeyword());
                filterVal.put(Filter.URL, filter.getUrl());
                filterVal.put(Filter.ENABLED, filter.isEnabled());
                long newFilterId = db.insert(Filter.TABLE_NAME, null, filterVal);
                if (newFilterId == -1) {
                    result = false;
                    break;
                }
            }
            if (result) db.setTransactionSuccessful();
        } catch (SQLiteException e) {
            e.printStackTrace();
        } finally {
            db.endTransaction();
        }
    }

    private ArrayList<Filter> getAllFilters(SQLiteDatabase db) {
        Cursor cursor = null;
        ArrayList<Filter> filters = new ArrayList<>();
        try {
            db.beginTransaction();
            String[] columns = {
                    Filter.ID,
                    Filter.TITLE,
                    Filter.KEYWORD,
                    Filter.URL,
                    Filter.FEED_ID,
                    Filter.ENABLED
            };
            cursor = db.query(Filter.TABLE_NAME, columns, "", null, null, null, null);
            if (cursor != null && cursor.getCount() > 0) {
                while (cursor.moveToNext()) {
                    int filterId = cursor.getInt(0);
                    String title = cursor.getString(1);
                    String keyword = cursor.getString(2);
                    String url = cursor.getString(3);
                    int feedId = cursor.getInt(4);
                    int enabled = cursor.getInt(5);
                    Filter filter = new Filter(filterId, title, keyword, url, feedId, enabled);
                    filters.add(filter);
                }
            }
            db.setTransactionSuccessful();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            db.endTransaction();
        }
        return filters;
    }
}
LINEで送る
Pocket

JenkinsでSlaveを作ってビルドする

久々にデスクトップを買ったのでUbuntuを入れてビルドマシンとして設定してみる。

Slave(Ubuntu14.0.4)

まずJenkinsをインストールする。

wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt-get update
sudo apt-get install jenkins

次にMaster-slave間でssh通信するために、公開鍵を受け取る必要があるので、一旦パスワード認証を有効にする。

# PasswordAuthentication yesに変更
sudo vim /etc/ssh/sshd_config
sudo /etc/init.d/ssh restart

Host(Mac)

  • MacBook Air (11-inch, Mid 2012)
  • OS X El Capitan 10.11.6
  • brew 1.1.0

Jenkinsのインストール

brew install jenkins
brew services start jenkins

brewで楽々インストール・・・と思ったらサービス開始のところで躓いた。

/Users/phicdy/Library/LaunchAgents/homebrew.mxcl.jenkins.plist: Operation not permitted

どうやらtmuxから実行したのが原因らしい。
brew servicesは中でlaunchctlを使っていて、tmux上ではnamespaceが変わってしまうため、launchctlをうまく実行できないとのこと。

参考: launchctl load で Operation not permitted(解決?)

brew install reattach-to-user-namespace

~/.tmux.confに以下を追加

set-option -g default-command "reattach-to-user-namespace -l bash"

鍵設定

scpで鍵を送って登録する。鍵送信後はパスワード認証を無効に戻しておく。

ssh-keygen
scp .ssh/id_rsa.pub slave-user@<slave IP>:/home/slave-user/
ssh slave-user@<slave IP>

# ここからSlave
cat id_rsa.pub >> ~/.ssh/authorized_keys
rm id_rsa.pub
exit

Slaveの追加

  1. Jenkinsの管理→ノードの管理→新規ノード作成
  2. リモートFSルートにリモートのワークスペースのパスを入力
  3. 起動方法を「SSH経由でUnixマシンのスレーブエージェントを起動」に変更
  4. ホストにIPを入力
  5. 認証情報を追加
  6. 種類をSSHユーザ名と秘密鍵
  7. ユーザ名をリモートのログインユーザに変更
  8. 秘密鍵を「Jenkinsマスター上の~/.sshから」に変更
  9. 保存

おわりに

Android開発のビルドはかなりCPUとメモリを使うので、スペックが低いとビルド中に何もできないことがあった。デバッグ中はAndroid Studioを使うでビルドする必要があるが、それ以外のビルドはSlaveを使うことでかなりリソースを節約できると思う。3万くらいで買えたマシンだったので有効活用していきたい。

LINEで送る
Pocket

ErgoDox EZを買いました

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

ergodox

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

キーマップ

現在のキーマップです。

layer0

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

親指周辺

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

英数⇔かな変換

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

括弧

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

ショートカットキー

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

レイヤー

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

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

layer1

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

L2: Windows, Ubuntu用

layer2

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

おわりに

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

LINEで送る
Pocket