2012年1月14日土曜日

ググれカス plug-in for twicca 1.1 リリースしました

ググれカス plug-in for twicca の新バージョン1.1をリリースしました。今回のバージョンでは、以下を修正しています。


- 英語リソース追加
- 日本語単語抽出ロジック変更
- バグ修正

思いつきの小ネタだったので、日本語での利用しか想定していませんでしたが、非日本語圏でも使えそうなのでとりあえず英語リソースと英語での紹介文を追加してみました。その他の言語については把握しきれないので放置の方向で。ちなみに、英語向けの紹介文はこんな感じ。英語ユーザーじゃないのでこういう表現が妥当かよくわからないけど。



----
ggrks plug-in enables to search the words in tweet .
This plug-in is for twicca.
ggrks means "You should search the word in google before ask other. fuckin' novice!".
----


あと、他の修正は微調整ということで。
その他としては、「ggrks」、「ググれカス」あたりでも検索に引っかかるように紹介文を変更してみたり。

もちろん私はググれカスだなんて思ったことはないですよ。
あくまでネタですから。

2012年1月9日月曜日

ググれカス plug-in for twicca 公開

新年明けましておめでとうございます。正月はTVを全く観ていないせいか、正月気分には浸れませんでしたが、表題の件、公開しました。

twiccaで表示されているツイートの内容から単語を抽出し、ググるためのプラグインです。似たようなモノはありそうですが、まぁ、いいでしょう。
TL上のネットジャンキーのスラングがわけわかめー!という状況で使えるかもしれません。でも、わけわかめなスラングが単語として抽出できるかはわかりません。

◆使い方
わけわかめなツイートを選択して、メニュー上から「ググれカス」を選択。

単語抽出されるまでしばし待つ。抽出されたら検索したい単語を選択する。

ググる。
以上。

◆仕組み
1. twiccaからTweetの文字列を受け取る。
2. 形態素解析とキーフレーズ抽出により単語抽出を行う。
3. 抽出した単語を一覧表示。
4. 選択された単語をググる。
単純ですね。

難点は2.をWebサービスを利用して行っているので、
・数秒待たされる。なんて致命的。
・特に乱れた日本語の場合、思ったとおりに単語抽出してくれないことがある。なぁんて致命的。
って感じです。

ま、ネタなんで許してやってください。

そういうわけで、本年も小ネタで行きますが、よろしくお願いいたします。

2011年12月31日土曜日

2年ちょっとを振り返って

2009/10/17にver.1.0.0をリリースして以来、2年ちょっとの期間で94回のリリースを行ったSmartTraining。ver.6.8.1。たぶん、今年はこれが最後のリリースになると思うので、まとめてこれまでのことを振り返ってみます。去年は振り返ってなかったので今回2年分まとめて。

ver.1.0.0(2009/10/17)のスクリーンキャプチャ。
トップ画面(ver.1.0.0)

計測画面(ver.1.0.0)
設定画面(ver.1.0.0)
とにかく、「入門書読みながら初めて作りました」的な雰囲気が満載ですね。元々、Androidで動く万歩計が欲しかったのと、GPSデータの取り扱いの練習用に作ってみようということで作り始めたアプリでした。最初はServiceの使い方も手探り(つい最近のバージョンでようやく正しい使い方を知ったのですが)で、画面を閉じると計測も止まってしまう作りでした。ボタンや画面テーマも標準のものをそのまま使った貧相なもの。アイコンの作り方を知らなかったので、画面上に画像リソースは皆無。ただ、画面は殺風景ですが、基本的な画面遷移や構成は今と変わっていません。MapViewによる地図表示とGoogle Calendarにデータを保存するという機能が最初からありました。
確か、最初に作ろうと思ってから初期バージョンをリリースするまで3ヶ月ほどかかったと記憶しています。たぶん、今これと同じ機能を作ったら1日あれば余裕で作れます。というか、コードを書くことより、どういう画面遷移にしたら使いやすいか?、どういう機能があったら楽しいか?という点を考えることに時間を多く割いていました。あと、たった一個のアプリケーションアイコンがどうしても作れなくて結局嫁さんに作ってもらいました。
江川さんの「Google Androidプログラミング入門」片手にひたすら真似するところからスタート。この本には本当にお世話になりました。

そしてVer.6.8.1(2011/12/30)のスクリーンキャプチャ。
トップ画面(ver.6.8.1)

計測画面(ver6.8.1)

設定画面(ver.6.8.1)

ラリー画面(ver6.8.1)
だいぶ賑やかになりました。アプリを作っていて途中で気づいたのですが、GPS万歩計アプリはいろいろな技術要素の練習台としてはとても良い題材だったと思います。
・位置情報やMapViewの取り扱い
・方位センサーと連動して回転する地図
・Serviceによるバックグラウンド処理
・タイマー、AlarmManagerの利用
・Notificationによる音声・振動・画面通知
・AccountManagerやGoogle Data APIs(Calendar、Fusion tables、Docs)との連携
・AWSとの連携
・他アプリ(特にTwitter)との連携
・Felica連携(串かつ経由)
・Mixi voiceやFacebookなどのSNSとの連携
・音声合成サービスとの連携
・加速度、方位センサーの利用
・ANT+対応の外部センサーの利用
・カスタムViewによるグラフ描画
・QuickAction
・LibAndroTranslationによる翻訳サービス
・画面テーマ
・有料アプリ
・広告

本質的に不要なものもあるかもしれませんが、自分としてはそんなことはどうでもよくて、全てのモチベーションは「その機能を使ってみたいから取り込んでみる」でした。結果、全体的にちぐはぐになったと思います。行き当たりばったりで追加した機能ばかりなので当然です。練習用に作ったアプリなのでそれで問題ありません。

中でもラリー機能は、特に思い出深い機能です。子どもがもうすぐ産まれる、というタイミングでこの機能を思いついて、ラリーのコースデータを作っている途中で産まれました。車で1時間以上かかる病院に毎日のように通ってから帰ってきてからデータを夜中まで作る、という繰り返しでした。眠い中ひたすらGoogle Earthの画面上をクリックしてコースデータをチマチマ作っていて肘が痛かったことを覚えています。

たぶん、アプリを作って公開されている方は皆さん感じていると思いますが、ユーザさんからのフィードバックが最も大きな収穫物だったと思います。時にはけちょんけちょんに叩かれることもありますが、現実世界でそこまでダメ出しされる機会というのはそうそうありません。ダメなところは時間をかけて真摯に改善していくことで必ず良い評価を得られると感じています。

それと、いったん世間に公開してユーザさんがつくと、そのアプリは開発者個人のものだけではなく、ユーザさんのもの、という扱いをされることも認識しました。ユーザが望まないことは開発者といえども自由にはならないのだと。

私の場合、Androidアプリを作成する作業に割り当てることのできる時間は、仕事を終えて、家に帰ってきて、家の事柄全てを終えた後のほんの僅かな自由時間だけです。たぶん1日平均30分もないと思います。本職ではあまりコードを書く機会はなく、見積もり、営業への同行、PLなどが主な仕事です。コードを書いていないとだんだんと見積もりの精度や技術的な難易度に対する勘が鈍くなり、自信を持って捌けなくなる場面が多いと感じています。アプリを作り始めてから、少なくともAndroid関連の案件であれば(判断が正しいかどうかはともかく)自信を持って捌けるようになったと思います。

まだまだやりたい事がたくさん溜まっています。現在のところ30項目ほどあります。十分時間が取れないのでなかなか消化できません。それどころか、どんどん増えていきます。

2011年の1年間に限っていうと、年間の目標としては、
1.ダウンロード数:25万
2.女子ウケ
3.日本以外でのユーザを増やす
でした。
1.については目標まであと8000というところまで来ましたが、残念ながら達成できそうにありません。2.はピンクの画面を作ってみましたということで頑張ってみました。達成できたかどうかは指標がないのでわかりませんが、某電話会社の女性向けスマホキャンペーン用パンフレットに何度も掲載していただいたので、広告制作側の方からは一応認識していただいたのではないかと勝手に思っています。3.については完全に失敗でした。たぶん、海外には日本国内よりも優れたサービスがあるからだろうと思っています。

2012年の目標としては、
・データのクラウドへの移行
です。単に自分がやった事が無いからやってみたいと、それだけのことです。実現したらかなり便利になると思いますが、そんなことはどうでもいいです。

ところで、元々このアプリは自分で使うために作り始めたのですが、実はもう自分はデバッグ以外では使っていません。元々陸上部で短距離をやっていたので長距離は性に合わないのです。

そんなわけで、今後もチマチマ作り続けていくつもりです。飽きたら他のネタやります。


2011年12月28日水曜日

AccountManagerから取得したauthtokenを使用するとFusion Tablesへのアップロードに失敗する件

ユーザの認証情報をAccountManagerに変更した時から、時々「Fusion Tablesへのアップロードに失敗する」という報告を受けるようになりました。
手元にある端末で確認してみると、4台中1台だけがどうしてもうまくアップロードできません。エラーはcreate table時に401。
その後、原因がわからないまま1ヶ月程度過ぎた頃、またアップロードを試してみると、先日うまくアップロード出来ていた他の端末もことごとくエラーcreate table時に401が出ていることを確認。

なんだろうねぇ。何気にと思いググって情報がみたら見つかりました。
http://d.hatena.ne.jp/yellow_73/20110819
まさにこれ。この問題です。

端末に保存されているtokenの賞味期限が切れているっぽい。
401が発生したらAccountManager#invalidateAuthToken()を呼んでauthtokenを破棄した上でもう一回最初から認証のやり直しをすることでうまくアップロード出来るようになりました。

Google DocsやGoogle Calendarにも似たような仕組みで認証かけてデータアップロードしているのに、こちらではそんな状況に陥ったことがない。なんでだろう?Fusion Tablesはtokenの賞味期限が他サービスよりもかなり短くせっていされているのか?と思いました。実際のところわかりませんが。

AccountManagerのgetAuthToken()をReflectionでヤッてみた」で書いたとおり、私のアプリは1.6もサポートしているので、AccountManagerを普通に使うことができない。というわけで、これまたReflectionの登場。前回の記事読めばReflectionのパターンはわかりますよね。ということで省略。


2011年11月27日日曜日

地味だけど重要な変更

久々にSmartTrainingのバージョンアップ(6.6.0)を行いました。
今回のバージョンでは、地味だけど重要な変更を加えています。
----
◇ Ver.6.6.0 変更内容

・使用するパーミッションとライブラリの変更。
・Android 2.1以降でサービスをフォアグラウンドに設定
・Android 2.1以降で端末のGoogleアカウントと連動
・Android 2.1以降で過去に端末内に保存したGoogleパスワードの削除

----

・使用するパーミッションとライブラリの変更。
最近、Androidに対する情報セキュリティがいろいろとうるさくなってきました。アプリケーションが持つパーミッションに対する意識も高くなり、ただの万歩計なのになんでそんなパーミッションが必要なの?とコメントをいただくこともでてきました。このアプリケーションは元々私自身がAndroidアプリ作成と公開を通じて様々な知見を得ることが目的で作り始めたものでした。そのため、通常の利用者が必要としないいくつかの機能を含んでいます。ここのところ実験的に取り入れた機能のために与えたパーミッションが増えてきたので、一度整理してみました。合わせて、使用しているパーミッションとその使用目的をAndroid Market上に公開しました。
----
このアプリケーションが使用する各パーミッションが必要な理由は以下の通りです。
・データをGoogleサービスにアップロードする際に端末内のGoogleアカウント設定を使用するために必要としています。
android.permission.USE_CREDENTIALS
android.permission.GET_ACCOUNTS
android.permission.AUTHENTICATE_ACCOUNTS
・画面テーマ変更時に自アプリケーションのリセットのために必要としています。
android.permission.KILL_BACKGROUND_PROCESSES
・GPSによる位置情報を取得するために必要としています。
android.permission.ACCESS_FINE_LOCATION
・広告モジュールが動作するために必要としています。
android.permission.ACCESS_NETWORK_STATE
・データの送信および、広告モジュールが動作するために必要としています。
android.permission.INTERNET
・定期通知のために必要としています。
android.permission.VIBRATE
・画面OFF時にCPUがスリープしないようにするために必要としています。
android.permission.WAKE_LOCK
・バックアップデータをSDカードに出力するために必要としています。
android.permission.WRITE_EXTERNAL_STORAGE
・ANTデバイスを制御するために必要としています。ANTデバイスをサポートする端末でのみ有効です。
com.dsi.ant.permission.ANT_ADMIN
com.dsi.ant.permission.ANT
----



・Android 2.1以降でサービスをフォアグラウンドに設定
これまで、計測中に動作するサービスはService.setForeground()を使用していました。これって、APILevel4までしか有効じゃなかったんです。Level5以降はService.startForeground()なるものが用意されていましたが、Reflectionを使わないと1apkで複数のバージョンに対応できなかったので、面倒臭がって対応していませんでした。今回、Reflectionを使ってService.startForeground()に対応しました。やり方はここに書いてある通り。これで、画面がバックグラウンドにまわったら落ちやすい、とかいう現象が解消されるのではないかと期待しています。期待しているだけで、絶対大丈夫とは言いませんし、自分の場合、落ちやすい現象を再現できないので気休めだと思っています。

・Android 2.1以降で端末のGoogleアカウントと連動
APILevel5以降で使用可能になったAccountManager対応をしました。AccountManagerを利用することで、ユーザは以下のメリットを受けることが出来ます。
・アカウントとパスワードをこのアプリケーションのためだけに再入力が不要になる。
・このアプリケーションがアカウントとパスワード情報をどこかに送信しているのではないかという不安を解消できる。

ちなみにLevel4以前ではAccountManagerが利用できないので既存の動作と同じです。両方のバージョンに対応するのは面倒なので、早くLevel4以前の端末には爆発して欲しいです。っていうかもう対応しちゃったんでどうでもいいです。

・Android 2.1以降で過去に端末内に保存したGoogleパスワードの削除
もうこのアプリケーションでは保存する必要がなくなったので自動的に消します。



あと、どこにも記載していませんが、こっそりminSDKVersionを3から4に変更しました。さようならLevel3。たぶん、影響を受けるユーザのほとんどはHTC myTouch3Gかと思われます。でも、大丈夫。最近のAndroid MarketはマルチAPK機能をサポートしているのでLevel3の端末からでも旧バージョンのSmartTrainingはインストールできるはずです。マルチAPKマンセー。

2011年11月20日日曜日

AccountManagerのgetAuthToken()をReflectionでヤッてみた

AccountManager#getAuthToken()を利用することでAndroid端末に登録したアカウント情報のTokenが取得できます。普通の使い方はこちらあたりに詳しく載っています。

これを利用することで例えばGoogle Serviceへのアクセス時認証に利用できます。取得したTokenをClientLoginの際のAuthに設定してやればOKです。



と、普通はこれで話が済んでしまうのですが、AccountManagerはAPI Level 5以降じゃないと利用できない。これより古い環境でこのコードを実行するとjava.lang.VerifyErrorが起きてしまいます。



これは後方互換性の問題です。Androidアプリを開発する際に気をつけなければいけないのがこの問題。詳しい対処方法はこちらに掲載されています。これらの対処方法のうち、今回はReflectionを利用することで回避してみました。当然のことながら、Reflectionを使ったからといって古い環境でAccountManagerの機能が使えるようになるわけではありません。実行時のVerifyErrorが回避されるだけです。同等のことを古い環境でやる場合は、Google Data APIのClientLoginなりをHTTPリクエスト送ってやればOKです。Java(Android)向けのライブラリも公開されているのでそれを使っても良いと思います。まぁそれはそれで置いといて。



今回主題のやり方はこちら。

public class AuthenticatorAfterForoyo extends AuthenticatorBase {

private static final String ACCOUNT_TYPE_GOOGLE = "com.google";

/**
* コンストラクタ
* @param context
*/
public AuthenticatorAfterForoyo(Context context)
{
super(context);
}

/**
* トークンの取得
* @param account
* @param passwd 不使用
* @param service
*/
public void getAuthToken(String acc, String passwd, String service)
{
try {
// AccountManagerクラス取得
Class accountManager = Class.forName("android.accounts.AccountManager");

// Accountクラス取得
Class account = Class.forName("android.accounts.Account");

// AccountManagerCallbackのクラス取得
Class accountManagerCallback = Class.forName("android.accounts.AccountManagerCallback");

InvocationHandler handler = new MyInvocationHandler();

@SuppressWarnings("rawtypes")
Class[] proxyInterfaces = new Class[] { accountManagerCallback };
Object accountManagerCallbackInstance = Proxy.newProxyInstance(
accountManagerCallback.getClassLoader(),
proxyInterfaces,
handler);

if ( accountManagerCallbackInstance == null )
{
Log.w(Define.TAG, "accountManagerCallbackInstance is null");
return;
}

// getメソッド
Method get = accountManager.getMethod("get", Context.class);
if ( get == null )
{
Log.w(Define.TAG, "get is null");
return;
}
// AccountManagerクラスインスタンス取得
Object accountManagerInstance = (Object)get.invoke(accountManager, context);
if ( accountManagerInstance == null )
{
Log.w(Define.TAG, "accountManagerInstance is null");
return;
}

// AccountManager.getAuthToken()メソッド
Method getAuthToken = null;
Method[] accountManagerMethods = accountManager.getMethods();
for ( int i = 0; i < accountManagerMethods.length; i++ )
{
Method method = accountManagerMethods[i];
if ( method.getName().equals("getAuthToken") )
{
// 引数の数とタイプが一致する関数を取得
// TODO もうちょっとスマートな解決方法はないものか?
// APILevel4以前の環境では引数の型特定にAccountだのAccountManagerCallbackだのを利用できないため、
// こんな無様な方法で関数を取得している。
Class[] types = method.getParameterTypes();
if ( types.length != 6 )
break;
if ( !types[0].getName().equals("android.accounts.Account") )
break;
if ( !types[1].getName().equals("java.lang.String") )
break;
if ( !types[2].getName().equals("android.os.Bundle") )
break;
if ( !types[3].getName().equals("android.app.Activity") )
break;
if ( !types[4].getName().equals("android.accounts.AccountManagerCallback") )
break;
if ( !types[5].getName().equals("android.os.Handler") )
break;
//Log.d("TAG", "Found getAuthToken() method");
getAuthToken = method;
}
}
if ( getAuthToken == null )
{
Log.w(Define.TAG, "getAuthToken is null");
return;
}

// AccountManager.getAccountsByType()メソッド取得
Method getAccountsByType = accountManager.getMethod("getAccountsByType", String.class);
if ( getAccountsByType == null )
{
Log.w(Define.TAG, "getAccountsByType is null");
return;
}

// Accountクラスインスタンス取得
Object[] accountObject = (Object[])getAccountsByType.invoke(accountManagerInstance, ACCOUNT_TYPE_GOOGLE);

java.lang.reflect.Field name = account.getField("name");
if ( name == null )
{
Log.w(Define.TAG, "name is null");
return;
}

for ( Object ac : accountObject )
{
if ( name.get(ac).toString().equals(acc) )
{
//Log.d(Define.TAG, "getAuthToken.invoke() name : " + name.get(ac).toString());
getAuthToken.invoke(
accountManagerInstance,
ac,
service,
null,
context,
accountManagerCallbackInstance,
null);
return;
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}

class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
//Log.d(Define.TAG, "MyInvocationHandler#invoke() method : " + method.getName() + ", args.length : " + args.length);
if ( method.getName().equals("run") && args.length == 1)
{
Class accountManagerFeature = Class.forName("android.accounts.AccountManagerFuture");
Method getResult = accountManagerFeature.getMethod("getResult", null);
if ( getResult == null )
return null;
Bundle bundle = (Bundle)getResult.invoke(args[0], null);
String token = bundle.getString("authtoken");
listener.onGotToken(token);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}


InvocationHandlerとかProxy.newProxyInstance()とか初めて使ったでござる。
こんなのあったら何でもアリじゃねぇ?

いじょ。

2011年10月4日火曜日

AppLog対応しました

2011/10/10 日本Androidの会10月定例会での発表内容を見た後で追記。

非常に残念な発表内容でした。
Androidの可能性そのものが潰されている気持ちでいっぱいです。
下記の内容を撤回します。

----
SmartTrainingでAppLog対応しました。
Twitterでもいろんな意味で盛り上がって(炎上?)いたり。 AppLogを検出するツールがでたり。 
実際使ってみなきゃ意見を言ってもねぇ。ってことで、使ってみての感想です。 

◆機能面


  • ログ送信確認画面の表示タイミングが不定。
    • サービス開始時に確認画面が表示されるわけではない。

  • どのアプリにAppLogが組み込まれているかわからない。 
    • ユーザとのインターフェイスとして送信確認画面しかないので、このタイミングでどのアプリ経由でAppLogが動いているのかがわからない。
  • 一度許可したら後で拒否するための方法がある。 
    • Marketから「AppLog Opt-in」をインストールして拒否設定に変更できる。後付けで作ったのでこういう状態。いずれSDKに組み込んだほうが良さそう。

◆それぞれのメリット・デメリット

  • 開発者
    • メリット
      • 月額報酬 or 収集したログのいずれかが貰える。まだ導入して間も無いので何ももらえてないけど、ログを使って新しい視点で今後の開発に活用していければいいな、と。これまでこういうサービスはなかったので貴重だと思う。(似たような情報はGoogleも収集しているはずだけど、Android端末に関する情報は提供されていないはず)ただ、実際何にどう使えるかは未知(データを見てないので)。
    • デメリット
      • ちゃんとユーザに説明しないと拒否反応に合う。
      • 当初、Marketの更新情報に「微調整」と書いたことで不評を買ってしまいました。まぁ、ユーザ観点でみたら気持ちいいものではないので、面倒くさがらずにちゃんと「AppLog入ってます」って書くべきだったか。反省してコメント修正しました。
  • ユーザ
    • メリット
      • 直接的には無い。間接的にはあるかもしれないが、結局開発者次第か。
    • デメリット
      • 気持ち悪いと感じられる人には気持ち悪く感じられる。

◆感情面

  • 「ログを取ってもいい?」って聞かれるだけで無条件で拒否反応を示す人は確実にいる。それは理解できる。


◆その他

  • AppLogサービス紹介サイトに英語版を追加して欲しい。英語圏のユーザに対しても説明が必要。(欲を言うならそれ以外の言語も)
  • まだこのサービスは始まったばかりで、今後どうなるかはわからないが、新しい試み。


AppLogはリスクをとって新しいビジネスモデルを模索してチャレンジしている。
応援したい。