Android用アプリをJavaで作成してみた【スクショソフト編】

Hello World!編→http://qiita.com/YSRKEN/items/945d134c540367ba8734
スクショソフト編→ここ

#概要
　[前回](http://qiita.com/YSRKEN/items/945d134c540367ba8734)ではとりあえずHello World!させてみましたが、当然このままでは終わりません。ボタンを設置して、本格的なアプリケーションを作成してみます。今回作成したいアプリの仕様は次の通り。

* 「スタート!」ボタンを押すと、タイマーが起動する
* アプリが起動している間、1秒間隔で画面のスクショが保存される

#あの日押したボタンの結果を僕達はまだ知らない。
　[以前Swingを利用してJavaアプリを作成した](http://qiita.com/YSRKEN/items/ac75ffcb0f72adbd2a7f)記憶から、今回も同じような手順なんだろうなと考えていました。ただ、「どうせRADだし簡単だろｗｗｗ」と、ボタンをウィンドウに配置してとりあえずダブルクリックしても、**Visual Studioみたいに自動でClickイベントが生成されてそこまで表示が遷移しない**んですね……。
　じゃあどうやってイベントを記述するかですが、手順としては次のようになります。
(参考：[2.1 ボタンのタップイベントを取得する](http://dixq.net/Android/02_01.html))

* ボタンのプロパティから、**id**を適当な名前(StartButtonなど)、**text**を適当な名前("スタート！"など)に変更する。前者は**オブジェクトのID**、後者は**オブジェクトのテキスト**(ボタンで言えば表示される名前)となる
* 上記の変更点は、デフォルトではactivity_main.xmlに記録されている。気になる人はプロジェクト全体から検索を掛けてみよう
* OnClickListenerをimplementsすることでonClickメソッドを引っ張ってくる。OnClickListenerを利用するには[他にも多種多様な手法がある](http://qiita.com/t2low/items/8ac683c7ebf4b6dd1b41)が、一番手っ取り早い方法はこれなはず。以下にサンプルを示すが、//addが追記場所であることに注意したい(宣言を端折ってたりした場合は形名などが赤字で表示されるので、**Alt+Enterで自動補完**することが出来る。なかなかに優秀ですな)

```sample1.java
package com.example.(略).capture;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;	//add
import android.view.View;		//add
import android.widget.Toast;	//add
import android.view.View.OnClickListener;	//add

//add(implements OnClickListener)
public class MainActivity extends AppCompatActivity implements OnClickListener{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button)findViewById(R.id.StartButton);	//add
        button.setOnClickListener(this);	//add
    }
    public void onClick(View v){	//add(メソッド全体)
        Toast.makeText(this, "ボタンが押されました", Toast.LENGTH_LONG).show();
    }
}
```

* ここでまず注目したいのがonClickメソッド。OnClickListenerを利用する際は必ず実装しなければならない。setOnClickListenerで登録して～という部分は、[普通のJavaプログラミングでもおなじみ](http://www.atmarkit.co.jp/ait/articles/0608/19/news014_2.html)なので迷うことはないはず。
* また、Toastは下記画像のように[ポップアップメッセージを出すための機能](http://techbooster.org/android/application/163/)。Toast.LENGTH_SHORTで短時間、Toast.LENGTH_LONGで長時間表示します。スマホ触ってたら時々見ますよね？
![cap2.png](https://qiita-image-store.s3.amazonaws.com/0/29802/8a79c451-bff4-76c7-ad3c-aeb9513d1a41.png)

#そのタイマーイベントが、僕達の永遠になる
　Javaにおけるタイマーイベントは幾つか種類がありますが、今回は[Javaで艦これ用高性能スクショソフトを作成した](http://qiita.com/YSRKEN/items/ceb75d86650b030c4044#%E6%96%B0%E9%96%8B%E7%99%BA%E3%81%AE%E8%87%AA%E5%8B%95%E7%94%BB%E5%83%8F%E5%8F%96%E5%BE%97%E6%A9%9F%E8%83%BD%E3%81%8C%E8%87%AA%E6%85%A2%E3%81%AA%E3%81%AE)時とは違い、java.util.Timerを利用することにします。だって今回は別に[Swing使ってない](http://allabout.co.jp/gm/gc/80740/)しね。
　こちらも[使用方法はとても簡単](http://www5b.biglobe.ne.jp/~psoft/lesson/timer.html)で、java.util.TimerTaskを継承したクラスのrunメソッドで定期実行したいタスクを記述した後、Timerのインスタンスを生成してTimer#scheduleでスケジューリングすれば実行できます。やったぜ。

　……ただ、[UIスレッドじゃないとGUI部品にアタッチできない](http://d.hatena.ne.jp/Kazzz/20101125/p1)ので、次のようなコードだとボタンを押した際に「**java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()**」が出て停止してしまいます。

```sample2.java
import java.util.Timer;
import java.util.TimerTask;
class showMessageTask extends TimerTask {
	public void run() {
		Toast.makeText(MainActivity.this, "メッセージ表示", Toast.LENGTH_SHORT).show();
	}
}

public void onClick(View v) {
	Timer timer = new Timer();
	timer.schedule(new showMessageTask(), 100, 5000);
}
```
そこで、[Handlerで更新処理をUIスレッドにpostする](http://techbooster.jpn.org/andriod/application/934/)ことで、上記エラーを防止しています。

```sample3.java
import android.os.Handler;
import java.util.Timer;
import java.util.TimerTask;
class showMessageTask extends TimerTask {
	private final Handler handler = new Handler();

	public void run() {
		handler.post(new Runnable() {
			public void run() {
				Toast.makeText(MainActivity.this, "メッセージ表示", Toast.LENGTH_SHORT).show();
			}
		});
	}
}

public void onClick(View v) {
	Timer timer = new Timer();
	timer.schedule(new showMessageTask(), 100, 5000);
}
```

#心がスクショしたがっているんだ。
　後はTimer内のrun()にスクショ機能を組み込めばいいだけです。具体的には、Viewオブジェクトに対してView#getDrawingCacheでBitmap型のキャッシュを取得し、Bitmap#createBitmapで複製すればいいでしょう。ただ、**Androidは画像保存にjavax.imageio.ImageIOが使えません**ので、FileOutputStreamとBitmap#compressメソッドで保存することになります。また、「\<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/\>」という宣言をAndroidManifest.xmlに入れておかないと**権限不足で画像を保存できません**のでご注意下さい。以上の手順の詳細については次の記事をご覧ください。
　[AndroidでViewのキャプチャを撮る &lsaquo; ワンダープラネット株式会社（Wonderplanet Inc.）](http://wonderpla.net/blog/engineer/Android_CaptureView/)

```sample4.java
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.os.Environment;
import android.view.View;
import java.io.File;
import java.io.FileOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public void run() {
	final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
	final Date date = new Date(System.currentTimeMillis());
	final File file = new File(Environment.getExternalStorageDirectory() + "/hoge/" + df.format(date) + ".png");
	file.getParentFile().mkdir();
	saveCapture(findViewById(android.R.id.content),file);
	Toast.makeText(MainActivity.this, df.format(date), Toast.LENGTH_SHORT).show();
}

// スクリーンショットを取得して保存する
public void saveCapture(View view, File file) {
	Bitmap capture = getViewCapture(view);
	FileOutputStream fos = null;
	try {
		fos = new FileOutputStream(file, false);
		// 画像のフォーマットと画質と出力先を指定して保存
		capture.compress(CompressFormat.PNG, 100, fos);
		fos.flush();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		if (fos == null) return;
		try {
			fos.close();
		} catch (Exception ie) {
			fos = null;
		}
	}
}

// スクリーンショットを取得する
public Bitmap getViewCapture(View view) {
	view.setDrawingCacheEnabled(true);
		Bitmap cache = view.getDrawingCache();
		if(cache == null) return null;
		Bitmap screen_shot = Bitmap.createBitmap(cache);
	view.setDrawingCacheEnabled(false);
	return screen_shot;
}
```

　……んー？　これだと、**自分のアプリでのスクショは撮れるけど、それ以外は撮れないぞ？**
　というのも、Androidの場合、セキュリティの都合上、[Android 5.0以前では他アプリのスクショも撮れるAPIを提供していませんでした](http://techbooster.org/android/application/17026/)。逆に言えば5.0以降なら権限で許可すれば使えるのですが、[手持ちのスマホ](http://www.sharp.co.jp/products/sh02e/)が5.0以前なのでやる気が出ませんでした……。

#以下おまけ
##とあるスマホの遠隔動作確認《無線LANデバッグ》
※《》記号は[青空文庫形式のルビ](http://www.aozora.gr.jp/annotation/etc.html#teisei_mama)
　スマホの実機で動作確認する際は、普通PCにスマホを有線で接続します。だけど面倒臭いなーと思っていたところ、なんと[無線LAN接続でデバッグが利用できる](http://qiita.com/satoshi_imanishi/items/afb837cd22e5304abad5)と判明。具体的には、[スマホのIPアドレスをメモった後、そこに(任意の)指定したポートでアクセスするようにadbを設定する](http://www.absolute-keitarou.net/blog/?p=874)だけ！
　ただ、PCを再起動とかするとリセットされるのでその辺はご注意下さい。

```sample.bat
# PCとスマホを接続した後に下のコマンド(ポート番号は任意)を入れる
> adb tcpip 5555
# 192.168.0.17 はメモったIPアドレス。番号は先ほどと同じ
> adb connect 192.168.0.17:5555
# 接続解除するためのコマンド(PCを再起動すると自動で切られるので注意)
> adb disconnect 192.168.0.17
```

##上記で使用したスクショアプリの全ソース

```MainActivity.java
package com.example.(略).capture;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.os.Environment;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity implements OnClickListener {
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.StartButton);
        button.setOnClickListener(this);
    }

    class showMessageTask extends TimerTask {
        private final Handler handler = new Handler();

        public void run() {
            handler.post(new Runnable() {
                public void run() {
					final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
					final Date date = new Date(System.currentTimeMillis());
					final File file = new File(Environment.getExternalStorageDirectory() + "/hoge/" + df.format(date) + ".png");
					file.getParentFile().mkdir();
					saveCapture(findViewById(android.R.id.content),file);
                    Toast.makeText(MainActivity.this, df.format(date), Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

    public void onClick(View v) {
        Timer timer = new Timer();
        timer.schedule(new showMessageTask(), 100, 5000);
    }

	// スクリーンショットを取得して保存する
	public void saveCapture(View view, File file) {
		Bitmap capture = getViewCapture(view);
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file, false);
			// 画像のフォーマットと画質と出力先を指定して保存
			capture.compress(CompressFormat.PNG, 100, fos);
			fos.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (fos == null) return;
			try {
				fos.close();
			} catch (Exception ie) {
				fos = null;
			}
		}
	}

	// スクリーンショットを取得する
	public Bitmap getViewCapture(View view) {
		view.setDrawingCacheEnabled(true);
			Bitmap cache = view.getDrawingCache();
			if(cache == null) return null;
			Bitmap screen_shot = Bitmap.createBitmap(cache);
		view.setDrawingCacheEnabled(false);
		return screen_shot;
	}
}

```