Androidプロジェクトをシュッと開く

Androidプロジェクトを開く時、Android Studioのメニューから開くのはとてもめんどうくさい。File -> Open... と選択してからプロジェクトの在り処を選択する必要がある。めんどうだ。マウス操作めんどう。

実はコマンドラインからプロジェクトをAndroid Studioで開くコマンドラインツールがAndroid Studioに備わっている。なんかしらのプロジェクトを開いている状態でTools -> Create Command-line Launcher... を選んでコマンドを作るだけだ。

f:id:tomorrowkey:20160731142409p:plain

コマンドが作られると、プロジェクトがカレントディレクトリの状態で次のコマンドを実行するだけでプロジェクトを開くことができる。

studio .

楽ちん〜。

この機能はすべてのJetBrains系のIDEについているみたいなので、例えばRubyMineとかでも使えます。

さらにAlfredを使っている人は次のworkflowもオススメ

github.com

studioと入力すると開くプロジェクトがでてくるので素早くプロジェクトを開くことができる。

f:id:tomorrowkey:20160731143152p:plain

ここにでてくるプロジェクトはAndroid Studioで開いたことがあるものだけなので注意が必要。

adb-pecoを書き直しました

複数デバイスが接続されていたときにデバイスを選択する支援をしてくれる adb-peco ですが、この度、Rubyでコードを書き直しました。 通常だとmore than one device/emulator と出力されるようなシチュエーションでも、こんな風にコマンド実行時にデバイス選択することができます。

f:id:tomorrowkey:20160731134935g:plain

書き直したことにより次の2つの改善がなされました。

  • インストールの簡素化
  • バイス指定の必要のないコマンドではデバイス指定なしに

インストールの簡素化

Ruby Gemで作っているためインストールがコマンド1つで完結するようになりました。

gem install adb-peco

バイス指定の必要のないコマンドではデバイス指定なしに

以前のshell版ではdeviceskill-serverなどでもデバイス指定が必要でした。これらのコマンドでは本来デバイス指定は必要ないので、なくしました。ホワイトリスト形式で除外しているので、デバイス指定が必要ないコマンドが他にありましたらissue立てて貰えると対応します。

最後に

ソースコードGitHubで公開していますので、うまく動かないなどありましたらissue立ててもらえると助かります。

github.com

Androidアプリのビルドが終わったらにゃーんと猫の声が聞こえるようにする

Androidアプリのビルドはターミナル派でしょうか、それともAndroid Studio派でしょうか。 最近はInstant Runや、接続されているデバイスの情報から不要なリソースを削ることでビルドの高速化されるなど、Android Studioでのビルドは魅力的なところが多いのですが、私は依然ターミナル派です。*1

いままで作ったコマンド

これまで、ターミナルで快適に開発ができるように、便利なコマンドを開発しては公開してきました。

techlife.cookpad.com techlife.cookpad.com

中でも特に気に入ってるものの1つがnotifier-pluginです。

tomorrowkey.hatenablog.jp

notifier-pluginとは

Androidアプリのビルドって結構長いですよね。規模によってはフルビルドで3~4分かかってしまうアプリも少なくないんじゃないでしょうか。 ビルドの待ち時間が長いとついついTwitterでハイハイワロスワロスと糞ツイートが捗ってしまいます。いつのまにかTwitterに熱中してしまってビルドが終わっても気づかずに10分以上Twitterやってたみたいな経験はないでしょうか。
そこで開発したのがnotifier-pluginで、ビルドが終わったタイミングで音声で教えてくれたり、通知センターで教えてくれたり、決まった音楽を流すことができます。 導入方法はbuild.gradleに書いて、設定ファイルを1つ追加するだけです。

iTerm2

話は変わりますが、私は快適なターミナル生活のためにiTerm2を使っています。
iTerm2はMacのターミナルをもっと便利にするアプリケーションで、決まったショートカットを押すとシュッとターミナルを出すことができるところが気に入ってます。そのiTerm2が最近version 3になりました。ただシュッとするためだけに使ってきたiTerm2ですが、これを機に機能を調べているとTriggerという機能があり、これを使えばプラグインすらいらねーんじゃねと思って使ってみました。

iTerm2でビルドがにゃーんと分かる

Triggerの設定をしましょう。

Preferences > Profiles > $YOUR_PROFILE > Advanced > Triggers > Edit

f:id:tomorrowkey:20160609154345p:plain

+ボタンから新しいトリガーを設定します。

f:id:tomorrowkey:20160609154350p:plain

正規表現で一致したテキストが出力されると何らかのアクションを起こすことができます。

Gradleビルド成功/失敗時のログは次のようになります。

# 成功時
BUILD SUCCESSFUL

# 失敗時
BUILD FAILED

よって正規表現を次のように定義しました。

^(BUILD (SUCCESSFUL|FAILED))$

起こすアクションはコマンドの実行です。 次のコマンドを実行するように定義しました。

say -v 'Alex' \1

Macにプリインされているsayコマンドを使い、Alexがビルド結果を教えてくれます。 正規表現で一致したテキストをアクションで使用しています。そのへんのルールはここに https://www.iterm2.com/documentation-triggers.html あります。

にゃーんと鳴いてねーじゃねーかと思いますが、任意の音声を再生するならこんな感じでしょうか。

afplay ~/Downloads/meow.mp3

これでにゃーんと鳴きます。

通知センターで表示しながらAlexに喋らせるならこんな感じでしょうか。

echo 'display notification "\1" with title "Gradle"' | osascript; say -v 'Alex' \1

まとめ

Gradle pluginを使わずにビルドが終わったことを音などで知らせることができました。これはAndroidアプリのビルド以外にも時間がかかる処理に応用ができてなかなか便利ですね。 またnotifier-pluginはGradle pluginとして動作するので、Android Studio経由のビルドでも音を鳴らすことができるので、そちらもぜひご活用ください。

*1:Instant Runがまだ不安定な部分があり、完全に導入しきれてないなどの理由がありますが、多くは語りません

Androidエンジニアは自分専用のDockerイメージを作るべきだと思った

初めてDockerイメージを作ったのでエントリに書いておきます。タイトルはすこし言い過ぎな感じはありますが、CIにDocker使っている人は自分でDockerイメージ作った方がいいよねって話です。

私とCI

CIという言葉もかなり広まり、会社で使うだけではなく個人の開発でもCIを導入している人は多いのではないでしょうか。私はVPN + JenkinsでCIを始めて、1年半前にWerckerに乗り換えました。Jenkinsにはとてつもない自由さやCIがコケた時に原因究明が楽な点がありますが、メンテナンスが大変でした。CIサービスはメンテナンスがあまり必要なくてとても気に入ってます。
Werckerのメリットについては他に書かれている方がたくさんいるので、適当にググって読んでみてください。

WerckerとDocker

私がWerckerに移行した当時はBoxと呼ばれる実行環境でビルドするようになっていましたが、少し前からDockerに差し替わりました。Dockerというキーワードは知っていたもののイメージを作ったことも使ったこともなかったので、適当にイメージを探して他の人のイメージを使わせてもらっていました。しばらくはそれで大丈夫だったのですが、最近この辺でストレスが溜まるようになってきました。

ストレスの原因

一番の理由はAndroidのbuild toolやサポートライブラリのアップデートについていくために実行環境のアップデートをしないといけないのですが、他人のイメージを使っているがために、なかなかアップデートすることができないところです。Dockerfileはオープンソースで公開されているんだからPR出せばいいじゃんという声も聞こえてきそうですが、PRを出したとしてもすぐにマージされるわけではないので、そこでストレスが溜まってしまいます。他の人のライブラリや環境を使うなら当然のリスクですが、プライベートで開発できる時間は限られています。こういった待ち時間があるとついついモチベーションが落ちがちなのでどうにかしたいです。

環境の変化

Android誕生してからもう少しで10年経とうとしていますが、その開発環境は決して枯れることはなく常に変化しています。今もJack and JillやOpen JDK移行などが迫ってきており、今後も開発環境の決定版といえるものはでてこないと思います。

使いたいパッケージの違い

また、人によって使いたいパッケージは異なります。私の場合、CIが終わった後にSlackで通知をしたく、そういったプラグインを使っているのですが、そのプラグインRubyで書かれているためRubyをインストールする必要があります。こういった要望はなかなかPRにすることはできません。

そして自分のDockerイメージを作った

というわけでこのストレスを解消すべく自分でAndroidアプリのビルドができるDockerイメージを作りました。

作ったイメージはdocker hubに公開したので、試してみたいという方はどうぞ

https://hub.docker.com/r/tomorrowkey/wercker-container-android/

DockerfileもGitHubに公開しました。

github.com

できるだけ最新のものにアップデートしていきますしそういったPRも大歓迎ですが、ベストエフォートなのであしからず。そこでストレスが溜まってきたら自分のイメージを作るいいタイミングだと思います。私のリポジトリをforkしてもいいですし、一から作るのもDockerを理解することができると思うのでおすすめです。

メモ

以下、イメージを作るにあたってどう解決したかメモを残しておきます。

OS

FROM ubuntu:14.04

ベースはAndroidのビルド環境によく使われるUbuntuにした。

Oracle Java8のインストール

# Java8 installation
RUN \
  apt-get install -y software-properties-common curl && \
  add-apt-repository -y ppa:webupd8team/java && \
  apt-get update -y && \
  echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
  apt-get install -y oracle-java8-installer

oracle-javaのインストールはapt-getに新しくリポジトリを追加することでできると知った。

How To Install Oracle Java 8 In Debian Via Repository [JDK8] ~ Web Upd8: Ubuntu / Linux blog

リポジトリ追加したらapt-get updateしてインストール可能なソフトウェアの更新が必要なところに注意

echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selectionsと書かれているところは使用許諾の同意に必要。

Docker上でOracle Javaの実行はライセンス違反の可能性があるとの情報を得たのでOpen JDKを使うように修正しました。

Dockerコンテナ上でのJavaの実行はライセンス違反なのか?

RUN \
  apt-get install -y software-properties-common curl && \
  add-apt-repository -y ppa:openjdk-r/ppa && \
    apt-get update && \
    apt-get install -y openjdk-8-jdk

OpenJDK8もppaの追加が必要なので、add-apt-repositoryを使ってインストール。以前OpenJDKを使ったときにビルドできないなどの不具合があった気がしたが、いまのところ問題なさそうなのでしばらくこれでやってみようと思います。

Android SDKのインストール

# Android SDK Installation
ENV ANDROID_SDK_REVISION r24.4.1
RUN \
  cd /usr/local && \
  curl -L -O "https://dl.google.com/android/android-sdk_$ANDROID_SDK_REVISION-linux.tgz" && \
  tar -xf "android-sdk_$ANDROID_SDK_REVISION-linux.tgz" && \
  rm "/usr/local/android-sdk_$ANDROID_SDK_REVISION-linux.tgz"
RUN apt-get install -y lib32z1 lib32gcc1

Android SDKのインストールはアーカイブをダウンロードして、配置すればよいと思っていたが、aaptが使うパッケージが足りなかったみたいなので、追加でlib32z1lib32gcc1をインストールしている。実際にCIでビルドしようと思っていたアプリをビルドしてみたらaaptがエラーを吐いていたので何か足りないんだなと分かった。あとは適当に調べてパッケージをインストールした。

androidコマンドがなくなる

RUN echo y | android update sdk --no-ui --force --all --filter "tools"
RUN echo y | android update sdk --no-ui --force --all --filter "platform-tools"

toolsとplatform-toolsを同時にインストールしようとしたが、そしたらandroidコマンドがなくなるという不具合に出会った。別にインストールすると問題なかった。

Rubyのインストール

# Ruby installation
RUN add-apt-repository -y ppa:brightbox/ruby-ng && apt-get update && apt-get install ruby

ビルド終わった後にSlackで通知するようにしているのだが、そのプラグインRubyが必要なのでインストールしている。 当初はrbenvを使ってRubyをインストールするように実装してみたが、rbenvはexport環境変数を設定しようとする。Dockerではビルド中にexportしても無視されてしまうので、素直にrbenvを使うことができない。ではDockerで環境変数を設定するにはどうするかというとENVを使わなければならない。どうにかスクリプトに書き込むことでrbenvを使うことができるようになったが、CI上ではパスが通らなくて調査困難で諦めた。

Rubyのインストールも新しくリポジトリを追加しないといけなかったので、追加してる。

Ruby packages for Ubuntu - Brightbox Cloud

apt-add-repository vs add-apt-repository

リポジトリの追加するコマンドはapt-add-repositoryadd-apt-repositoryがあるみたいで、どう違うか調べた。どちらも一緒らしいので、add-apt-repositoryを使うように揃えた。

Dockerコマンドとか

docker-machineの起動

docker-machine start default

Dockerfileのビルド

docker build .

イメージの列挙

docker images -a

イメージの起動

docker run -it <IMAGE_ID>

コンテナの列挙

docker ps -a

コンテナの停止

docker rm <CONTAINER_ID>

全コンテナの停止

docker ps -aq | xargs docker rm

ネットワークが繋がらなくなる

ルノアールにてdocker buildしようとしたらネットワークが不通になることに少しだけハマった。詳しくはよくわからないが、接続するネットワークが変わったことが原因っぽい。docker-machine restartで再起動すると治った。

RoundRectShapeの引数について調査した

角丸画像をコードから生成したくて、ShapeDrawableを使うことになった。 角丸にするにはRoundRectShapeをShapeDrawableに食わせるんだけど、その引数がドキュメントだけだと分からなかったので検証した。

RoundRectShape(float outerRadii, RectF inset, float innerRadii) RoundRectShape constructor. Specifies an outer (round)rect and an optional inner (round)rect. outerRadii float: An array of 8 radius values, for the outer roundrect. The first two floats are for the top-left corner (remaining pairs correspond clockwise). For no rounded corners on the outer rectangle, pass null.

時計回りで指定すればいいんだよってことは分かるけど、8つ渡すのはよく分からなかった。

RoundRectShape | Android Developers

結論からいうと以下のようになっている

float[]{
  topLeft-x, topLeft-y,
  topRight-x, topRight-y,
  bottomRight-x, bottomRight-y,
  bottomLeft-x, bottomLeft-y
}

4つの角 + xとyで8つある理由ですね。 検証に使用したコードを載せておきます。

f:id:tomorrowkey:20160512132934p:plain:w200

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import jp.tomorrowkey.android.roundrectshapeapp.databinding.ActivityMainBinding;
import jp.tomorrowkey.android.roundrectshapeapp.databinding.ListItemBinding;

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        binding.recyclerView.setAdapter(new MyRecyclerViewAdapter(this));
    }

    private static class MyRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> {

        private static float[][] PATTERNS = new float[][]{
                {50, 50, 0, 0, 0, 0, 0, 0},
                {0, 0, 50, 50, 0, 0, 0, 0},
                {0, 0, 0, 0, 50, 50, 0, 0},
                {0, 0, 0, 0, 0, 0, 50, 50},
                {50, 50, 50, 50, 0, 0, 0, 0},
                {25, 50, 25, 50, 0, 0, 0, 0},
                {25, 50, 25, 50, 25, 50, 25, 50},
        };

        private static final int SOLID_COLOR = Color.rgb(0x00, 0x80, 0x80);

        private static final int RECT_SIZE = 100;

        LayoutInflater inflater;

        public MyRecyclerViewAdapter(Context context) {
            inflater = LayoutInflater.from(context);
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new ViewHolder(inflater.inflate(R.layout.list_item, parent, false));
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            ListItemBinding binding = DataBindingUtil.bind(holder.itemView);

            float[] outerR = PATTERNS[position];
            binding.textView.setText("{" + Util.join(outerR, " ,") + "}");

            ShapeDrawable drawable = new ShapeDrawable(new RoundRectShape(outerR, null, null));
            drawable.getPaint().setColor(SOLID_COLOR);
            drawable.getPaint().setStyle(Paint.Style.FILL);
            drawable.setIntrinsicHeight(RECT_SIZE);
            drawable.setIntrinsicWidth(RECT_SIZE);
            binding.imageView.setImageDrawable(drawable);
        }

        @Override
        public int getItemCount() {
            return PATTERNS.length;
        }
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);
        }
    }

}

ObjectAnimatorでTextViewの内容を書き換える

ObjectAnimatorおもろいなー setNumber(int)を持つNumberTextViewというクラスを作り

public class NumberTextView extends AppCompatTextView {

    public NumberTextView(Context context) {
        super(context);
    }

    public NumberTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NumberTextView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setNumber(int number) {
        setText(String.valueOf(number));
    }
}

ObjectAnimatorでエイッとすると

ObjectAnimator objectAnimator = ObjectAnimator.ofInt(binding.numberTextView, "number", 0, 10000);
objectAnimator.setDuration(10 * 1000);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.start();

数値がテキストとして表示される

f:id:tomorrowkey:20160301093402g:plain

単にリフレクション使って値を設定しているだけなのかなー。

ProgressBarの値をアニメーションで変更する

f:id:tomorrowkey:20160229190212g:plain ↑こういうことやりたいんだけど、ProgressBar#setProgress(:int)だとアニメーションしてくれない。

そうだ、ObjectAnimatorを使ってアニメーションを実現しよう。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context=".activity.MainActivity"
  >
  <LinearLayout
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ProgressBar
      style="?android:attr/progressBarStyleHorizontal"
      android:id="@+id/progress_bar"
      android:layout_margin="16dp"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>

    <Button
      android:id="@+id/update_progress_button"
      android:text="Update progress"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {

  ActivityMainBinding binding;

  ObjectAnimator objectAnimator;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.updateProgressButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        int progress = (int) (Math.random() * 100);
        setProgressWithAnimation(progress);
      }
    });
  }

  private void setProgressWithAnimation(int progress) {
    int[] values = {binding.progressBar.getProgress(), progress};
    if (objectAnimator == null) {
      objectAnimator = ObjectAnimator.ofInt(binding.progressBar, "progress", values);
    } else {
      objectAnimator.cancel();
      objectAnimator.setIntValues(values);
    }
    objectAnimator.setDuration(1000);
    objectAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    objectAnimator.start();
  }

}

Activityに書いちゃうとアレなんでカスタムViewでやるとスッキリしそうでよさそう。 アニメーションしている最中はそのときのProgressの値になってしまうので、getProgress()を使いたいケースではこれは使えない。