2013/02/16

Androidの非同期処理と画面回転

Android-x86-4.4r2 の設定はこちら


Androidで非同期処理を Thread でやろうとするとこんな例外が出て怒られる。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
UI スレッド以外が UI を操作出来ないって事らしい。

普通は Handler か AsyncTask を使えば回避できるのだが、ここで一つ問題が発生する。

Android のデフォルト動作では画面が回転すると Activity の破棄と再構築が行われる。
したがって AsyncTask が処理を終らせて結果を Activity に通知しようとしても既に存在しない可能性がある。

とりあえずの解決法としては AndroidManifest.xml の <activity> タグに以下の属性を追加して回転しても再構築を行わせないようにすることができる。

android:configChanges="orientation|keyboardHidden"

とは言えこれで逃げられないケースも有るのでまじめな対策を考える。

そもそも、Android の Activity はテンポラリな物で何時破棄されてもおかしく無いという位置付けになっている。にもかかわらずアプリケーションは Activity を中心に設計するようになっているので問題が発生しているように思える。

ここのページの方は Activity はあくまで「従」立場で使えとおっしゃってますが現実には色々難しいかと思います。既存のコードも有るし。


問題の本質は「主」である Activity が「従」である AsyncTask の知らない間に入れ替わってしまっていることだ。

朝、出社したら課長の席に知らないおじさんが座っているようなものである。

こういう場合は人事課に現在の所属課の課長が誰なのか問い合わせられれば良いわけだけど Android にはそういう仕掛けが用意されていない。

と言うわけで、Activity を ID で管理するクラスを用意して AsyncTask は ID から必要な時に Activity 問い合わせる方式で実装しみた。

ソースコード


ActivityManager.java:
package org.kotemaru.android.asyncrotate;

import java.util.HashMap;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;

/**
 * Activityの管理クラス。
 * - Activityが destroy/create されても同一IDで継続的にアクセスできる。
 * - インスタンスをApplication.registerActivityLifecycleCallbacks()に設定する事。
 * - Bundle のキー "___ACTIVITY_ID___" を汚染する。
 * @author kotemaru.org
 */
public class ActivityManager implements ActivityLifecycleCallbacks {
    public final String ACTIVITY_ID = "___ACTIVITY_ID___";
   
    /** Application内で一意のActivityのIDカウンタ */
    private Integer nextActivityId = 0;
   
    // マップ
    private HashMap aid2activity = new HashMap();
    private HashMap activity2aid = new HashMap();

   
    /**
     * ActivityからIDの取得。
     * - すでにIDを持っている場合はそれを返す。
     * - IDを持っていない場合はマップに新規登録して返す。
     * @param activity
     * @return Application内で一意のID
     */
    public synchronized String getActivityId(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) {
            aid = (nextActivityId++).toString();
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
        return aid;
    }
    /**
     * IDからActivityの取得。
     * - 登録されているIDからActivityを引いて返す。
     * - Activity が destroy/create されていてると更新されている。
     * @param aid ActivityのID
     * @return Activity。未登録の場合はnull。
     */
    public synchronized Activity getActivity(String aid) {
        return aid2activity.get(aid);
    }
   
   
    /**
     * Activity.onCreate()のハンドリング。
     * - Bundleに ___ACTIVITY_ID___ を持っていればマップを更新。
     */
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (savedInstanceState == null) return; // First
        String aid = savedInstanceState.getString(ACTIVITY_ID);
        if (aid == null) return; // Not managed.
       
        synchronized (this) {
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
    }
   
    /**
     * Activity.onSaveInstanceState()のハンドリング。
     * - ___ACTIVITY_ID___ にActivityのIDを保存。
     */
    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        String aid = activity2aid.get(activity);
        outState.putString(ACTIVITY_ID, aid);
    }

    /**
     * Activity.onDestroy()のハンドリング
     * - マップからActivityを削除。
     * - Activityインスタンス開放の為、必須。
     */
    @Override
    public synchronized void onActivityDestroyed(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) return; // Not managed activity.
        aid2activity.put(aid, null);
        activity2aid.remove(activity);
    }
   
   
    @Override
    public void onActivityStarted(Activity activity) {
    }
    @Override
    public void onActivityResumed(Activity activity) {
    }
    @Override
    public void onActivityStopped(Activity activity) {
    }
    @Override
    public void onActivityPaused(Activity activity) {
    }

}

AsyncHelperApplication.java:
package org.kotemaru.android.asyncrotate;

import android.app.Application;

public class AsyncHelperApplication extends Application {
    private ActivityManager activityManager = new ActivityManager();

    @Override
    public void onCreate() {
        // API14 からサポートのActivityのライフサイクルのコールバック設定。
        registerActivityLifecycleCallbacks(activityManager);
    }

    @Override
    public void onTerminate() {
    }

    public ActivityManager getActivityManager() {
        return activityManager;
    }
}
MainActivity.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       
        // 3秒後にメッセージを書き換えるタスクを起動
        TextView m = (TextView) this.findViewById(R.id.message);
        new SlowAsyncTask(this).execute(m.getText()+"{3sec}");
    }
   
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
}

SlowAsyncTask.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.AsyncTask;
import android.widget.TextView;

public class SlowAsyncTask extends AsyncTask {
    private ActivityManager activityManager;
    private String activityId;
   
    public SlowAsyncTask(Activity activity) {
        activityManager = ((AsyncHelperApplication)activity.getApplication()).getActivityManager();
        // Activity の ID を取得して保存。
        activityId = activityManager.getActivityId(activity);
    }
   
    @Override
    protected String doInBackground(String... params) {
        // 時間のかかる非同期処理。
        try {Thread.sleep(3000);} catch (Exception e) { }
        return params[0];
    }

    @Override
    protected void onPostExecute(String result) {
        // Activityは保存しておいたIDから取得する。
        Activity activity = activityManager.getActivity(activityId);
        TextView message = (TextView) activity.findViewById(R.id.message);
        message.setText(result);
    }
}

ソースの解説

とりあえずこれでちゃんと動作している。

API14 からサポートされた ActivityLifecycleCallbacks を利用して Activity の create/destory をフックし入れ替わりを管理している。ID の保存の為に Activity の Bundle を1つ汚染することになるが最小限だろう。

AsyncTask.onPostExecute()が create と destory の間に発生する事を危惧したが起こり得ないようである。onPostExecute() は UI スレッドで呼ばれる事が保証されているので UI の準備の整わない状態で呼ばれないと解釈した。





2013/02/09

VirtualBoxのAndroid 4.0 x86を縦置きにする。

Android-x86-4.4r2 の設定はこちら


VirtualBoxのAndroid4.0 x86をEclipseから使う」 の記事で宿題になっていた縦画面を調べてみた。

画面の回転

画面の回転は[F9]〜[F12]キーの長押しで発生させる事ができる。

キー
方向
F9
F10
F11
F12

但し、VirtualBox は回転したことは知らないのでそのまま。

つまりこういう事になる。


マウスも90度回転するのでモニタを縦にできる場合はこれて解決する。
また、ちゃんと回転イベントが発生しているようなのでテストにもちょうど良い。

普通の縦画面

縦置きできるモニタは常に有るわけでもないので普通に縦画面にできないか調べてみた。 現行のVirtualBox-4.2はモニタの回転をサポートしていない様子。
しかし、オプションで任意のVGA解像度を指定できるらしいのでこれを試す。

1. 仮想環境の設定ファイルをテキストエディタで開いて タグを追加する。
  • ファイルの場所は VirtualBox のディレクトリの "仮想環境名\仮想環境名.xml"
  • このときVirtualBoxは完全に終了させて置くこと。

<ExtraData>
   <ExtraDataItem name="CustomVideoMode1" value="640x800x32"/>
              :
</ExtraData>


2. 起動オプションで追加したVGAモードのコードを確認する。
  • GRUB の起動メニューで 「A」を押すと起動オプションが入力できるので「vga=ask」を追加して[Enter]。

  • するとこのような画面になるのでもう一度 [Enter]。

  • VGAモードの一覧が出るので追加したVGAモードを探してモードのコードをメモる。

3. GRUBの起動メニューに Portrait を追加する。
  • Androidをデバッグモードで起動し GRUB の menu.lst を編集する。
# mount -o remount,rw /mnt
# vi /mnt/grub/menu.lst
  • 起動メニューをコピペして 「vga=モード」 を追加する。
  • モードは 16進->10進 変換が必要。(360 -> 864)

4. 再起動して確認
  • Portrait が増えているはず。

  • 縦画面になれば成功。
  • Android側の orientation の認識も Portrait になります。縦横比で決まるのでしょう。


以上。

2013/01/26

Nexus7にキーボードとマウスを付けてみた。

Nexus7を開発に使っているとコンソールを使う事が多いのだがソフトウエアキーボードだと ls -l とか打つだけでも結構つらいw

というわけでキーボードとマイクロB接続のUSBハブを購入してみた。

その結果こういう事に。


もはや、タブレットの必然性が全く有りませんが入力は全然楽になりました。
マウスを繋ぐとちゃんとカーソルが現れてタッチと併用できます。画面に指紋を付けたく無いとき便利です。

このキーボードは Bluetooth ですが普通のUSBキーボードでも認識します。
っぽいキーボードが欲しかったので合えて買いました。

一見、アップル純正に見えますが中国製のパチ物で作りはチープです。



完全な英語配列である事だけが取りえなのですが一つ落し穴が...



[DEL]キーの位置に意味不明のキーがあります。
PCに繋いだときはブラウザ起動のホットキーになりますがAndroidの時は何も起こりません。
DEL は [Fn]+[BS] に割り振られているのですが利用頻度を考えると全く意味不明です。
これさえ無ければ値段相応の良いキーボードだったんですが (;_;)

追記:Macでは [Fn]+[BS] で DEL になるのが標準なのですね。



その他:
ハブに USBメモリを差せば読み込みはできます。書き込みはrootを取らないと出来ないようです。
商売の都合でしょうがちょっとがっかりです。

USBハブは必ず外部電源付きの物を買いましょう。Nexus7を電源に使っているとあっと言う間に電池が無くなります。




2013/01/20

eclipseにNexus7を繋げてみた

Nexus7を買ったので eclipse と繋げてみたのだが軽くハマったのでメモ。

Nexus7はWin7に繋げるとストレージとして認識される。
但し、eclipse からはそのままだと認識されない。ドライバが必要らしい。

Android実機が eclipse から認識されてアプリを実行するまでの手順。

1. Nexus7をデバッグモードにする。

4.1.2 から[開発者向けオプション]が隠しになったらしく
[設定]->[タブレット情報]の[ビルド番号]を7回タップする必要がある。

情報元

ついでに開発元不明のアプリを有効にする。


2. ドライバのインストール


開発用にSDKのドライバをインストールする必要が有る。
オプションなのでまず SDK Manager から「Google USB Driver」をインストール。


デバイスマネージャからNexus7のドライバを更新。


Android-SDK のフォルダを指定する。



OSにデバイスが認識される。




3. eclipse から実行


DDMSのパースペクティブを開くとNexus7が認識されている。


アプリを実行するとNexus7側にいきなりアプリが起動してくる。



以上。


2012/12/20

VirtualBoxのAndroid4.0 x86をEclipseから使う

速い新PCでAndroidアプリを始めてみただのだがやっぱり開発用エミュレータが重い!
みんなどんなPCで開発やってんだ?

と言うわけで VirtualBox の Android/4.0 x86 を開発用に使えないかと調査したら何とかなったので手順をメモ。


1. VirtualBox の仮想マシン準備


OS: Linux/Other Linux

MEM: 1G (512MでもOK)

VIDEO: 16M

NET(重要): 
 - ホストオンリーアダプタ
 - VirualBox Host-Only Ethernet Adapter
 - ブリッジアダプタでも動作確認。OS側でブリッジ作成不要。
 - PCnet-FAST III
VirualBox Host-Only Ethernet Adapter をブリッジ接続して固定IPを割振っておく



ストレージ:
 - 仮想HDD 1G (SSDなら可変/HDDなら固定)
 - CD はここから android-x86-4.0-RC2-eeepc.iso を落してマウント


2. Android 4.0 のインストール

インストーラにしたがうだけなので省略。
途中 /system の Write 権の設定のみデフォルトで無く Yes を選択する。

CD を抜いて再起動。
再起動したら Debug mode を選択。

コンソール画面にて init.sh を書き換え。
# vi /system/etc/init.sh
一番最後の行をコメントアウトして固定IPの設定を追加。

[ -e /sys/class/net/eth0 ] && start dhcpcd_eth0:eth0
#[ -e /sys/class/net/eth0 ] && start dhcpcd_eth0:eth0
netcfg eth0 up
ifconfig eth0 xxx.xxx.xxx.xxx                         ←固定IP
route add default gw xxx.xxx.xxx.yyy dev eth0    ←ルータIP
setprop net.dns1 xxx.xxx.xxx.zzz                    ←DNS IP
通常モードで再起動。

カーソルが消えちゃう場合は「マウス統合の無効化」を選択。


3. adb の準備

Android が起動している状態でDOS窓から adb を接続。
> c:\<AndroidSDK>\platform-tools\adb connect xxx.xxx.xxx.xxx
Android を再起動した場合は一旦切断。
> c:\<AndroidSDK>\platform-tools\adb disconnect xxx.xxx.xxx.xxx
xxx.xxx.xxx.xxxはAndroidの固定IP。


4. Eclipseからの起動

adb が接続された状態で Eclipse からAndroidアプリの実行を行うとデバイスの選択画面になるので VirtualBox を選択する。


一瞬で VirtualBox 上の Android にアプリが起動する \(^o^)/


5. TODO:

縦画面ってどうすれば良いんだろ?->「VirtualBoxのAndroid 4.0 x86を縦置きにする




参考にさせて頂いたサイト:
- http://d.hatena.ne.jp/goriponsoft/20110212/1297510921
- http://d.hatena.ne.jp/hiratake55/20090107/1231316875



2011/10/27

Android を VirtualBox で動かしてみた

作成中の jQuery Mobile アプリをAndroidで動かしてみた。
開発用のエミュのAndroidは重すぎて話しにならなかったので 以下のページを参考に VirtualBox で動かしてみた。

  • http://d.hatena.ne.jp/OkadaHiroshi/20100507/1273250632
VirtualBox がインストールしてあれば実行は簡単で以下のサイトから ISO ファイルをどれか一つ落してきて VirtualBox にマウントして起動するだけ。

  • http://www.android-x86.org/download
仮想HDDにインストールも出来るが仮想CDDからそのまま起動する事もできる。

Androidのバージョンは現時点(2011/10)で 1.6/2.2/2.3 が存在する。
但し、1.6はDeprecated、2.3はUnstableとなっているので注意。

ハードウェア別に幾つかの ISO があるのだが良く分からないのでとりあえず android-x86-2.2-r2-sparta.iso を落してきた。
一応仮想HDDにインストールして起動したらサクっと起動してきた。

4つボタンが不明だったがいじくり回して
  • 戻る:マウス右ボタン
  • メニュー:マウス中ボタン
  • ホーム:HOMEキー
  • 検索:不明
と判明。 検索は無くてもあんまり困んないからまあいいや。

で、肝心のアプリを実行してみたらブラウザが落ちた orz

jQuery Mobile のサイトでもブラウザが落ちるので android-x86 の問題か jQuery Mobile がブラウザのバグを突いているのだろう。

とりあえず、一旦ペンディングだなー (´・ω・`)ショボーン


2011/07/27

スマホで現在位置を表示するWebページ

今まで使ってなかった Google Map を使って見ようと思って調べてみた。

とりあえず GPS から現在位置を取ってきて地図を表示させてみることに。

スマホは HTML5 とか標準装備なので 数十行のWebページだけで出来てしまった。

Map API ちょっと面白いかもしれない。

サンプルはここ。

  • http://wsjs-gae.appspot.com/test/gps.html
iPhone、Android、PCでも動く(IE除く)。
PCはIPアドレスから計算しているのか地域レベルまで。
GPS無しの Android でも結構正確に取れる。WiMAX から情報を得ているのかもしれない。

ソース:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <meta http-equiv="content-type" content="text/html;charset=utf-8"/> <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no"> <script type="text/javascript"> window.onload = function(){ navigator.geolocation.watchPosition(update); } function update(position){ var lat = position.coords.latitude; var lng = position.coords.longitude; var img = document.getElementById("map"); var url = "http://maps.google.com/maps/api/staticmap" +"?center="+lat+","+lng +"&zoom=15" +"&size=400x400" +"&sensor=false" +"&markers=color:red|"+lat+","+lng ; img.src = url; } </script> </head> <body> <img id="map" src="" /> </body> </html>

2011/05/03

Android & iPhone 共通開発環境 Titanium を試してみた。

知人から Android の開発環境なら Titanium って言うのが有るよ、 と教えてもらったのでちょっと調べてみた。

Titanium は HTML+JavaScript で記述したソースを Android と iPhone の両方のバイナリにコンパイルする事ができる結構凄いやつらしい。

この辺を参考に開発環境を作ってみた。

  • http://www.atmarkit.co.jp/fsmart/articles/titanium01/01.html
  • http://code.google.com/p/titanium-mobile-doc-ja/
  • http://developer.appcelerator.com/
  • http://wiki.appcelerator.org/display/guides/Getting+Started+with+Titanium
Titanium はバイナリ形式でしか供給されていないので FreeBSD でやるのは最初からあきらめて VirtualBox 上の WinXP でやることにした。

とりあえず Win32 版を落してインストールすると問題なくインストールできた。

最初にユーザ登録が必要。

メールアドレスとパスワード、姓、名だけ入力すれは良い。
※VirtualBoxのNICの設定がNATだとうまく行かなかった。Bridgeにしたらうまく行った。

次に Android の開発環境をインストールする。

  • http://developer.android.com/sdk/index.html
インストールしたディレクトリをTitaniumに設定する。

ここで問題発生。

adb.exe が見付からないと言っている。
adb.exe は最近のバージョンで tools/ から platform-tool/ に 移動されているので1つ前のバージョンに戻したが 今度は package の update ができない。

いろいろ悩んだあげく GettingStart を良く読んだら書いてあった。

Titanium Developer expects the adb executable to be in the same location, 
ie $ANDROID_SDK/tools, as the Android SDK and AVD Manager (android executable),
but Google has recently moved it to $ANDROID_SDK/platform-tools. 
Thus, it is necessary to create a symbolic link in $ANDROID_SDK/tools 
that references the new location.

For Linux, create the symbolic link as follows:
For Windows, you must create a symbolic link for adb.exe and its associated AdbWinApi.dll:

adb.exe と DLL を tools/ にコピーしたらあっさり解決。
これ Titanium 側をちょこっと直せば済む話しなんじゃねーの?
絶対はまるだろ。

サンプルプログラムをインポートして実行するも途中でエラーになり失敗する。


根が深そうなので一旦、諦め新規作成したプロジェクトを実行してみる。

  • ProjectType を Mobile にする。
  • AppId はドットを含むパッケージ名になっていないとコンパイル時におかしくなる。 参考:http://d.hatena.ne.jp/siso9to/20110404/1301933484
  • Name,Dir,URLは適当に。
実行してみる。

Android のバージョンを選んで Launch をクリックするとエミュレータが立ち上がって インストールまでやってくれる。

おぉ一応、初期画面が出て来た。

タブを2つ出している初期状態のプログラムはこうなっていた。 // this sets the background color of the master UIView (when there are no windows/tab groups on it) Titanium.UI.setBackgroundColor('#000'); // create tab group var tabGroup = Titanium.UI.createTabGroup(); // // create base UI tab and root window // var win1 = Titanium.UI.createWindow({ title:'Tab 1', backgroundColor:'#fff' }); var tab1 = Titanium.UI.createTab({ icon:'KS_nav_views.png', title:'Tab 1', window:win1 }); var label1 = .createLabel({ color:'#999', text:'I am Window 1', font:{fontSize:20,fontFamily:'Helvetica Neue'}, textAlign:'center', width:'auto' }); win1.add(label1); // // create controls tab and root window // var win2 = Titanium.UI.createWindow({ title:'Tab 2', backgroundColor:'#fff' }); var tab2 = Titanium.UI.createTab({ icon:'KS_nav_ui.png', title:'Tab 2', window:win2 }); var label2 = Titanium.UI.createLabel({ color:'#999', text:'I am Window 2', font:{fontSize:20,fontFamily:'Helvetica Neue'}, textAlign:'center', width:'auto' }); win2.add(label2); // // add tabs // tabGroup.addTab(tab1); tabGroup.addTab(tab2); // open tab group tabGroup.open();

Titanium.UI の API を理解すればそれなりにアプリが作れそうな 感じではある。
但し、この環境ではソース修正からエミュレータに画面が現れるまで 1分くらいかかってしまうので実際の開発に使うのはかなり厳しそう。


プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【android】

リンク

アーカイブ