40hくらいでデジタル時計ウィジェットを作った話。


追記(2012/1/13):
ウィジェットをダウンロードできるようにしました。
詳細は群衆三世のアプリサイト amemilitia.com をご覧ください。


アンドロイドで動作するデジタル時計ウィジェット(AppWidget)を作りました。
10月26日頃から作り始めて、それなりに動くまでに40時間くらいかかりました。
レイアウトの微調整をしたり、
あれこれAndroidの仕様を確認してたので、
最終的に60時間くらいかかってると思います・・。
 
この程度であれば、
仕様が分かってる人なら4時間くらいで作っちゃうんじゃないですかね。

事情

標準のデジタル時計ウィジェットを使ってたんですが、
UTC(世界協定時)が表示されないのが不便だったんです。
 
俺、こっそりLang-8で英語日記をつけてるんですよ。
たまにメンテナンスで日記が書けなくなるタイミングがあって。
その復旧予定時刻がUTCでアナウンスされるんですね。
そこで「いったい今はUTCで何時なのよ」と思うわけです。
 
加えて、Facebookにログインしている時の話。
全然知らない外国人からフレンド登録されたり
チャットしたりすることがあるんです。
話すときに相手の地域が何時なのかを知りたかったりね。
「そっちは昼かー。こっちは深夜で、もう寝るからまたね」
みたいな。
 
だからUTCが常に表示されていてくれるとうれしいのです。

ついでに

バッテリー状況をパーセントで表示できたらうれしいな!と。
一応表示できるウィジェットはたくさんあるけど、
電池のグラフィック使ってたり、
バッテリーの温度とか表示してたり、
無駄に豪華なので邪魔。
 
その上、バッテリーの状況を表示するだけなのに
「完全なインターネットアクセス」
って権限を要望するのはなんなのこわい。
 

参考にしたサイト

最近のアプリ開発は、たいがいネットにサンプルソースが転がってるので、
とても楽ちんですね。


http://www.techfirm.co.jp/lab/android/widget.html
ウィジェットの作り方


http://www.atmarkit.co.jp/fsmart/articles/android10/android10_3.html
Androidのホーム画面に常駐するアプリを作るには


http://www.adakoda.com/android/000140.html
↑バッテリーの情報(Battery information)を取得するには


http://www.adakoda.com/android/000071.html
↑テキストビュー(TextView)を使用するには


http://android-er.blogspot.com/2010/10/update-widget-in-onreceive-method.html
↑Update Widget in onReceive() method


http://side2.jp/2011/04/textview-dropshadow/
↑[TIPS]文字にドロップシャドウを付ける方法


http://boco.hp3200.com/beginner/widget02-4.html
↑バッテリー残量ウィジェットの作り方 (4/4)


以下コード。

package com.amemilitia.KairosForce;

import java.util.Date;
import java.util.TimeZone;
import java.util.regex.*;

import com.amemilitia.KairosForce.R;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.IBinder;
import android.util.Log;
import android.widget.RemoteViews;


public class KairosForceProvider extends AppWidgetProvider {
	private static final String ACTION_KAIROS_INTERVAL = "com.amemilitia.KairosForce.INTERVAL";
	private static final long INTERVAL = 60 * 1000;
	
	//----------------------------------------------------------
	private PendingIntent getAlermPendingIntent(Context context){
		Intent alarmIntent = new Intent(context, KairosForceProvider.class);

		alarmIntent.setAction(ACTION_KAIROS_INTERVAL);
		
		return PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
	}

	//----------------------------------------------------------
	private void setInterval(Context context, long interval) {
		PendingIntent operation = getAlermPendingIntent(context);

		AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);

		long now          = System.currentTimeMillis();
		long oneHourAfter = ((long)(now / interval)) * interval + interval;


		am.set(AlarmManager.RTC, oneHourAfter, operation);

	}

	private void updateClock(Context context){
		ComponentName	cn = new ComponentName(context, KairosForceProvider.class);
		RemoteViews		rv = new RemoteViews(context.getPackageName(), R.layout.main);
		
		long			now = System.currentTimeMillis();
		Date			dt = new Date(now);
		
		
		// 表示の更新
		java.text.DateFormat fmt;
		
		//*****************************************
		//
		// ローカル時間の表示
		//
		//*****************************************
		fmt = android.text.format.DateFormat.getTimeFormat(context);
		
		
		Pattern pt = Pattern.compile("^(\\D*)(\\d+\\:\\d+)(\\D*)$");
		Matcher match = pt.matcher(fmt.format(dt));
		
		String prefix	= "";
	 	String suffix	= "";
		String TimeText	= "";

		if(match.find()){
		 	prefix		= match.group(1).trim();
			TimeText	= match.group(2).trim();
		 	suffix		= match.group(3).trim();
		}
		
		rv.setTextViewText(R.id.PrefixText, prefix);
		rv.setTextViewText(R.id.TimeText,   TimeText);
		rv.setTextViewText(R.id.SuffixText, suffix);
		
		//*****************************************
		//
		// ローカル日付の表示
		//
		//*****************************************
		fmt = android.text.format.DateFormat.getLongDateFormat(context);
		rv.setTextViewText(R.id.DateText, fmt.format(dt));

		//*****************************************
		//
		// UTC日付の取得
		//
		//*****************************************
		fmt = android.text.format.DateFormat.getLongDateFormat(context);
		fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
		rv.setTextViewText(R.id.UTCDateText, fmt.format(dt));

		//*****************************************
		//
		// UTC時間の取得
		//
		//*****************************************		
		fmt = android.text.format.DateFormat.getTimeFormat(context);
		fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
		rv.setTextViewText(R.id.UTCTimeText, "UTC / " + fmt.format(dt));

	 	// インターバルの設定
		setInterval(context, INTERVAL);

		AppWidgetManager.getInstance(context).updateAppWidget(cn, rv);
		
		return;
	}

	@Override
	public void onEnabled(Context context){
		super.onEnabled(context);

		Log.i("onEnable", "DONE.");
		return;
	}

	@Override
	public void onUpdate(Context context, AppWidgetManager manager, int ids []){
		super.onUpdate(context, manager, ids);

		// バッテリー情報更新のサービスを開始する
		Intent intent = new Intent(context, WidgetService.class);
		
		if(context.startService(intent) == null){
			Log.e("onUpdate", "サービスが登録できなかった!");
		}

		//時計の更新処理
		//updateClock(context);
		setInterval(context, 100);
		Log.i("onUpdate", "DONE.");
				
		return;
	}
	//----------------------------------------------------------
	public static class WidgetService extends Service {
		@Override
		public void onStart(Intent in, int si) {
			Log.i("onStart", "Begin WidgetService!");
			
			IntentFilter filter = new IntentFilter();

			filter.addAction(Intent.ACTION_BATTERY_CHANGED);
			registerReceiver(mBroadcastReceiver, filter);
		}

		@Override
		public IBinder onBind(Intent in) {
			return null;
		}
		
		@Override
		public void onDestroy(){
			unregisterReceiver(mBroadcastReceiver);
			Log.i("onDestroy", "Stop WidgetService!");
			return;
		}
	}
	//----------------------------------------------------------
	private static BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			String action = intent.getAction();
			if (action.equals(Intent.ACTION_BATTERY_CHANGED) == false) {
				Log.i("mBroadcastReceiver", action);
				return;
			}

			ComponentName	cn = new ComponentName(context, KairosForceProvider.class);
			RemoteViews		rv = new RemoteViews(context.getPackageName(), R.layout.main);
			
			int status = intent.getIntExtra("status", 0);
			//int plugged = intent.getIntExtra("plugged", 0);
			int level = intent.getIntExtra("level", 0);

			switch (status) {
			case BatteryManager.BATTERY_STATUS_CHARGING:
				rv.setImageViewResource(R.id.IconView, R.drawable.plug);
			    break;
			default:
				rv.setImageViewResource(R.id.IconView, R.drawable.battery4);
				break;
			}
		
			rv.setTextViewText(R.id.BatteryText, level + "%" );
			AppWidgetManager.getInstance(context).updateAppWidget(cn, rv);
		}
		
	};
	
	
	@Override
	public void onReceive(Context context, Intent intent){

		if(ACTION_KAIROS_INTERVAL.equals(intent.getAction())){
			updateClock(context);
			Log.i("onReceive", "ACTION_KAIROS_INTERVAL.");
		}

		super.onReceive(context, intent);
		return;
	}
	
	@Override
	public void onDeleted(Context context, int[] ids){

		super.onDeleted(context, ids);
		
		Log.i("onDeleted", "DONE.");
		
		return;
	}
	
	@Override
	public void onDisabled(Context context){

		// バッテリー情報更新のサービスを停止する
		Intent intent = new Intent(context, WidgetService.class);
		context.stopService(intent);
		//context.unregisterReceiver(mBroadcastReceiver);

		// 時計更新のタイマーを停止する
		PendingIntent operation = getAlermPendingIntent(context);

		AlarmManager	am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);

		am.cancel(operation);
		
		Log.i("onDisabled", "DONE.");

		super.onDisabled(context);
		return;
	}
}

アプリ名

今、シュタゲ脳になってるのであえて厨二風な名前にしてみました。
時間の神様カイロスと、電力表示するので「力」って意味でフォースをつけて、くっつけました・・。