LiveWallpaperをつくる -デバッグ編-

LiveWallpaperの動作確認用のActivityを用意して開発するためのメモ。
前回、LiveWallpaperを作りましたが、デバッグや動作確認を行うのが結構めんどいため、Androidアプリとして開発を行えるようにします。

概要

手順としては、以下のとおりです。

  1. Androidアプリにするため、Activityクラスを作成
  2. AndroidManifest.xmlに、アプリとしての設定を記述

手順

MainActivityの作成

Activity継承クラス、MainActivityを作成します。
また、描画をSerfaceViewに行うため、SerfaceViewを設定したlayout/main.xmlを準備します。

package com.ttshrk.livewallpaper_example;

import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.Handler;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {

	private final Handler mHandler = new Handler();
	private SurfaceView mSurfaceView;
	boolean mVisible = true;
	private int mMillisPerFrame;

	private final Runnable mDrawer = new Runnable() {
		public void run() {
			drawFrame();
		}
	};

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		getWindow().setFormat(PixelFormat.TRANSLUCENT);

		setContentView(R.layout.main);
		mMillisPerFrame = 1000 / 30;

		mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView1);
		// 描画開始
		mHandler.postDelayed(mDrawer, 0);
	}

	public void drawFrame() {
		final SurfaceHolder holder = mSurfaceView.getHolder();

		Canvas c = null;
		try {
			c = holder.lockCanvas();
			if (c != null) {
				// draw something
				update();
				c.drawColor(Color.BLACK);
				draw(c);
			}
		} finally {
			if (c != null)
				holder.unlockCanvasAndPost(c);
		}

		mHandler.removeCallbacks(mDrawer);
		if (mVisible) {
			mHandler.postDelayed(mDrawer, mMillisPerFrame);
		}
	}

	void update() {
	}

	void draw(Canvas canvas) {
	}
}

SerfaceViewに描画を行うだけのActivityです。
タイトルの表示をけして、画面いっぱいに表示しています。
onCreate、update、drawに確認を行いたいモジュールを設定してください。
(widthやheightなど必要なプロパティは適宜設定してください。)


次にレイアウトxmlです。

  • layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<SurfaceView android:id="@+id/surfaceView1"
		android:layout_height="fill_parent" android:layout_width="fill_parent"></SurfaceView>
</LinearLayout>

これも、画面いっぱいにSurfaceViewを貼り付けただけです。

AndroidManifest.xmlの修正

AndroidManifest.xmを修正します。
Androidアプリとして起動できるよう、ApplicationにMainActivityを設定します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.ttshrk.livewallpaper_example"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="7" />
    <!--<uses-feature android:name="android.software.live_wallpaper" />-->

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MainActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

<!-- 
    <application
        android:label="@string/app_name"
        android:icon="@drawable/icon" android:debuggable="true">

        <service
            android:label="@string/app_name"
            android:name=".ExampleWallpaer"
            android:permission="android.permission.BIND_WALLPAPER">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper" />
        </service>
        
    </application>
-->
</manifest>

とりあえず、Wallpaperサービスを残してあります。
これも、applicationタグを追加しただけです。


だいたい以上です。
描画を行う部分をクラスにして設定すれば壁紙をあぷりとして実行できると思います。


おまけ

Handler#delayPostのdelay値は結構いい加減なので、FPSを一定にしたいときは、以下のようにするといいと思います。

  • MainActivity.java 抜粋
	public void drawFrame() {
		final SurfaceHolder holder = mSurfaceView.getHolder();

		Canvas c = null;
		try {
			c = holder.lockCanvas();
			if (c != null) {
				// draw something
				update();
				c.drawColor(Color.BLACK);
				draw(c);
			}
		} finally {
			if (c != null)
				holder.unlockCanvasAndPost(c);
		}

		mHandler.removeCallbacks(mDrawer);
		if (mVisible) {
			long delay = delay();
			mHandler.post(mDrawer, delay);
		}
	}

	private long delay() {
		long currentDelay = mMillisPerFrame - (int) mMilliTimeMesure.getElapsed();
		mMilliTimeMesure.reset();
		// 遅延は無視
		if (currentDelay < 0)
			return 0L;
		return currentDelay;
	}

1フレームごとにかかった経過時間を測定して、自前でsleep処理を行います。
IS01でも、30FPSくらいなら問題なく動作します。


MilliTimeMeasureですが、こんな感じの経過時間を取得するだけのクラスを用意します。

  • MilliTimeMeasure.java
public class MilliTimeMeasure {
	private long lastTime;
	
	public MilliTimeMeasure() {
		reset();
	}
	
	private long getCurrent() {
		return System.currentTimeMillis();
	}
	
	public void reset() {
		lastTime = getCurrent();
	}
	
	public long getElapsed() {
		return getCurrent() - lastTime;
	}
}


サンプルコード
livewallpaper_example2.zip 直

LiveWallpaperをつくる

LiveWallpaperを作るメモ。
Handlerを利用してメッセージを処理する(描画を行う)サンプルです。

概要

必要条件
実装メモ
  • WallpaperServiceを実装する
  • WallpaperService.Engine、を実装する
  • Handlerを通じて描画を行う
  • layoutのxmlは使用できない
  • AndroidManifest.xmlファイルにlive_wallpaperを設定
  • SurfaceView、Canvasで描画

実装

プロジェクトの作成

API lvevel 7(Android 2.1)以上でAndroidプリプロジェクトを生成します。

WallpaperService継承クラス作成

WallpaperServiceを継承するクラスを作成します。
このクラスがLiveWallpaperサービスの主な実装となります。

  • ExampleWallpaer.java
package com.ttshrk.livewallpaper_example;

import android.os.Handler;
import android.service.wallpaper.WallpaperService;
import android.service.wallpaper.WallpaperService.Engine;

public class ExampleWallpaer extends WallpaperService {

    private final Handler mHandler = new Handler();
    
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public Engine onCreateEngine() {
    	// TODO
    	return null;
    }
}

このクラスの中のWallpaperService::onCreateEngineが描画を行うインスタンス(後述)を返します。
このEngineクラスのインスタンスは複数作成されることがあるため、静的プロパティの扱いに注意が必要です。(壁紙に設定してあるLiveWallpaperを設定画面のプレビューで表示したときなど。)

WallpaperService.Engineを実装する

壁紙の描画を行うクラスExampleWallpaer.ExampleEngineを作成します。
このクラスは、WallpaperService.Engineを継承します。

  • WallpaperService.java
public class ExampleWallpaer extends WallpaperService {
	private final Handler mHandler = new Handler();

	@Override
	public void onCreate() {
		super.onCreate();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
	}

	@Override
	public Engine onCreateEngine() {
		return new ExampleEngine();
	}

	class ExampleEngine extends Engine {
		Context context;
		int width;
		int height;
		boolean mVisible;
		PointF timePoint = new PointF();
		Paint timePaint;
		float mMillisPerFrame;
		SimpleDateFormat simpleDateFormat;
		String timeString;

		private final Runnable mDrawer = new Runnable() {
			public void run() {
				drawFrame();
			}
		};

		ExampleEngine() {
			context = ExampleWallpaer.this.getApplicationContext();
			width = getWallpaperDesiredMinimumWidth() / 2;
			height = getWallpaperDesiredMinimumHeight();
			mMillisPerFrame = 1000f / 30f;
			simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
			timePaint = new Paint();
			timePaint.setAntiAlias(true);
			timePaint.setColor(Color.WHITE);
			timePaint.setTextSize(80);
			timePaint.setTextAlign(Paint.Align.RIGHT);
			timePaint.setAlpha(0xcf);
			timePoint.set(width, height * 3 / 5);
			timeString = "";
		}

		@Override
		public void onCreate(SurfaceHolder surfaceHolder) {
			super.onCreate(surfaceHolder);
		}

		@Override
		public void onDestroy() {
			super.onDestroy();
			mHandler.removeCallbacks(mDrawer);
		}

		@Override
		public void onVisibilityChanged(boolean visible) {
			mVisible = visible;
			if (visible) {
				drawFrame();
			} else {
				mHandler.removeCallbacks(mDrawer);
			}
		}

		@Override
		public void onSurfaceChanged(SurfaceHolder holder, int format,
				int width, int height) {
			super.onSurfaceChanged(holder, format, width, height);
			drawFrame();
		}

		@Override
		public void onSurfaceCreated(SurfaceHolder holder) {
			super.onSurfaceCreated(holder);
		}

		@Override
		public void onSurfaceDestroyed(SurfaceHolder holder) {
			super.onSurfaceDestroyed(holder);
			mVisible = false;
			mHandler.removeCallbacks(mDrawer);
		}

		@Override
		public void onOffsetsChanged(float xOffset, float yOffset, float xStep,
				float yStep, int xPixels, int yPixels) {
			drawFrame();
		}

		/*
		 * Store the position of the touch event so we can use it for drawing
		 * later
		 */
		@Override
		public void onTouchEvent(MotionEvent event) {
			timePoint.set(event.getX(), event.getY());
			super.onTouchEvent(event);
		}

		void drawFrame() {
			final SurfaceHolder holder = getSurfaceHolder();

			Canvas c = null;
			try {
				c = holder.lockCanvas();
				if (c != null) {
					update();
					c.drawColor(Color.BLACK);
					draw(c);
				}
			} finally {
				if (c != null)
					holder.unlockCanvasAndPost(c);
			}

			// Reschedule the next redraw
			mHandler.removeCallbacks(mDrawer);
			if (mVisible) {
				mHandler.postDelayed(mDrawer, (int) mMillisPerFrame);
			}
		}

		void update() {
			timeString = simpleDateFormat.format(new Date());
		}

		void draw(Canvas canvas) {
			canvas.drawText(timeString, timePoint.x, timePoint.y, timePaint);
		}

	}
}


ポイントは以下の通りです。

  • Handler

ExampleWallpaer#mHandlerにメッセージ(mDrawer)をPostして描画を行います。Handlerクラスは受け取ったメッセージを独立したスレッドで順次処理していくため、サービスの実行を阻害せずに描画を行ってくれます。

  • ExampleEngine#mDrawer

描画を行うRnnbable(メッセージ)です。

  • ExampleEngine#onVisibilityChanged

壁紙が表示、非表示されたタイミングで呼ばれます。
このイベントで、描画の開始や停止を設定します。

  • ExampleEngine#drawFrame

1frameあたりの処理を行います。また、次に行う描画処理の登録も行います。

AndroidManifest.xml

AndroidManifest.xmlにLiveWallpaperの設定を作成します。

  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.ttshrk.livewallpaper_example"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="7" />
    <uses-feature android:name="android.software.live_wallpaper" />

    <application
        android:label="@string/app_name"
        android:icon="@drawable/icon" android:debuggable="true">

        <service
            android:label="@string/app_name"
            android:name=".ExampleWallpaer"
            android:permission="android.permission.BIND_WALLPAPER">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper" />
        </service>
        
    </application>
</manifest>


ポイントは以下の通りです。

  • live_wallpaperの設定

uses-featureにandroid.software.live_wallpaperを設定します。

<uses-feature android:name="android.software.live_wallpaper" />
  • serviceの登録

ExampleWallpaerをサービスとして登録します。

  • wallpaperのxmlファイルを設定

壁紙の設定を記述したXMLファイルを、meta-data指定します。

<meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper" />
リソースファイル作成

壁紙の設定を記述したXMLファイルを作成します。
これはライブ壁紙の設定画面の一覧に表示されます。

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"  
	android:thumbnail="@drawable/icon" 
	android:description="@string/service_description" />


stringリソース設定。

  • res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Livewallpaper Example</string>
    <string name="service_description">壁紙の説明</string>
</resources>


以上です。
実行すると壁紙に時刻が表示されます。また、タッチしたポイントに表示が移動します。


サンプルコード
livewallpaper_example.zip 直

PhoneGapでSencha Touchを使っためも

PhoneGapでSencha Touchを使ったときに、PhoneGapのJavascriptブリッジを有効にする方法が少し特殊だったため、メモしておきます。あわせてSench(Ext JS)風味の書き方ものせておきます。

参考サイト

Sencha Touch
http://www.extjs.co.jp/
Sencha Touchドキュメント
http://www.extjs.co.jp/deploy/touch/docs/

Tutorial:A Sencha Touch MVC application with PhoneGap
http://www.sencha.com/learn/Tutorial:A_Sencha_Touch_MVC_application_with_PhoneGa

PhoneGap
http://www.phonegap.com/home/

概要

PhoneGapでは、onDevideReadyイベント後にJavascriptブリッジが有効になるため、以下のようにコーディングしていると思います。

  • body onloadに登録する例
<script type="text/javascript">
funciton onLoad() {
    document.addEventListener("deviceready", onDeviceReady, false);
}
function onDeviceReady() {
}
</script>
...

<body onload="onLoad();">
...
</body>


ただ、PhoneGapでSencha Touchを利用する場合、上記の方法ではうまくいかないようです。そのためここでは、PhoneGap.jpの初期化後にSencha Touchのアプリケーションクラスを初期化するにします。おおまかな手順は以下のようになります。

  • Sencha Touchのアプリケーションクラス(Ext.Application)の作成
  • onDevicereadyイベントでSencha Touchのアプリケーションクラスの初期化を行うコードを追加
  • Viewクラス(Ext.Panel)の作成
  • PhoneGapのJavascriptブリッジクラスを使ってみるクラスを追加

サンプルコード

Sencha Touchのアプリケーションクラス(Ext.Application)の作成

Sencha Touchのアプリケーションクラスを作成します。
view、controller、modelをサポートした、MVCアプリケーションとして利用できますが、ここではviewのみ設定します。

  • js/sample.js
/**
 * アプリケーションクラス登録
 */
Ext.regApplication({
    name: 'app',
    
    launch: function() {
        this.launched = true;
        this.mainLaunch();
    },
    mainLaunch: function() {
        console.log('start mainLaunch');
        if (!device || !this.launched) {return;}
        console.log('mainLaunch');

        // ページView生成。後の手順で宣言している「Ext.app.views.Viewport」です
        this.views.viewport = new this.views.Viewport();
    }
});

mainLaunchメソッドでdevice変数のチェックを行っていますが、スマートフォンのときのみ有効な変数です。デスクトップで動作の確認を行いたい場合は、この部分をコメントアウトすると動作します。

onDevicereadyイベントでSencha Touchのアプリケーションクラスの初期化を行うコードを追加

PhoneGapの初期化が終わってからSencha Touchのアプリケーションクラスを初期化します。

  • index.html
<!DOCTYPE html>
<html>
    <head>
        <title></title>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <link rel="stylesheet" type="text/css" href="css/sencha-touch.css">
        <script type="text/javascript" charset="utf-8" src="js/sencha.js"></script>
        <script type="text/javascript" charset="utf-8" src="js/phonegap.js"></script>
        <script type="text/javascript" charset="utf-8" src="js/sample.js"></script>
        <script type="text/javascript">
            document.addEventListener("deviceready", app.mainLaunch, false);
        </script>
    </head>
    <body></body>
</html>

onDevicereadyイベントにapp.mainLaunchを登録します。

Viewクラス(Ext.Panel)の作成

ヘッダとフッダのパネルのあるページです。

  • js/sample.js
/**
 * View
 */
app.views.Viewport = Ext.extend(Ext.Panel, {
	tabletStartupScreen: 'images/startup.png',
	phoneStartupScreen: 'images/startup.png',
	icon: 'icon.png',
	glossOnIcon: false,

	initComponent: function() {
		var me = this;
		
		var panel = new Ext.Panel({	// Ext.Panelクラスを拡張する
			fullscreen: true,
			cls: 'cards',
			componentCls: 'bodycls',	// cssのclassを指定できる
			centered: true,
			layout: {
				type: 'vbox',
				align: 'stretch'
			},
			defaults: {
				flex: 1
			},
			html: [
				'<p>hello sencha touch</p>',
				'<button onclick="vibrate();">vibrate</button>'
			],
			// ツールバーなどのdock item定義
			dockedItems: [
			    {
					dock : 'top',
					xtype: 'toolbar',	// Ext.Toolbarクラスを指定
					title: 'HELLO SENCHA TOUCH',
					cls  : 'topcls',
			    },
			    new Ext.Toolbar({	// Ext.Tollbarを直接生成してもよい
					dock : 'bottom',
					title: 'copylight hoge',
					cls  : 'bottomcls',
			    }),
			],
		});
		
		// パネル構築
		panel.doComponentLayout();
	}
});


以下の部分がツールバーに囲まれた部分に表示されます。

			html: [
				'<p>hello sencha touch</p>',
				'<button onclick="vibrate();">vibrate</button>'
			],
PhoneGapのJavascriptブリッジクラスを使ってみるクラスを追加

PhoneGap APIのnavigator.notification.vibrateを使ってみます。


js/sample.js

/**
 * 振動
 */
Ext.app.Vibrate = Ext.extend(Object, {
    name: 'vibrate',
    vibrateCount: 0,
    vibrateTime: 0,
	
    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);	// configのキーと値をインスタンスのフィールドとして設定する
    },

	play: function() {
		console.log("vibrate play");
		navigator.notification.vibrate(this.vibrateTime);
	}
});

/**
 * 振動呼び出し
 */
function vibrate() {
	var v = new Ext.app.Vibrate({'vibrateTime':200});
	v.play();
}

Ext.extendを使って、Objectの拡張クラスを作成します。Ext.applyを利用して、パラメータの設定を行います。
app.views.Viewportの以下の部分で呼び出しています。

<button onclick="vibrate();">vibrate</button>


以上です。起動すると、以下のように表示されると思います。


ContentProviderでアプリ内のファイルを公開するサンプル

ContentProviderを使って、asset内のファイルをGMailに添付ファイルとして渡すサンプルです。
アプリ内のassets配下のファイルをIntent経由でほかのアプリに渡したい場合も動作します。
当時、ContentProviderがよくわかっていないこともあり、プロジェクトのassets配下のファイルが添付できなくてかなり悩みましたが、ContentProviderのリファレンスにちゃんと書いてあった・・・。

概要

Intent経由でアプリからGMailを起動したときに、ContentProviderを使って添付ファイルを渡します。
実装するものは以下のとおりです。

  • AssetFileProvider

  コンテンツプロバイダ。
  アプリ内のAssetファイルを渡します。insert,update,deleteなどは実装しません。

  • FileProvider

  コンテンツプロバイダ。
  SDカードなどのファイルを渡します。insert,update,deleteなどは実装しません。

  • AndroidManifest.xmlの編集

  コンテントプロバイダをAndroidManifest.xmlに登録します。

  • その他リソース

  res/layout/main.xml
  res/values/strings.xml
  画像ファイル

  • CPSampleの作成

  メインActivityクラス、CPSampleにGmailを起動するコードを追加します。

実装

AssetFileProviderの作成

アプリ内のAssetファイルを返す、ContentProviderを実装します。
onCreate、getType、insert、update、deleteは使用しません。
ここではAssetファイルを返すにはopenAssetFileを実装します。

  • FileContentProvider.java
package com.ttshrk.provider;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

public class AssetFileProvider extends ContentProvider {

	public static final String AUTHORITY = "com.ttshrk.provider.AssetFile";
	public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
	public static final String TAG = "AssetFileProvider";

	@Override
	public boolean onCreate() {
		return false;
	}

	@Override
	public String getType(Uri uri) {
		return null;
	}

	@Override
	public AssetFileDescriptor openAssetFile(Uri uri, String mode)
			throws FileNotFoundException {
		Log.i(TAG, "open Asset file");
		try {
			return getContext().getAssets().openFd("sample_01.gif");
		} catch (IOException e) {
			e.printStackTrace();
			Log.e(TAG, "ERROR: " + e);
			throw new FileNotFoundException(e.getMessage());
		}
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		return null;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		return null;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		return 0;
	}
}
FileProviderの作成

SDカードなどのファイルを返す、ContentProviderを実装します。
onCreate、getType、insert、update、deleteは使用しません。
ここではopenFileを実装します。

  • FileProviderr.java
package com.ttshrk.provider;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URI;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

public class FileProvider extends ContentProvider {

	public static final String AUTHORITY = "com.ttshrk.provider.File";
	public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
	public static final String TAG = "FileProvider";

	@Override
	public boolean onCreate() {
		return false;
	}

	@Override
	public String getType(Uri uri) {
		return null;
	}

	@Override
	public ParcelFileDescriptor openFile(Uri uri, String mode)
			throws FileNotFoundException {
		Log.i(TAG, "open file");
		URI path = URI.create("file:///sdcard/sample_02.jpg");

		ParcelFileDescriptor parcel = ParcelFileDescriptor.open(new File(path),
				ParcelFileDescriptor.MODE_READ_ONLY);
		return parcel;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		return null;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		return null;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		return 0;
	}
}
AndroidManifest.xmlの編集

providerを追加します。

  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.ttshrk.cp_sample" android:versionCode="1"
	android:versionName="1.0">
	<uses-sdk android:minSdkVersion="3" />

	<application android:icon="@drawable/icon" android:label="@string/app_name">
		<activity android:name=".CPSample" android:label="@string/app_name">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
		<provider android:name="com.ttshrk.provider.FileProvider" android:authorities="com.ttshrk.provider.File" />
		<provider android:name="com.ttshrk.provider.AssetFileProvider" android:authorities="com.ttshrk.provider.AssetFile" />
	</application>

	<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

</manifest>
その他リソース
  • res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout1">
    <Button android:layout_height="wrap_content" android:layout_width="wrap_content" android:id="@+id/button1" android:text="@string/attach_mail"></Button>
    <Button android:text="@string/action_02" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
</LinearLayout>
<ImageView android:src="@drawable/icon" android:id="@+id/imageView1" android:layout_height="fill_parent" android:layout_width="fill_parent"></ImageView>
</LinearLayout>
  • res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">cp_sample</string>
    <string name="attach_mail">メール送信</string>
    <string name="action_02">View表示</string>
</resources>
  • 画像ファイル

以下のファイルを設置
assets/sample_01.gif
/sdcard/sample_02.jpg

CPSampleの作成

コンテントプロバイダのテストコードを書きます。
実装例は以下のとおりです。

    • Gmailに画像を添付する。
    • ImageViewに画像を渡す。
package com.ttshrk.cp_sample;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;

public class CPSample extends Activity {
	ImageView imageView;
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Button button1 = (Button) findViewById(R.id.button1);
		button1.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonClick1();
			}
		});
		
		Button button2 = (Button) findViewById(R.id.button2);
		button2.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonClick2();
			}
		});
		
		imageView = (ImageView)findViewById(R.id.imageView1);
	}

	/**
	 * GMailにAssetFileProvider経由でAssetのファイル(sample_01.gif)を添付
	 */
	private void buttonClick1() {
		Intent intent = new Intent(Intent.ACTION_SEND);

		// subject
		intent.putExtra(Intent.EXTRA_SUBJECT, "さぶじぇくと");
		// text
		intent.putExtra(Intent.EXTRA_TEXT, "テキスト");

		// attachment
		intent.setType("image/gif");
		Uri uri = Uri.parse("content://com.ttshrk.provider.AssetFile/sample_01.gif");
		intent.putExtra(Intent.EXTRA_STREAM, uri);
		
		startActivity(Intent.createChooser(intent, null));
	}

	/**
	 * imageViewにFileProvider経由でSDカードのファイル(sample_02.jpg)を表示
	 */
	private void buttonClick2() {
		imageView.setImageURI(Uri.parse("content://com.ttshrk.provider.File/"));
	}
}


以上で、実装は終了です。
起動すると、「メール送信」と「View表示」のボタンが表示されます。

  • メール送信

GMailにファイルを添付してを起動します。送信するとAssetのファイル(assets/sample_01.gif)が添付されます。
 uriを「content://com.ttshrk.provider.AssetFile/sample_01.gif」のように指定していますが、GMailに添付ファイル名を渡すため、sample_01.gifをつけています。上記の実装では、AssetFileProvider側で「sample_01.gif」の部分を無視しています。

  • View表示

→アプリ上に、SDカードから読み込んだファイル(/sdcard/sample_02.jpg)が表示されます。ContentProvider経由でImageViewを表示するサンプルとしてみてもらえれば・・・。


以上です・・・。


上記のサンプルコードです。(すこしコードが変わってしまっていますが・・・)
cp_sample.zip 直

参考サイト

Android content provider example
http://www.xinotes.org/notes/note/1294/

JavaMailでのメール送信まとめその4

JavaMailでのメール送信まとめその4。
AndroidアプリからJavamailでメール送信を行うサンプル。
Android端末専用のjarファイル(mail.jar、additionnal.jar、activation.jar)が必要ですが、MimeMessageの作成、送信手順は同じです。

また、Outbound Port 25 Blockingにより、25番ポートでメール送信が不可能なことがあるため、gmailのアカウントを使用して送信しています。

Android用JavaMailのダウンロード

android用のJavaMailを提供しているサイト「javamail-android」よりダウンロードします。
以下のjarファイルをダウンロードしてください。

http://code.google.com/p/javamail-android/

    • mail.jar
    • additionnal.jar
    • activation.jar

libsといったディレクトリを作成し、上記のjarファイルをコピーしてください。
その後、ビルドパスに追加すれば準備完了です。
(eclipseの場合は、jarファイルを右クリックした後、ビルドパスに追加を選択すればOKです。)

サンプル

gmailでメール送信

ネットワークアクセスが発生するため、パーミッションの設定が必要になります。

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
  • gmail経由での送信サンプルコード
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com"); // SMTPサーバ名
props.put("mail.host", "smtp.gmail.com");      // 接続するホスト名
props.put("mail.smtp.port", "587");       // SMTPサーバポート
props.put("mail.smtp.auth", "true");    // smtp auth
props.put("mail.smtp.starttls.enable", "true");	// STTLS

// セッション
Session session = Session.getDefaultInstance(props);
session.setDebug(true);

MimeMessage msg = new MimeMessage(session);
try {
	msg.setSubject("gmailでメール送信テスト(utf-8)", "utf-8");
	msg.setFrom(new InternetAddress("Fromアドレス"));
	msg.setSender(new InternetAddress("Senderアドレス"));
	msg.setRecipient(Message.RecipientType.TO, new InternetAddress("送信先アドレス"));
	msg.setText("gmail経由でgmail.com向けメール送信テスト",	"utf-8");

	Transport t = session.getTransport("smtp");
	t.connect("gmailアカウント XXXX@gmail.com", "gmailパスワード");
	t.sendMessage(msg, msg.getAllRecipients());
} catch (MessagingException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}
携帯メール、デコメを送信

iPhone(i.softbank.jp)向けに送信してみます。

package com.ttshrk.send_mail_from_app;

import java.util.Properties;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.NoSuchProviderException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.ttshrk.mbemoji.Emoji;
import com.ttshrk.mbemoji.EmojiUtility;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownServiceException;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;

public class SendMailFromApp extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Button button1 = (Button) findViewById(R.id.button1);
		button1.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonClick1();
			}
		});

		Button button2 = (Button) findViewById(R.id.button2);
		button2.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonClick2();
			}
		});
	}

	/**
	 * utf-8 softbank.ne.jp
	 */
	private void buttonClick1() {
		Session session = createSendGMailSession();
		MimeMessage msg = new MimeMessage(session);

		try {
			msg.setSubject("gmailで携帯向けメール送信テスト(utf-8)", "utf-8");
			msg.setFrom(new InternetAddress("from@example.com"));
			msg.setSender(new InternetAddress("from@example.com"));
			msg.setRecipient(Message.RecipientType.TO, new InternetAddress(
					"?????@i.softbank.jp"));
			msg.setDataHandler(new DataHandler(new TextBinaryDataSource(
					"gmail経由でi.softbank.jp向けメール送信テスト[:1025:][:0105:]"
							.getBytes(), "utf-8", "plain", Emoji.Mode.SOFTBANK)));

			sendGmail(msg, session);
		} catch (MessagingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * utf-8 i.softbank.jp でこめ
	 * multipart/related
	 *  ├ multipart/alternative
 	 *  │ ├ text/plain
 	 *  │ └ text/html
  	 *  └ image (インライン画像/添付画像)
	 */
	private void buttonClick2() {
		Session session = createSendGMailSession();
		MimeMessage msg = new MimeMessage(session);

		try {
			msg.setSubject("gmailでメール送信テスト(utf-8) でこめ", "utf-8");
			msg.setFrom(new InternetAddress("from@example.com"));
			msg.setSender(new InternetAddress("from@example.com"));
			msg.setRecipient(Message.RecipientType.TO, new InternetAddress(
					"?????@i.softbank.jp"));
			
			// related
			Multipart relatedPart = new MimeMultipart("related");
			
			// alternative
			MimeBodyPart alternativeBodyPart = new MimeBodyPart();
			Multipart alternativePart = new MimeMultipart("alternative");
			alternativeBodyPart.setContent(alternativePart);
			relatedPart.addBodyPart(alternativeBodyPart);

			// text mail
			MimeBodyPart textBodyPart = new MimeBodyPart();
			textBodyPart.setDataHandler(new DataHandler(new TextBinaryDataSource(
					"gmail経由でi.softbank.jp向けメール送信テスト[:1025:][:0105:]".getBytes(),
					"utf-8", "plain", Emoji.Mode.SOFTBANK)));
			textBodyPart.setHeader("Content-Transfer-Encoding", "base64");
			alternativePart.addBodyPart(textBodyPart);

			// html mail
			MimeBodyPart htmlBodyPart = new MimeBodyPart();
			htmlBodyPart.setDataHandler(new DataHandler(new TextBinaryDataSource(
					"<HTML><BODY>gmail経由でi.softbank.jp向けメール送信テスト[:1025:][:0105:]<IMG src=\"cid:12345@12345\"></BODY></HTML>".getBytes(),
					"utf-8", "html", Emoji.Mode.SOFTBANK)));
			htmlBodyPart.setHeader("Content-Transfer-Encoding", "base64");
			alternativePart.addBodyPart(htmlBodyPart);

			// inline image
			MimeBodyPart imageBodyPart = new MimeBodyPart();
			imageBodyPart.setDataHandler(new DataHandler(new FileDataSource("/sdcard/sample.gif")));
			imageBodyPart.setFileName(MimeUtility.encodeWord("sample.gif"));
			imageBodyPart.setDisposition("inline");	    // inline指定しておく
			imageBodyPart.setContentID("12345@12345");  // インライン画像を指定
			relatedPart.addBodyPart(imageBodyPart);
			
			// set related
			msg.setContent(relatedPart);
			
			sendGmail(msg, session);
		} catch (MessagingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 
	 * @return
	 */
	private Session createSendGMailSession() {
		Properties props = new Properties();
		props.put("mail.smtp.host", "smtp.gmail.com"); // SMTPサーバ名
		props.put("mail.host", "smtp.gmail.com"); // 接続するホスト名
		props.put("mail.smtp.port", "587"); // SMTPサーバポート
		props.put("mail.smtp.auth", "true"); // smtp auth
		props.put("mail.smtp.starttls.enable", "true"); // STTLS

		// セッション
		Session session = Session.getDefaultInstance(props);
		session.setDebug(true);
		return session;
	}

	/**
	 * 
	 * @param msg
	 * @param session
	 * @throws MessagingException
	 */
	private void sendGmail(MimeMessage msg, Session session)
			throws MessagingException {
		Transport t = session.getTransport("smtp");
		t.connect("gmailユーザアカウント", "gmailパスワード");
		t.sendMessage(msg, msg.getAllRecipients());
	}
}

/**
 * 特定のコードをバイナリ絵文字に変換するDataSource
 */
class TextBinaryDataSource implements DataSource {

	private byte[] text;
	private byte[] convertedText;
	private String charset;
	private String subType;
	private Emoji.Mode emojiMode;

	public TextBinaryDataSource(byte[] text, String charset, String subType,
			Emoji.Mode emojiMode) throws IOException {
		this.text = text;
		this.charset = charset;
		this.subType = subType;
		this.emojiMode = emojiMode;
		initialize();
	}

	public TextBinaryDataSource(String text, String charset, String subType,
			Emoji.Mode emojiMode) throws IOException {
		this(text.getBytes(), charset, subType, emojiMode);
	}

	public byte[] getPlainText() {
		return text;
	}

	private void initialize() throws IOException {
		InputStream dec_in = new ByteArrayInputStream(text);
		ByteArrayOutputStream bo = null;
		OutputStream os = null;

		try {
			bo = new ByteArrayOutputStream();
			os = EmojiUtility.decode(bo, charset, emojiMode);
			byte[] buf = new byte[2048];
			int len;

			while ((len = dec_in.read(buf)) != -1) {
				os.write(buf, 0, len);
			}
		} finally {
			if (os != null) {
				os.close();
			}
		}
		convertedText = bo.toByteArray();
	}

	@Override
	public InputStream getInputStream() throws IOException {
		InputStream is = new ByteArrayInputStream(convertedText);

		return is;
	}

	@Override
	public OutputStream getOutputStream() throws IOException {
		throw new UnknownServiceException();
	}

	@Override
	public String getContentType() {
		return "text/" + subType + "; charset=" + charset;
	}

	@Override
	public String getName() {
		return "";
	}

}

デコメの部分の添付ファイルはSDカードから取得するようにしました。
(assetsから読み込む場合、「new InputStreamReader(context.getAssets().open("sample.gif"))」のようにして取得するため、InputStreamを設定できるDataSourceが必要らしいので・・・)

  • サンプルコード

send_mail_from_app.zip 直


また、shift-jis、utf-8のコードで、ソフトバンク携帯アドレス(XXXX@softbank.ne.jp)、iPhone(i.softbank.jp)向けに送信してみたところ、どちらも送信はできましたが以下のような違いがありました。ただ、ちゃんと検証したわけではないので、目安程度にしてください。
(どちらのメールも、iPhone宛てに送信しました。)

  • softbank.ne.jp宛てのメールの場合、shift-jis、utf-8ともに絵文字を問題なく扱えました。メールクライアントによるのかもしれませんが・・・。
  • i.softbank.jp宛てのメールの場合、utf-8でないと(バイナリコードの)絵文字を正しく認識しませんでした。

phonegap-pluginsを使っためも

phonegap-pluginsを使っためも。
PhoneGap(Android版)のJavascript上からIntentを呼び出す必要があったので。。。

phonegapの動作環境を準備

http://www.phonegap.com/start#androidを参考に環境を構築。

phonegap-pluginを適用

phonegap-plugin ダウンロード

phonegap-pluginをダウンロードします。
https://github.com/borismus/phonegap-plugins


今回はIntentの機能を追加するため、ダウンロードしたファイルのうち、「phonegap-plugins / Android / WebIntent」を利用します。

pluginの追加

com.borismus.webintentというパッケージ名で、ダウンロードしたファイル「phonegap-plugins / Android / WebIntent/WebIntent.java」を追加します。
このWebIntent.javaがPhoneGapのプラグインの実装ファイルになります。
また、PhoneGapからプラグインを利用するとき、javascriptからクラス名を指定して読み込みを行うため、パッケージ名が異なるとエラーとなるのでご注意ください。

javascriptファイルの追加

「asset/www」あたりにダウンロードしたファイル「phonegap-plugins / Android / WebIntent/webintent.js」を追加します。
このwebintent.jsの中で、PhoneGapにプラグインの追加を行っています。

実行例

index.htmlで実行してみます。

  • asset/www/index.html
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      PhoneGap
    </title>
    <script type="text/javascript" charset="utf-8" src="phonegap.js"></script>
    <script type="text/javascript" charset="utf-8" src="webintent.js"></script>
    <script type="text/javascript" chareset="utf-8">
      
      // Wait for PhoneGap to load
      //
      function onLoad() {
        document.addEventListener("deviceready", onDeviceReady, false);
      }

      function onDeviceReady() {
        open_maps();
        //open_mail();
      }

	  function open_maps() {
	    var address = 'hogehoge';
		window.plugins.webintent.startActivity({
		    action: WebIntent.ACTION_VIEW,
		    url: 'geo:0,0?q=' + address}, 
		    function() {}, 
		    function() {alert('Failed to open URL via Android Intent')}
		);
	  }

	  function open_mail() {
  		  var extras = {};
		  extras[WebIntent.EXTRA_SUBJECT] = "サブジェクト";
		  extras[WebIntent.EXTRA_TEXT] = "テキスト";
		  window.plugins.webintent.startActivity({
		    action: WebIntent.ACTION_SEND,
		    type: 'text/plain',
		    extras: extras
		  }, function() {}, function() {alert('Failed to send email via Android Intent');});
	  }
    </script>
  </head>
  <body onload="onLoad()">

    <h2>
      plugin test
    </h2>
    
  </body>
</html>

アプリの起動後に、google mapsやメーラを起動できると思います。
注意点として、devicereadyイベントの後でないと、プラグインが有効にならないという点があります。


また、メーラ起動時に添付ファイルを指定する方法がないか試してみましたが、無理っぽい気がします。
プラグインを改造するのが手っ取り早いかも・・・。

mysql-5.5.9インストールめも

mysql-5.5.9のインストールめも。cmakeが使いづらかい。


mysql
http://dev.mysql.com/

cmake
http://www.cmake.org/

インストール手順

cmake install

依存ライブラリがいろいろと必要になるかも。
自分のときは
gcc-c++.x86_64
ncurses.x86_64
ncurses-devel.x86_64
が足りませんでした・・・。

  • cmake インストール
cd /usr/local/src
wget http://www.cmake.org/files/v2.8/cmake-2.8.4.tar.gz
tar xvfz cmake-2.8.4.tar.gz

cd cmake-2.8.4
./configure
make

su
make install
exit
mysql install
  • ユーザ、グループの追加
su -
groupadd mysql

useradd -g mysql -d /home/mysql mysql
exit
  • インストール

mysql downloadページからGAのバージョンをダウンロードしてください。

cd /usr/local/src
wget http://www.mysql.com/get/Downloads/MySQL-5.5/mysql-5.5.9.tar.gz/from/http://ftp.jaist.ac.jp/pub/mysql/
tar xvfz mysql-5.5.9.tar.gz

cd mysql-5.5.9
su
# cmake
cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/mysql -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci -DWITH_EXTRA_CHARSETS=all -DWITH_PIC=1

# make
make

# install
make install

# ユーザ、グループ設定
cd /usr/local/mysql
chown -R mysql:mysql /usr/local/mysql
    • cmakeのときに以下のオプションで構築していましたが、起動時にエラーが発生したため、上記のオプションに変更しました。
cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/mysql -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci
    • cmakeをやり直すときは、以下の操作が必要です。
rm CMakeCache.txt
make clean


rootユーザのまま作業

  • DB初期化、設定
# DB初期化
cd /usr/local/mysql
./scripts/mysql_install_db --basedir=/usr/local/mysql --user=mysql

# 設定ファイル(my.cnf)
cp -p /usr/local/mysql/support-files/my-medium.cnf /usr/local/mysql/data/my.cnf

mysql_install_db はスクリプトを実行したディレクトリ配下のdataディレクトリにmysql権限テーブルのインストールを行うようなので、「/usr/local/mysql/scripts」で作業を行わないように注意してください。

  • 起動設定
# 起動ファイル
cp -p /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql

# サービス登録
chkconfig --add mysql

# 起動
/etc/init.d/mysql start

# 接続確認
/usr/local/mysql/bin/mysql -u root
mysql> quit
  • rootユーザ設定
# rootユーザのパスワード設定
mysqladmin -u root password [パスワード]

# 匿名ユーザ削除
/usr/local/mysql/bin/mysql -u root -p
mysql> delete from mysql.user where password = '';
mysql> flush privileges
作業ユーザ追加
# PATH設定
cat >> ~/.bashrc
MYSQL=/usr/local/mysql
export PATH=$PATH:$MYSQL/bin
export MANPATH="$MANPATH":$MYSQL/man
# ctrl+D

source ~/.bashrc

# DB追加
mysql -u root -p
mysql> create database testdb; 

# ユーザ追加
mysql>  create user 'testuser'@'%' identified by 'testpassword';
mysql> grant all on testdb.* to testuser@'%';
mysql> quit

# 接続確認
mysql -u testuser -p 


以上です。