Java初心者が携帯百景投稿用クライアントを作る その3

とりあえずコマンド行編集と投稿用アドレスの自動設定の機能のみの実装版ですがAndroid Marketに登録しましたので携帯百景のアカウントをお持ちの方は試してみてはいかがでしょうか。

http://image.movapic.com/pic/m_200908220857234a8f346371740.jpeg
こんな感じで表示されるはずですが、後ろの方に流されているかもしれませんね
http://jp.androlib.com/android.application.com-natu-android-movapichelper-qzxx.aspxにアクセスしてQRコードで直接、マーケットを開いてしまうのが早いかもしれません。

    • コンタクトリストを見に行くのでインストールの時に注意が表示されます
  • 無事インストールが終わったら投稿用アドレスを自分のAndroid携帯に登録しましょう
    • 一旦PCのgmailから携帯百景から投稿した後、Android携帯の連絡先を同期させてAndroid携帯に投稿用アドレスを登録します
  • 次からは通常の投稿パターン

カメラアプリやギャラリーから投稿したい画像を選んで共有をクリック

  • アプリが起動しますのでドロップダウンリストやチェックボックスで文字の色や表示位置を調整します

  • 設定したら「送る」ボタンでメーラーを起動します

  • 本文を入力したら投稿しましょう
    • ね、(ちまちま手入力するよりは)簡単でしょ。

追記(重要)

「送る」を押した後、メーラーの一覧が表示されますので使いたいメーラーを選んでください

Java初心者が携帯百景投稿用クライアントを作る その2

前回の多量のボタンのアクションの記述を簡単にしたい・・・のある程度の対応が出来たのでメモ

  • strings.xmlにボタンに貼付ける文字列を記述
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, mainActivity!</string>
    <string name="app_name">testEdit</string>
    <string-array name="array01">
    	<item>[[</item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item></item>
    	<item>]]</item>
</string-array>
</resources>
  • レイアウトをmain.xmlに記述*1
<?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">
	<EditText android:text="EditText01" android:id="@+id/EditText01"
		android:layout_width="fill_parent" android:layout_height="wrap_content"></EditText>
	<LinearLayout android:id="@+id/LinearLayout01"
		android:layout_width="fill_parent" android:layout_height="wrap_content">
		<Button android:text="Button02" android:id="@+id/B0"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button03" android:id="@+id/B1"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button04" android:id="@+id/B2"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button05" android:id="@+id/B3"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button06" android:id="@+id/B4"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button07" android:id="@+id/B5"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button08" android:id="@+id/B6"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button09" android:id="@+id/B7"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button10" android:id="@+id/B8"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
	<LinearLayout android:id="@+id/LinearLayout02"
		android:layout_width="fill_parent" android:layout_height="wrap_content">
		<Button android:text="Button11" android:id="@+id/B9"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button12" android:id="@+id/B10"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="Button13" android:id="@+id/B11"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
</LinearLayout>
  • メインとなるmainActivity.java
package com.natu.testedit;

import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class mainActivity extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		// コマンドボタン創成
		int len = getResources().getStringArray(R.array.array01).length;
		ArrayList<Button> btns = new ArrayList<Button>();
		for (int i = 0; i < len; i++) {
			int wId = getResources().getIdentifier("B" + String.valueOf(i),
					"id", "com.natu.testedit");
			final Button btn = (Button) findViewById(wId);
			btn.setText(getResources().getStringArray(R.array.array01)[i]);
			btn.setOnClickListener(new View.OnClickListener() {

				public void onClick(View view) {
					EditText txt = (EditText) findViewById(R.id.EditText01);
					int loc = txt.getSelectionStart();
					String ttxt = txt.getText().toString();
					// カーソル位置にボタンの文字を挿入
					String prefix = ttxt.substring(0, loc);
					String suffix = loc < ttxt.length() ? ttxt.substring(loc,
							ttxt.length()) : "";
					txt.setText(prefix + btn.getText() + suffix);
					txt.setSelection(loc + btn.getText().length());
				}
			});
			btns.add(btn);
		}
	}
}

今回の肝はgetIdentifierというメソッド、これを使うことにより指定した条件でコンポーネントを検索してIdを返してくれる
つまり変数でIdを指定できるということ

public int getIdentifier (String name, String defType, String defPackage)

Return a resource identifier for the given resource name. A fully qualified resource name is of the form "package:type/entry". The first two components (package and type) are optional if defType and defPackage, respectively, are specified here.

Note: use of this function is discouraged. It is much more efficient to retrieve resources by identifier than by name.
Parameters
name The name of the desired resource.
defType Optional default resource type to find, if "type/" is not included in the name. Can be null to require an explicit type.
defPackage Optional default package to find, if "package:" is not included in the name. Can be null to require an explicit package.
Returns

* int The associated resource identifier. Returns 0 if no such resource was found. (0 is not a valid resource ID.)

で、このような画面が出来上がる

あとは絵文字の対応が済めばいよいよリリースだが、情報が一画面に納まらないため機能毎にタブで分けたり絵文字ボタンの見せかたをどうするか等の問題が山積みである

今まで出来ているところ

  • カメラアプリやギャラリーの共有機能にアプリを追加する

  • ローカルのコンタクトリストから投稿用アドレスの取得、文字位置等のコマンドをドロップダウンリストで指定

  • 送るボタン押下でメーラアプリが起動


という部分は完成済みでDeveloper Registration Feeも支払い済みだったりはしているので近いうちβ版として登録しても良いかもしれない

*1:本当はこれも省きたい

Java初心者が携帯百景投稿用クライアントを作る その1

HT-03Aで撮影した写真を携帯百景に投稿する時に文字の表示位置や色をソフトキーボードで入力するのも大変だなと思い、Javaの勉強を兼ねて入力ヘルパーを作ってみる実験

やりたいこと

    1. カメラアプリやギャラリーの共有メニューに入力ヘルパーアプリを追加する
    2. ヘルパーアプリではコンタクトリストから投稿用メールアドレスを取得、文字色や文字位置、サイズをドロップダウンリストから選択出来るようにする
    3. 投稿ボタンをクリックで予め送信先アドレス、タイトル(表示コマンド)がセットされた状態でメーラーアプリを起動する
    4. メールを送信後にカメラアプリまたはギャラリーへ戻る

Androidの場合ゴリゴリとすべて作り込まなくてもIntentという仕組みを使って、こういうデータでこういうことがしたいと投げることによりその処理が可能なアプリがある場合にそのアプリが出来ますよと手を挙げてくれる仕組み(かなりの意訳ですが)があるので、今回は画像データを受けてメーラーに投げるところまでが範囲

    • カメラアプリやギャラリーの共有メニューに入力ヘルパーアプリを追加する

AndroidManifest.xmlにintent-filterを追加

<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <action android:name="android.intent.action.SEND"/> 
    <category android:name="android.intent.category.DEFAULT"/> 
    <data android:mimeType="image/jpeg"/> 
</intent-filter>

jpegデータが投げられたら受けます(起動)

はまったところ

  • メールアドレスを指定してもメーラーのto欄に設定されない
Intent it = new Intent(Intent.ACTION_SEND);
it.putExtra(Intent.EXTRA_EMAIL, "foo@bar.com");
it.putExtra(Intent.EXTRA_SUBJECT, title);
it.setType("image/jpeg");
it.putExtra(Intent.EXTRA_STREAM, Uri.parse(String.valueOf(uri)));
it.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
startActivity(it);

なんて固定で書いても空っぽになる。

String[] adrs = {
  "foo@bar.com",
};
it.putExtra(Intent.EXTRA_EMAIL, adrs);

と配列でセットしてあげればOK

  • SQLのWHERE句はWHEREは書かなくて良い

どのサンプルを見ても実際にWHERE句を書いてある例がなくて四苦八苦*1

/*		携帯百景投稿用アドレス取得	*/        
String[] projection = new String[] {
		android.provider.Contacts.ContactMethodsColumns.DATA
};

final Cursor cur = managedQuery(android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI,
                                     projection,
                                     android.provider.Contacts.ContactMethodsColumns.DATA + " LIKE '%movapic.com'",
                                     null,
                                     android.provider.Contacts.ContactMethodsColumns.DATA + " ASC");
int emailColumn = cur.getColumnIndex(android.provider.Contacts.ContactMethodsColumns.DATA);

spinner_mail = (Spinner) findViewById(R.id.Spinner01);
ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(
		this, 
    	android.R.layout.simple_spinner_item);
adapter1.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
while (cur.moveToNext()){
        adapter1.add(cur.getString(emailColumn));
}
spinner_mail.setAdapter(adapter1);

これで(ローカルの)コンタクトリストからドメインがmovapic.comのメールアドレスのみを抽出してドロップダウンリストにセット出来た

はまっているところ

大量のボタンのアクションを登録するのにいちいち全部の項目に対して記述していったら無駄なので配列の数分ループさせてボタンのテキストとかだけを変えたいが、どのIdに対してイベント等を登録するかというやり方しか調べられていない。

Button button1 = (Button) findViewById(R.id.Button01);
button1.setOnClickListener(new View.OnClickListener() {

        public void onClick(View view) {
         //  処理
        }
);

ようはbutton1とかButton01と固定で書いているところを変数にしたい

メモ

EditTextのカーソル位置はgetSelectionStart()で取得出来るので、getText().toString()でEditTextのテキストを取得してカーソル位置に特定の文字列(タグとか)を挿入すればOK

*1:全部必要なのでnullとか

ISBNをハイフン編集してJSONPで返すサービスを最新化してみた

Business::ISBNのデータを自分で更新出来たらなとモジュールを眺めるとデータはData.pmにあるらしい

use Business::ISBN::Data 20081208; # now a separate module

で、Data.pmでどのように持っているかというと

%country_data = (
0     => ['English speaking area' =>   ['00' => '19', '200' => '699', '7000' => '8499', '85000' => '89999', '900000' => '949999', '9500000' => '9999999'] ],
1     => ['English speaking area' =>   ['00' => '09', '100' => '399', '4000' => '5499', '55000' => '86979', '869800' => '998999'] ],
2     => ['French speaking area' =>   ['00' => '19', '200' => '349', '35000' => '39999', '400' => '699', '7000' => '8399', '84000' => '89999', '900000' => '949999', '9500000' => '9999999'] ],

http://www.isbn-international.org/converter/ranges.htmで使っているhttp://www.isbn-international.org/converter/ranges.jsをごにょるスクリプトを書きながら出版社コードの上限下限をセットで苦戦
Data.pmのコメントを眺めると

This module lives in the Github repository with Business::ISBN:

	git://github.com/briandfoy/business--isbn.git

Github見たら最新情報があるかなと見に行くと最新バージョンは20081208版で変わりないけど、make_data.plなんてのを発見、そのものズバリのPerlスクリプトが本家にあるとは・・・
早速うごかしたいところであるけどPerlのバージョンが5.010以上じゃないと動かない、次のあたりを5.8で書くには面倒だ

my @keys = qw(text ranges);
my %data;

while( $js_data =~
    /
        ^gi\.area(?<group>\d+)\.text \s* = \s* "(?<text>.*?)" ;?  [\r\n]+
        ^gi\.area(\1)\.pubrange \s* = \s* "(?<ranges>.*?)"    ;?  [\r\n]+
    /gmx
    )
    {
    @{ $data{ $+{group} } }{ @keys } = @+{ @keys };
    }

#名前付きキャプチャバッファすごいな
こんなきれいに書く自信が無いのでMacBookにPerl5.010をインストール(別ディレクトリで)して実行、サーバのData.pmを最新化した
これで次のあたりも対応できました

09.12.2008; Affed Mongolia (99962) "0-4;50-79;800-999"
24.11.2008; Added El Salvador (99961) "0-3;40-89;900-999"
20.11.2008; Changed Benin (99919) to include "300-399" (previously undefined)
24.10.2008: Define India (93)"00-09;100-499;5000-7999;80000-94999;950000-999999"
20.10.2008; Changed Kenya (9966) "00-19" becomes "000-199"
05.10.2008; Addec Ukraine (611)"00-49;500-699;7000-8999;90000-99999"
29.09.2008; Added Syria (9933) "0-0;10-39;400-899;9000-9999"

ハイフン無しのISBNを渡すとハイフン付きのJSONPを返すサービスを作ってみた

中身的にはBusiness::ISBN - work with International Standard Book Numbers - metacpan.orgを呼んでJSONPにして返すだけですが、ニッチな需要があるのでw
2008年10月ぐらい?のデータを元にしているので全部のパターンには対応出来ていないのが残念ではありますが

サンプル画面

http://natu.blue.coocan.jp/isbntest.htmlにアクセス

    • 入力エリアにISBNを入力(ISBN-10またはISBN-13)

http://img.skitch.com/20090110-8s4hmrtr1fq2q22xuwi82rmtch.png

http://img.skitch.com/20090110-c2h6gpen4eybigfbruanxi65ce.png

    • 桁数が合ってないとエラーになる

http://img.skitch.com/20090110-m23crmjhkiat216gtgqc3aj4n4.png

    • 99945(Namibia)以降が対応されてない

http://img.skitch.com/20090110-kxqmen69155s77pt9inyie4itg.png

JSONPのURL

http://natu-n.com/cgi/ISBN.cgi?isbn=ISBN-13(10)&callback=コールバック名
ただしcallbackはオプション

レスポンス例

  • ISBN(10桁または13桁)を渡して取得する
callback({
   "isbn10" : "4150116784",
   "isbn10e" : "4-15-011678-4",
   "isbn13" : "9784150116781",
   "isbn13e" : "978-4-15-011678-1",
   "success" : true
})
  • 与えたISBNが正しくない(桁数)とfalseで帰る
callback({
   "success" : false
})
  • callback=コールバック名で任意のコールバック名で取得出来る
callbacks({
   "isbn10" : "4150400083",
   "isbn10e" : "4-15-040008-3",
   "isbn13" : "9784150400088",
   "isbn13e" : "978-4-15-040008-8",
   "success" : true
})

サンプルソース(HTML+JS)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="text/javascript" src="lib/jquery-1.2.6.pack.js"></script>
    <title>TEST</title>
    <script type="text/javascript">
        /* <![CDATA[ */
$(document).ready(function() {
    $("#isbn").keydown(function(e) {
        if (e.which == 13) {
            return false;
        }
    });
    $("#btn").click(function() {
        $("#success").text("");
        $("#isbn10").text("");
        $("#isbn10e").text("");
        $("#isbn13").text("");
        $("#isbn13e").text("");
        $.ajax({
            url : "http://natu-n.com/cgi/ISBN.cgi",
            dataType : "jsonp",
            data : {
                isbn : $("#isbn").val()
            },
            success : function(json){
                // ロード完了時にここが呼ばれる
                $("#success").text(json.success);
                if ( json.success == true ) {
                    $("#isbn10").text(json.isbn10);
                    $("#isbn10e").text(json.isbn10e);
                    $("#isbn13").text(json.isbn13);
                    $("#isbn13e").text(json.isbn13e);
                }
            },
            error : function(){
                alert('error');
            }
        });
    });
});
        /* ]]> */
    </script>

</head>
<body>
<h2>ISBNを入力(10または13桁)</h2>
<form id="iform" name="iform">
    <input type="text" id="isbn" name="isbn" size="13">
    <input type="button" id="btn" value="変換">
</form>
<table>
    <tdoby>
        <tr><td>status</td><td>:</td><td id="success"></td></tr>
        <tr><td>isbn10</td><td>:</td><td id="isbn10" ></td></tr>
        <tr><td>isbn10e</td><td>:</td><td id="isbn10e"></td></tr>
        <tr><td>isbn13</td><td>:</td><td id="isbn13" ></td></tr>
        <tr><td>isbn13e</td><td>:</td><td id="isbn13e"></td></tr>
    </tbody>
</table>
</td>
</body>
</html>

jQueryでクエリ文字列をとりだす

prototype.jsの場合は

var para = document.location.search.substr(1).toQueryParams();
if (para['hoge']){
//  hageが存在したとき
}
else {
//  hogeが存在しないとき
}

なんてサラっと書けるがjQuery単体では同様な機能がないので、http://plugins.jquery.com/project/query-objectを使用する

var bar = $.query.get('foo');
if (bar){
//  barが存在したとき
}
else {
//  barが存在しないとき
}

余談、最初このPluginの存在を知らなくてprototype.jsを一緒に読み込ませようとしたけど他のPluginがjQuery.noConflictに対応してなくて残念な結果に

携帯百景でフォロー/フォロワーの差分を取ってみる

一ヶ月の出張予定が二ヶ月になって夏服しかなくてしょんぼりなid:natu_nですが、
長期出張中にドハマりしてるのが携帯百景(ケイタイヒャッケイ)、携帯電話で撮影した写真に文字をのせてwebにアップして、みんなにコメントをつけてもらったりつけたり出来るサービスです。

特に継続してアップした写真を見れるってこと以外にあまり意味はないけど、Twitterみたいにフォロー/フォロワーな関係を築くことが出来ます。

しばらく前にwebで確認したら誰にフォローされてないんだろ?って疑問になって*1、リハビリついでにザックリ書いてみました

#!/usr/bin/perl
use FindBin::libs;
use strict;
use warnings;
use CGI;
use Encode;
use Perl6::Say;
use Readonly;
use URI;
use WWW::Mechanize;
use Web::Scraper;

Readonly my $MOVAPIC_URL_PREFIX => 'http://movapic.com/';
Readonly my $MOVAPIC_FOLLOWER_URL_SUFFIX => '/follower';
Readonly my $MOVAPIC_FOLLOWING_URL_SUFFIX => '/following';

my $mech = WWW::Mechanize->new;
my $q    = new CGI;
my $name = $q->param('name');
my $flg  = $q->param('flg');

my $uri = URI->new(
        $MOVAPIC_URL_PREFIX 
      . $name
      . (
          $flg == 0 ? $MOVAPIC_FOLLOWER_URL_SUFFIX
                    : $MOVAPIC_FOLLOWING_URL_SUFFIX
      )
);

my $s = scraper {
    process '//div[3]/div//td[2]/p/a', 'screen_names[]' => 'text';
    process 'a.nextLink',              'next'           => '@href';
};

my @screen_names = ();
my $url          = URI->new($uri);

while (1) {
    $mech->get( $url->as_string );
    die $mech->res->status_line unless $mech->success;
    my $res = $s->scrape( $mech->content );
    push @screen_names, @{ $res->{screen_names} };
    last unless $res->{next};
    $url->path_query( $res->{next} );
}

say encode( 'utf-8', $_ ) for sort @screen_names;

パラメータはnameが自分(または確認したい人の)ユーザ名と、flgが0:フォローワー、1:フォローで
例えばこんなふうに実行すると

$ perl ./keitai100.pl  name=natu flg=0 > followers.txt
$ perl ./keitai100.pl  name=natu flg=1 > following.txt
$ diff -U 0 following.txt followers.txt

意外な人にフォローされてなくてビックリとかできます

備考

use FindBin::libs;
use Perl6::Say;
use Readonly;

なんかは趣味の世界なので普通に書いても全然問題ないです
あと、次ページの辿りかたなどはWeb::Scraper で全ての following/followers の screen_name を取得する - nipottern - はてなグループ::ついったー部を参考にしてます

*1:フォローしてないのは画面でわかる仕組みになっています