Android開発メモ
忘れそうなのでメモ。
エミュレータの起動元のマシンを見に行く方法
開発環境でサーバを立ててそこのAPIを叩きにいく場合はエミュレータではlocalhostと入れるとエミュレータの中を見に行ってしまう。
10.0.2.2を指定してやると良い。
画面の回転禁止
Androidは画面を傾けると勝手に横向きにしたり、縦向きにしてくれたりするけど、縦固定、横固定にしたいときはAndroidManifest.xmlの指定したいactivityに下記を追加
- 縦固定
android:screenOrientation="portrait"
- 横固定
android:screenOrientation="landscape"
画面が回転したときに回転する前のデータを保存する
Androidは画面の向きを切り替えるときは初期化するようで、onCreateからやり直している。
これはつまり、onCreateのときにAPIで情報を取得して、それを表示する。みたいな処理をすると画面の向きが変わるだけでまた情報を取得しにいくという素敵なことになる。
で、回転前にデータを一旦保存しておきたい場合はonRetainNonConfigurationInstanceってメソッドを使うと簡単に保存できる。
Activitiyを継承した何か
private SaveData saveData; @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.mypage); /**回転時のデータ復元*/ saveData = (SaveMypage)getLastNonConfigurationInstance(); if(saveData == null){ //データが無かったときの処理 } else{ //あったときの処理 } } /** * 回転時のデータ保存 */ @Override public Object onRetainNonConfigurationInstance() { return saveData; }
こんな感じ。詳しくは下記に載ってる。
Androidでボタンを横に並べて自動で折り返す
はい、タイトル分かりづらいですね。まぁ前回の続きなんですけど。
AndroidのLinearLayoutとかを使って横にViewを並べると、長すぎる奴はそのまま外に出しちゃったり変な風にクシャッとしてくれたりして自動で折り返してくれない。
でもボタンを横に並べて、画面からはみ出そうになったら折り返して表示したい!しろよ!って思いまはしたものの、よい案が浮かばなかったのでWebViewと連携してその描画はCSSでやればいいんじゃね?って思って作っていたんですが、どうもレスポンスがよろしくない。
気晴らしにGジェネやっていたら、相対レイアウト使えばまさか!?って思いついて早速やってみたら出来たのでメモ。
まずはlayout
<?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"> <ScrollView android:layout_width="fill_parent" android:layout_height="fill_parent"> <RelativeLayout android:id="@+id/rl" android:layout_width="fill_parent" android:layout_height="wrap_content"> </RelativeLayout> </ScrollView> </LinearLayout>
ボタンは動的に出すので何も無いlayout。ScrollViewのなかにRelativeLayoutを入れているだけです。
Activityはこんな感じ。importとかは端折った。
public class TagsActivity extends Activity { private static String[] tags = { "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "12345678901234567890", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", "hoge", "fuga", "bar", "foo", "もげ", "daadae", "aadfefaea", "ちょっと長い", "1", }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 1000個くらいいれると若干固まるから別スレッドで作るとかやったほうがいいかも? for (int i = 0; i < tags.length; i++) { String s = tags[i]; Button btn = new Button(this); RelativeLayout.LayoutParams prm = new RelativeLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); // ボタン間のマージン 0にしても隙間が埋まらないんだけど!要調査 prm.setMargins(0, 0, 0, 0); btn.setText(s); btn.setId(i + 1); RelativeLayout r = (RelativeLayout) findViewById(R.id.rl); r.addView(btn, prm); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); RelativeLayout rl = (RelativeLayout) findViewById(R.id.rl); // 子供の数を取得 int l = rl.getChildCount(); // 無いなら何もしない if (l == 0) { return; } // ディスプレイの横幅を取得 WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); int max = display.getWidth(); int margin = 0; // 一番最初は基点となるので何もしない View pline = rl.getChildAt(0); // 一行全体の長さ int total = pline.getWidth() + margin; for (int i = 1; i < l; i++) { int w = rl.getChildAt(i).getWidth() + margin; RelativeLayout.LayoutParams prm = (RelativeLayout.LayoutParams) rl .getChildAt(i).getLayoutParams(); // 横幅を超えないなら前のボタンの右に出す if (max > total + w) { total += w; prm.addRule(RelativeLayout.ALIGN_TOP, i); prm.addRule(RelativeLayout.RIGHT_OF, i); } // 超えたら下に出す else { prm.addRule(RelativeLayout.BELOW, pline.getId()); // 基点を変更 pline = rl.getChildAt(i); // 長さをリセット total = pline.getWidth() + margin; } } } }
割と簡単。
onCreate時にButtonインスタンスを作って、RelativeLayoutにどんどん追加しています。
ボタンの数が多くなるようだったら別スレッドで処理したほうがいいのかな。ここも要検討。
Viewの長さはonCreateのときに取得しようとしてもまだ描画されてなく0になってしまうのでonWindowFocusChangedのタイミングで取得します。(タイミング的にここであってるよね?)
この辺のライフサイクルはほんとFlexと似てる。Flexの時もコンポーネントの長さを取得するのに頭を痛めた記憶があります。もう忘れたけど。
で、onWindowFocusChangedのところで、各ボタンの長さとディスプレイの長さを取得して、配置する場所を決めてるといった感じ。
今のところは当然ながらクリックしたときのレスポンスも良い感じ。これで先に進める…!?
AndroidでJavaScriptとJavaの連携
ボタンを一列に並べて、画面からはみ出るようだったら折り返すってビューを作りたかったんですが、JavaのViewで実現するのに妙案が浮かばず、じゃあWebViewで出しちゃえばいいじゃない!ってことで、JavaScriptとどうやって連携するのか調べました。
ちなみに何を作ってるのかというとタグを並べて、それを押したらEditTextに文字列を流し込むって画面。はてブアプリをパクr…、参考にして。あれかっこいい。あれもWebViewで実現してるんでしょうか。
で、JSの連携は説明はめんどいので実際のソースコード。
まずはlayout
<?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" android:focusable="false" android:focusableInTouchMode="false" > <TextView android:id="@+id/tv" android:layout_width="fill_parent" android:layout_height="wrap_content"/> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="btn" android:onClick="clickBtn"/> <WebView android:id="@+id/wv" android:layout_width="fill_parent" android:layout_height="fill_parent" android:focusable="false" android:focusableInTouchMode="false"/> </LinearLayout>
難しいところは何一つない。
お次はActivity。importとかは端折った。
public class JsBridgeActivity extends Activity { private static final String HOGE_HTML = "file:///android_asset/hoge.html"; private TextView textView; private WebView webView; /** * WebViewの中で使うJsObj */ public class JsObj{ private JsBridgeActivity act; private Handler handler; public JsObj(JsBridgeActivity act){ this.act = act; handler = new Handler(); } /** * ボタンクリック時に呼ばれる * @param tag */ public void send(final String str){ //ここで普通にact.setTextValue(str)とかやっても怒られる。 handler.post(new Runnable() { public void run() { act.setTextValue(str); } }); } /** * Textの中の値を取得 * @return */ public String getTextValue(){ if(textView == null){ return ""; } String str = textView.getText().toString() == null ? "" : textView.getText().toString(); return str; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); textView = (TextView) findViewById(R.id.tv); textView.setText("ほげー"); webView = (WebView) findViewById(R.id.wv); //js使用可能 webView.getSettings().setJavaScriptEnabled(true); //load webView.loadUrl(HOGE_HTML); JsObj js = new JsObj(this); //JavaScriptではandroidでJsObjにアクセスできる webView.addJavascriptInterface(js, "android"); } /** * ボタン押した * @param v */ public void clickBtn(View v){ webView.loadUrl("javascript:fromAndroid('androidからです')"); } /** * textに値を入れる * @param str */ public void setTextValue(String str){ if(textView == null) return; textView.setText(str); } }
ポイントはJavaScriptで使うオブジェクトを作ってあげることと、JavaScriptからActivityの値を変更したい場合は、handlerを経由してあげなきゃいけない。
handlerについて詳しくは下記。
http://www.adamrocker.com/blog/261/what-is-the-handler-in-android.html
で、最後にhoge.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1"> <script type="text/javascript" src="js/jquery-1.4.3.min.js"></script> <script type="text/javascript"> $(document).ready(function(){ $("#sendBtn").click(function(){ android.send($("#input").val()); }); $("#getBtn").click(function(){ $("#jText").text(android.getTextValue()); }); }); function fromAndroid(str){ $("#jText").text(str); } </script> </head> <body> <input type="text" value="" id="input" /> <button id="sendBtn">send</button> <p id="jText"></p> <button id="getBtn">get</button> </body> </html>
ソース見れば分かるように、
JsObj js = new JsObj(this); webView.addJavascriptInterface(js, "android");
と指定してやるとJavaScript側からJavaのメソッドを叩ける。
そしてJava側からJSを叩く場合はloadUrlの引数にJSを書けばよいらしい。すげーかっこわるい!?
ただ、あんまりレスポンスが良くないような気がします。この程度だったらよいけど、今作ってるタグのUIはタグが押されてから文字列を流し込むまで若干もたつく印象。パクリもとの参考にしたはてブのアプリはレスポンスも素敵なんですが…。アプローチが間違ってるんだろうか。ううむ。
AndroidでTwitterのOauth認証(Twitter4j使用)
Andoridの入門書をざっくりとですが、一冊ほど目を通して、なんとなーく概要がつかめたので、少しずつえごリブアプリを作ってます。
で、えごリブはTwitterを使って認証してるのでAndroidでもOauth認証させる必要があったので、どうやるのか調べてみました。
基本的な流れはWebとあまり変わらない。
- TwitterFactory#getInstanceでtwitterインスタンスを作成(必要に応じてConsumeKeyとConsumeSecretKeyをセット)
- twitter#getOauthRequestTokenでリクエストトークンを作成
- requestToken#getAuthorizationURLで認証用URLを取得
- 認証画面に飛ばす(ブラウザを起動させる)
- 認証に成功したらアプリケーションに戻ってoauth_verifierパラメータを取得
- twitter#getOuathAccessTokenでアクセストークンを取得
- 取得したアクセストークンをプリファレンスに保存するなり、それを使用してツイートしたりご自由に
Webと違うのは認証後にブラウザから自分のアプリケーションを起動させるところ。
あとは大体同じ。
Webから自分のアプリケーションを起動させる方法はスキーマを設定してあげると出来る模様。
http://www.atmarkit.co.jp/fsmart/articles/androidmixi/01.html
上記サイトの流れと基本一緒です。(上記はmixiのOauth認証)
具体的なソースとかは下記みたいな感じ。
あ、ちなみにtwitter4jのバージョンは2.2.1です。
まずAndroidManifest.xmlを変更
<activity android:name=".common.TwitterOauth" android:label="@string/twitter_oatuh_title" android:launchMode="singleTask" > <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="egolib" android:host="callback" /> </intent-filter> </activity>
こんな感じ。
activityにlaunchModeを追加。
intentにスキーマを設定してます。
Androidのシミュレータ重いってレベルじゃねーぞ!
次にlayoutを作成
<?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"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=""/> <TextView android:id="@+id/token" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=""/> <TextView android:id="@+id/secret_token" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=""/> </LinearLayout>
このサンプルは単純に認証が終わったらTextViewにAccessTokenの値を表示してるだけ。
Androidのシミュレータ重いってレベルじゃねーぞ!
で、Activityを作成
package sample.common; import twitter4j.Twitter; import twitter4j.TwitterException; import twitter4j.TwitterFactory; import twitter4j.auth.AccessToken; import twitter4j.auth.RequestToken; import sample.android.R; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class TwitterOauth extends Activity { private static final String CALLBACK_URI = "egolib://callback"; private static final String VERIFIER = "oauth_verifier"; private Twitter twitter; private RequestToken requestToken; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.twitter_oauth); try{ //consumeKey,consumeSecretKeyはtwitter4j.propertiesに記述 twitter = new TwitterFactory().getInstance(); requestToken = twitter.getOAuthRequestToken(CALLBACK_URI); String url = requestToken.getAuthorizationURL(); Intent i = new Intent(Intent.ACTION_VIEW , Uri.parse(url)); //ブラウザ起動 startActivity(i); }catch(TwitterException e){ Log.e("oauth_error", e.getMessage()); } } /** * コールバック時 */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); TextView tv = (TextView)findViewById(R.id.tv); Uri uri = intent.getData(); if(uri == null || !uri.toString().startsWith(CALLBACK_URI)){ tv.setText("こんなの絶対おかしいよ。"); return; } AccessToken aToken = null; String verifier = uri.getQueryParameter(VERIFIER); if(verifier == null || verifier.length() == 0){ tv.setText("認証が拒否されたし。"); return; } try{ aToken = twitter.getOAuthAccessToken(requestToken, verifier); }catch(TwitterException e){ Log.e("oauth_error" , e.getMessage()); tv.setText("失敗した!"); return; } tv.setText("success"); TextView token = (TextView)findViewById(R.id.token); token.setText(aToken.getToken()); TextView sToken = (TextView)findViewById(R.id.secret_token); sToken.setText(aToken.getTokenSecret()); } }
エラー処理は適当です。
このActivityは起動した瞬間にTwitterの認証ページにすっ飛ばしてます。
使い方としては、他のActivityで認証ボタンなんかを用意して、このActivityに飛ばすとかって感じですかね。
おかしなところがあったら教えてくれるとうれしいです。
Androidのオプションメニューのアイコンに標準のものを使用する
絶対使うけど、絶対忘れそうなのでメモ。
オプションメニューにアイコンを指定するときに、標準のシステムアイコンを使用するやり方。
android:icon="@android:drawable/ic_menu_add"
使えるアイコンの種類は下記サイトに載ってる。
Javaでメニューを設定する場合のやり方も載ってる。
http://www.taosoftware.co.jp/blog/2008/11/android_5.html
あと自作でアイコン作る場合はジェネレータがあった。ちょっと画像書き出してみたけど超便利そう!まぁアイコン作る技術はないわけだが。
PHPで暗号化(BlowfishのPKCS5Padding方式)
追記(2011/4/7)
なんかAndroidさんBlowfishサポートしてないくさい\(^o^)/
他の暗号化形式はいけるんだろうか…。久々にきれちまったよ…。
アンドロイドアプリを作る勉強をスーパースローペースで進めているんですが、
飽きてきたので、えごりぶ用の作るとなるとAPI作らないといけないなーと思い、仕様をモヤモヤと考えていたんですが、暗号化で通信する必要がありそうな箇所があったので、PHPで暗号化する方法を調べました。
PHPはmcryptっていう拡張モジュール(使ってるのはPHP5.3なので最初からmcryptは入っているようでした。)で主要な暗号方式をフォローしているようですが、パディング方式が選べず、ZeroPadding方式になってしまうらしい。パディング方式がなにかってのはggr。というか僕も説明できるほど理解してないけど。
Javaの方はというとパディング方式が選べるんですが、どうも下記サイトを見るとZeroPadding方式がないっぽい。\(^o^)/
http://www.trustss.co.jp/Java/JEncrypt100.html
で、下記サイトにまさに僕がやりたいことを既にやろうとしている人がいて、詳しく書いてあったので参考に。
http://d.hatena.ne.jp/pasela/20100612/crypto#20100612f3
そしてここ読んでたら、PHPでPKCS5Padding方式でパディングする方法も載ってたのでコピペしつつ作れました。さっくりと。うそ、めんどかった。
ソースは下記。BlowfishのECBモードで変換してます。また、暗号化したバイナリは16進数の文字列にしてます。
<?php function encrypt($key , $text){ $size = mcrypt_get_block_size('blowfish', 'ecb'); $input = pkcs5_pad($text, $size); $td = mcrypt_module_open('blowfish', '', 'ecb', ''); $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND); mcrypt_generic_init($td, $key , $iv); $data = mcrypt_generic($td, $input); mcrypt_generic_deinit($td); mcrypt_module_close($td); //16進数の文字列にする $data = bin2hex($data); return $data; } function decrypt($key , $text){ $td = mcrypt_module_open('blowfish', '', 'ecb', ''); //16進数の文字列をバイナリにする $data = pack("H*" , $text); $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND); mcrypt_generic_init($td, $key , $iv); $data = mdecrypt_generic($td, $data); mcrypt_generic_deinit($td); mcrypt_module_close($td); $data = pkcs5_unpad($data); return $data; } function pkcs5_pad ($text, $blocksize){ $pad = $blocksize - (strlen($text) % $blocksize); return $text . str_repeat(chr($pad), $pad); } function pkcs5_unpad($text){ $pad = ord($text{strlen($text)-1}); if ($pad > strlen($text)) return false; if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) return false; return substr($text, 0, -1 * $pad); }
でも実際使うかどうかは謎。そもそもAndoroidアプリが出来上がるかどうかも謎。
CakePHPで独自の共通コントローラを継承させて使用する
CakePHPさんはControllerを作成する場合は基本的にAppControllerを継承させて作成するわけですが、AppConroller→他の共通のController→HogeControllerみたいに1つ別の共通コントローラを継承させたいってときがあって、かなーりはまったのでメモ。
ただ、激しくバッドノウハウ。
で、何にはまったかというと、AppControllerに共通のコンポーネントやヘルパーを書いて継承した場合は、HogeControllerに書いたコンポーネント、ヘルパーとかはちゃんとマージしてくれるんですが、自分で作ったコントローラを継承させた場合は親のコンポーネントやヘルパーを上書きしてくれるという素敵仕様だった。ひどくね?
というわけでControllerのソースを見に行ったら__mergeVars()で、AppControllerの内容をうまくマージさせてるっぽかったので、こいつを少し改造しました。
具体的には下記のような感じに。
まず、AppControllerを作る
<?php class AppController extends Controller{ var $margeClassName = null; function __mergeVars() { if($this->margeClassName){ $pluginName = Inflector::camelize($this->plugin); $pluginController = $pluginName . $this->margeClassName; if (is_subclass_of($this, $this->margeClassName) || is_subclass_of($this, $pluginController)) { $appVars = get_class_vars($this->margeClassName); $uses = $appVars['uses']; $merge = array('components', 'helpers'); $plugin = null; if (!empty($this->plugin)) { $plugin = $pluginName . '.'; if (!is_subclass_of($this, $pluginController)) { $pluginController = null; } } else { $pluginController = null; } if ($uses == $this->uses && !empty($this->uses)) { if (!in_array($plugin . $this->modelClass, $this->uses)) { array_unshift($this->uses, $plugin . $this->modelClass); } elseif ($this->uses[0] !== $plugin . $this->modelClass) { $this->uses = array_flip($this->uses); unset($this->uses[$plugin . $this->modelClass]); $this->uses = array_flip($this->uses); array_unshift($this->uses, $plugin . $this->modelClass); } } elseif ($this->uses !== null || $this->uses !== false) { $merge[] = 'uses'; } foreach ($merge as $var) { if (!empty($appVars[$var]) && is_array($this->{$var})) { if ($var !== 'uses') { $normal = Set::normalize($this->{$var}); $app = Set::normalize($appVars[$var]); if ($app !== $normal) { $this->{$var} = Set::merge($app, $normal); } } else { $this->{$var} = array_merge($this->{$var}, array_diff($appVars[$var], $this->{$var})); } } } } if ($pluginController && $pluginName != null) { $appVars = get_class_vars($pluginController); $uses = $appVars['uses']; $merge = array('components', 'helpers'); if ($this->uses !== null || $this->uses !== false) { $merge[] = 'uses'; } foreach ($merge as $var) { if (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($this->{$var})) { if ($var !== 'uses') { $normal = Set::normalize($this->{$var}); $app = Set::normalize($appVars[$var]); if ($app !== $normal) { $this->{$var} = Set::merge($app, $normal); } } else { $this->{$var} = array_merge($this->{$var}, array_diff($appVars[$var], $this->{$var})); } } } } } parent::__mergeVars(); } }
$margeClassNameっていうプロパティを追加してやる。
そして__margeVarsをまるっとコピペして'AppController'って部分を$this->$margeClassNameに変えて、最後にスーパークラスの同じメソッドを読んでるだけ。
で、共通のControllerで$margeClassNameを設定してやる
<?php class CommonAppController extends AppController{ var $margeClassName = "CommonAppController "; var $components = array("共通のコンポーネント"); }
後はコントローラを実装するときにCommonAppControllerを普通に継承してやればよい。
<?php App::import("Controller" , "CommonApp"); class HogeController extends CommonAppController { var $components = array("ここで使うコンポーネント"); }
これでCommonAppController,AppController,HogeControllerのコンポーネントをうまくマージしてくれました。
けど、__margeVarsをまるっとコピペしてあるあたりアレ。
それと当然だけど、多段継承をさらにすると上書いてくれます。\(^o^)/
まぁ、コンポーネント、ヘルパー、モデルあたりをマージするだけだったら、コンストラクタ読んでるところでマージしてあげるだけでもいいっぽいんだけど。
でももっとうまいやり方ありそうだよなあ。ググってもそれらしいのが見つからなかった!もっとエレガントな方法教えてぷりいいいいず。