2013/08/11

Nexus7のカメラでQRコードを読込んでみた

Androidのカメラアプリを作ってみようと思ったのだが Nexus7 にはフロントカメラしか無い。orz
QRコード・リーダーぐらいならなんとかなるかと思い手を付けてみた。

カメラの準備

普通は以下のようにするとカメラが準備できるのだが Nexus7 だとリアカメラを開こうとして失敗する。

Camera camera = Camera.open();

Nexus7 のフロントカメラを開くにはこうする。

int cameraId = 0;
Camera camera = Camera.open(cameraId);

cameraId はハードのカメラの実装状況によりまちまちなので正しくはこんなことする必要が有りそう。

Camera.CameraInfo info = new Camera.CameraInfo();
for (int id=0; id<Camera.getNumberOfCameras(); id++) {
    Camera.getCameraInfo(id, info);
    if (info.??? == ???) { // 使いたいカメラの条件式
        cameraId = id;
    }
}

AnsroidManufest.xmlにカメラのパーミッションも必要。

<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<!--
<uses-feature android:name="android.hardware.camera.front" android:required="true"/>
-->

android.hardware.camera.front は無くても良いらしい...

プレビュー

SurfaceView を使う。 ググれば解説多数なので割愛。

QRコードのライブラリ

QRコードのライブラリは ZXing を使う。 google が開発しオープン化されている物。

ここから落せる。

しかし、現行の ZXing-2.2.zip にはソースしか入っていないので以下から2.3-SNAPのjarを落してきた。

  • https://oss.sonatype.org/content/repositories/snapshots/com/google/zxing/

必要なものは core-xxxxx.jar と javase-xxxxxx.jar のみ。
eclipse の Android プロジェクトの libs に入れておけば利用可能。

使い方はサンプル・コードの onPreviewFrame() 内で完結している。

注意するのはフロントカメラは左右反転しているので PlanarYUVLuminanceSource コンストラクタの最後の引数が ture になっている必要がある。

サンプル・コード

このソースだけ。リソースも参照していない。

package org.kotemaru.sample.camera;

import java.io.IOException;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;

import android.app.Activity;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.Toast;

public class MainActivity extends Activity {
    private static final String TAG = "CameraSample";

    private int CAMERA_ID = 0; // for Nexus7

    private Camera camera;
    private SurfaceView surfaceView;
    private CameraListener cameraListener = new CameraListener();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        surfaceView = new SurfaceView(this);
        setContentView(surfaceView);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        SurfaceHolder holder = surfaceView.getHolder();
        holder.addCallback(cameraListener);
    }

    private int getOrientation() {
        return getResources().getConfiguration().orientation;
    }

    private class CameraListener implements
            SurfaceHolder.Callback,
            AutoFocusCallback,
            Camera.PictureCallback,
            Camera.PreviewCallback
    {

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            camera = Camera.open(CAMERA_ID);
            try {
                camera.setPreviewDisplay(holder);
            } catch (IOException e) {
                Log.e(TAG, e.toString(), e);
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format,
                int width, int height) {

            // カメラのプレビューサイズをViewに設定
            Camera.Parameters parameters = camera.getParameters();
            Camera.Size size = parameters.getSupportedPreviewSizes().get(0); // 0=最大サイズ 
            parameters.setPreviewSize(size.width, size.height);
            camera.setParameters(parameters);

            // 画面回転補正。
            ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams();
            if (getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
                camera.setDisplayOrientation(90);
                layoutParams.width = size.height;
                layoutParams.height = size.width;
            } else {
                camera.setDisplayOrientation(0);
                layoutParams.width = size.width;
                layoutParams.height = size.height;
            }
            surfaceView.setLayoutParams(layoutParams);

            // オートフォーカス設定。
            camera.autoFocus(cameraListener);

            camera.startPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            camera.autoFocus(null);
            camera.setPreviewCallback(null);
            camera.release();
            camera = null;
        }

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            if (success) {
                Log.d(TAG, "focus");
                // プレビューのデータ取得。
                camera.setPreviewCallback(cameraListener);
                // フルサイズ画像はTODO
                //camera.takePicture(null,null,cameraListener);
            }
        }

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            // フルサイズ画像もやることは同じ。
            onPreviewFrame(data, camera);
        }

        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            // 処理は1回なのでコールバック取り消し
            camera.setPreviewCallback(null);

            // 基礎データ取得
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(CAMERA_ID, info);
            int w = camera.getParameters().getPreviewSize().width;
            int h = camera.getParameters().getPreviewSize().height;
            //int w = camera.getParameters().getPictureSize().width;
            //int h = camera.getParameters().getPictureSize().height;
            boolean isMirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);

            // プレビュー画像の型変換
            PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
                    data, w, h, 0, 0, w, h, isMirror);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

            // QRコード読み込み。
            Reader reader = new MultiFormatReader();
            try {
                Log.d(TAG, "decode");
                Result result = reader.decode(bitmap);
                String text = result.getText();

                Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG).show();
                Log.i(TAG, "result:" + text);
                camera.stopPreview();
                camera.autoFocus(null);
            } catch (Exception e) {
                // QRコード認識失敗でも例外発生する。
                Log.d(TAG, "decode-fail:" + e.toString());
                camera.autoFocus(cameraListener);
            }
        }
    };
}

実行結果

さすがにエミュレータでは動かないので実機で確認。

一応、QRコードを認識する事には成功した。 但し、QRコード自体をかなり拡大しないと認識できない。 小さいとカメラを非常に近づける必要があるためピントが合わないのかもしれない。

それ以前にプレビューを見るのに横からのぞき込まないといけないと言う致命的欠陥が...
(最初から分かっていた訳だがw)

技術的に可能だけど使い物にならないと言う予想通りの結論に達したのでこれで終りにしよう。



参考にさせて頂いたサイト:


2013/07/29

AndroidのPush通知(GCM)のサンプルを書いてみた

この記事の内容は古くなっています=>「GCMのサンプル再び」

Android でも Push メッセージが使えると言うので試して見ようと思ったら Google のサンプルが分り辛くて結局自分でサンプルを書いてしまった。

Push通知の仕組み

Androidはサーバ側から端末側へ通信を始める方法は無いので Push 通知できません。
Googleが取った解決方法は端末側からGoogleの専用サーバへの TCP セッションの繋ぎっぱなしです。
Push通知したいアプリは Google のサーバを経由してメッセージを送信します。

大まかな仕組みはこんな感じです。

  • (1) アプリ起動後、GCMサーバから 端末ID を取得します。
  • (2) ユーザID端末ID のペアをアプリサーバに送信してDBに保存します。
  • (3) 目的の ユーザ に向けた メッセージ をアプリサーバに送信します。
  • (4) アプリサーバは ユーザID から 端末ID を引いてGCMサーバにメッセージを転送します。
  • (5) GCMサーバは繋ぎっぱなしのセッションに メッセージ を送信します。

前提条件の注意点。

  • 端末は Google アカウントが登録されている必要があります。
  • ポート 5228、5229、5230 を使用するのでファイヤウォール等で開いている必要が有ります。

公式な文書は以下にあります。

準備

サーバ側

Googleアプリの管理コンソールにログインして 「Google Cloud Messaging for Android」サービスを有効にします。

GCMを利用するに当って必要な情報が幾つかあります。 いずれも Googleアプリの管理コンソール から取得できます。

  • API_KEY : アプリケーション・サーバがGCMサーバに接続する時の認証キーです。
    「APIAccess」の「create new server key」で生成できます。
  • SENDER_ID : 端末アプリがGCMサーバに接続する時のアプリのIDです。
    管理コンソールでは Product Number となっています。

管理コンソールに不慣れな方はこちらのサイトが詳しいです。

クライアント側

eclipse から Android SDK Manager を起動して Extras の 「GoogleCloudMessaging for Android Library」 をインストールします。

${android.sdk}/extras/google/ 配下に必要な jar とデモが入っています。

サンプル

注意: このサンプルは機能の理解を目的としている為、必要最小限の機能に絞ってあります。 Googleの作法から外れている場合も有りますので公式文書も読んで下さい。

サーバ側

サーバに必要なクラスはServlet 1つだけです。

  • 先に取得した API_KEY が必要になります。(ソースを書き換えて下さい。)
  • GCMサーバへの送信は com.google.android.gcm.server.Sender クラスを利用すれば非常に簡単です。
  • 必要な jar ファイルは以下です。
    • ${android.sdk}/extras/google/gcm/gcm-server/dist/gcm-server.jar
    • ${android.sdk}/extras/google/gcm/gcm-server/lib/*.jar
GCMServerSampleServlet.java:
package org.kotemaru.sample.gcm.server;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;


/**
 * GCMのサーバ・サンプル・サーブレット
 * - API
 * -- ?action=register&userId={ユーザID}&regId={端末ID}
 * -- ?action=unregister&userId={ユーザID}
 * -- ?action=send&userId={ユーザID}&mes={送信メッセージ}
 * 
 * 注:いろいろ端折ってます。Googleのサンプルも参照してください。
 * @author @kotemaru.org
 */

public class GCMServerSampleServlet extends HttpServlet {

    /**
     * https://code.google.com/apis/console/ で生成したAPIキー。
     */
    private static final String API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    private static final int RETRY_COUNT = 5;

    /**
     * ユーザIDからRegistrationIdを引くテーブル。
     * -本来はストレージに保存すべき情報。
     * -key=ユーザID: サービスの管理するID。
     * -value=RegistrationId: AndroidがGCMから取得した端末ID。
     */
    static Map<String,String> deviceMap = new HashMap<String,String>();

    public void doGet(HttpServletRequest req, HttpServletResponse res) 
            throws IOException {

        System.out.println("=> "+req.getQueryString());

        String action         = req.getParameter("action");
        String registrationId = req.getParameter("regId");
        String userId         = req.getParameter("userId");
        String msg            = req.getParameter("msg");

        if ("register".equals(action)) {
            // 端末登録、Androidから呼ばれる。
            deviceMap.put(userId, registrationId);

        } else if ("unregister".equals(action)) {
            // 端末登録解除、Androidから呼ばれる。
            deviceMap.remove(userId);

        } else if ("send".equals(action)) {
            // メッセージ送信。任意の送信アプリから呼ばれる。

            registrationId = deviceMap.get(userId);
            Sender sender = new Sender(API_KEY);
            Message message = new Message.Builder().addData("msg", msg).build();
            Result result = sender.send(message, registrationId, RETRY_COUNT);

            res.setContentType("text/plain");
            res.getWriter().println("Result="+result);
        } else if ("sendAll".equals(action)) {
            // TODO: 省略。googleのサンプル参照。
        } else {
            res.setStatus(500);
        }
    }
}


クライアント側

サーバに必要なクラスは MainActivity と GCMBaseIntentServiceの実装クラスの2つです。

  • 先に取得した SENDER_ID が必要になります。(ソースを書き換えて下さい。)
  • アプリサーバへの送信は通常の HttpClient で行います。
    (URLはソースを書き換えて下さい。)
  • 受け取ったPush通知はログとトーストに出力しています。
  • AndroidManifest はサンプルからパッケージ名を変更しただけです。
  • 必要な jar ファイルは以下です。
    • ${android.sdk}/extras/google/gcm/gcm-client/dist/gcm-client.jar
MainActivity.java:
package org.kotemaru.sample.gcm.client;

import android.app.Activity;
import android.os.Bundle;

import com.google.android.gcm.GCMRegistrar;

/**
 * クライアントアプリ本体。
 * @author @kotemaru.org
 */
public class MainActivity extends Activity {
    /**
     * https://code.google.com/apis/console/のProject Number。
     */
    public static final String SENDER_ID = "nnnnnnnnnnn";

    /**
     * アプリサーバーのURL。
     */
    public static final String SERVER_URL = "http://192.168.0.3:8888/gcmserversample";
    /**
     * アプリのユーザID。本来はログイン中のユーザとかになるはず。
     */
    public static final String USER_ID = "TarouYamada";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final String regId = GCMRegistrar.getRegistrationId(this);
        if (regId.equals("")) {
            // GCMへ端末登録。登録後、GCMIntentService.onRegistered()が呼ばれる。
            GCMRegistrar.register(this, SENDER_ID);
        } else {
            // 登録済みの場合、ここではアプリに登録しなおしているが
            // Googleのサンプルでは unregister して register しなおしている。
            String uri = SERVER_URL+"?action=register"
                    +"&userId="+USER_ID
                    +"&regId="+regId;
            Util.doGetAsync(uri);
        }
    }

    @Override
    protected void onDestroy() {
        GCMRegistrar.onDestroy(this);
        super.onDestroy();
    }

}
GCMIntentService.java:

注意事項:クラス名は「GCMIntentService」に固定です。 クラス名が異なるとレシーバがサービスを起動できなくなりハマります。

package org.kotemaru.sample.gcm.client;

import static org.kotemaru.sample.gcm.client.MainActivity.*;

import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import com.google.android.gcm.GCMBaseIntentService;
import com.google.android.gcm.GCMRegistrar;

/**
 * Push通知受け取りサービス。
 * @author @kotemaru.org
 */
public class GCMIntentService extends GCMBaseIntentService {

    private static final String TAG = "GCMIntentService";

    private Handler toaster;

    public GCMIntentService() {
        super(SENDER_ID);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        toaster = new Handler();
    }

    @Override
    protected void onRegistered(Context context, String registrationId) {
        Log.i(TAG, "onRegistered: regId = " + registrationId);
        // GCMから発行された端末IDをアプリサーバに登録する。
        String uri = SERVER_URL + "?action=register"
                + "&userId=" + USER_ID
                + "&regId=" + registrationId;
        Util.doGet(uri);
    }

    @Override
    protected void onMessage(Context context, Intent intent) {
        // アプリサーバから送信されたPushメッセージの受信。
        // Message.data が Intent.extra になるらしい。
        CharSequence msg = intent.getCharSequenceExtra("msg");
        Log.i(TAG, "onMessage: msg = " + msg);
        toast("Push message: " + msg);
    }



    @Override
    protected void onUnregistered(Context context, String registrationId) {
        Log.i(TAG, "onUnregistered: regId = " + registrationId);
        if (GCMRegistrar.isRegisteredOnServer(context)) {
            String uri = SERVER_URL + "?action=unregister"
                    + "&userId=" + USER_ID;
            Util.doGet(uri);
        } else {
            Log.i(TAG, "onUnregistered: ignore");
        }
    }

    @Override
    protected void onDeletedMessages(Context context, int total) {
        Log.i(TAG, "onDeletedMessages total="+total);
        toast("onDeletedMessages: " + total);
    }

    @Override
    public void onError(Context context, String errorId) {
        Log.i(TAG, "onError: " + errorId);
        toast("onError: " + errorId);
    }

    @Override
    protected boolean onRecoverableError(Context context, String errorId) {
        Log.i(TAG, "onRecoverableError: " + errorId);
        toast("onRecoverableError: " + errorId);
        return super.onRecoverableError(context, errorId);
    }


    private void toast(final String msg) {
        toaster.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(GCMIntentService.this, msg, Toast.LENGTH_LONG).show();
            }
        });
    }

}
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.sample.gcm.client"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <permission
        android:name="org.kotemaru.sample.gcm.client.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <uses-permission
        android:name="org.kotemaru.sample.gcm.client.permission.C2D_MESSAGE" />

    <uses-permission
        android:name="com.google.android.c2dm.permission.RECEIVE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="org.kotemaru.sample.gcm.client.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name="com.google.android.gcm.GCMBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <!-- Receives the actual messages. -->
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <!-- Receives the registration id. -->
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="org.kotemaru.sample.gcm.client" />
            </intent-filter>
        </receiver>
        <service android:name=".GCMIntentService" />

    </application>

</manifest>

実行結果

サーバを起動してからクライアントを起動します。

PCから以下の URI をブラウザで開きます。

http://アプリサーバ/gcmserversample?action=send&userId=ユーザID&msg=Hello%20Android!

Androidでトーストが無事表示されました。

  

ダウンロード

このサンプルの eclipse プロジェクトは SVN から落せます。

クライアント:
サーバ:

サーバはGAE用の環境です。

コンパイル前に以下の手順が必要です。

  1. プロジェクトのメニューから「プロパティ」->「Google」->「App Engine」 で App Engine の設定。
  2. lib/*.jar を war/WEB-INF/lib/ にコピー。
  3. プロジェクトのメニューから「実行」->「実行の構成」で GAE の引数 に -a 0.0.0.0 を追加。

所感

何億台あるか分からない Android が同時に接続しても大丈夫なサーバを用意できるのはさすが Google と言った所でしょうか。

最初はデモのソースに余計な物が多くて訳が分からんかったのですが整理したら割とシンプルになりました。

スリープ時の挙動とか電池の消費具合とかまだ色々しらべる必要が有りそうだけど できる事の幅が広がるので何か面白い使い道を考えたい所。

終り。


2013/06/29

Nexus7を外部モニタ化 (2)

Nexus7を外部モニタ化した時に邪魔だったVNCクライアントのナビゲーションバー消せないか調べてみた。

結論から言うとAndroid/4.1で追加されたAPIを使えばアプリ単位で消せる。
OS全体で消そうとするとroot権が必要になる。

前回試したアプリの中で androidVNC がソース公開されていたのでこれに 自前のパッチを当てて試したところ 800x1280 の完全な全画面モードでVNCを使う事ができた。

終了。

じゃあ、野良ビルドでも公開しようかなと思ったらここで問題が。
apkを作ってアプリを実機にインストールしようても 「×アプリはインストールされません。」のメッセージが出る。

eclipseからデバッグモードでのインストールは問題無い。

うーん、一度同じアプリをGooglePlayからインストールしているから 正規の証明書が残ってるって事だろうか?
これ以上の情報が無く対処しようがないので野良ビルドの公開は諦めとします。

=> 出来ました Nexus7を外部モニタ化 (3)

...

せっかくなのでビルド手順はメモして置きます。

androidVNCのSVNからプロジェクトを2つチェックアウトします。

  • http://android-vnc-viewer.googlecode.com/svn/trunk/eclipse_projects/
    • androidVNC
    • ZoomerWithKeys

trunkの日付が 2011年になっているのでこのプロジェクトは活動停止中っぽいです。

ビルドには sqlitegen というプラグインが必要です。 (専用プラグインのような気がします。)
以下のページの案内にしたがってインストールしてください。

以下のパッチを当てます。

Index: AndroidManifest.xml
===================================================================
--- AndroidManifest.xml (revision 204)
+++ AndroidManifest.xml (working copy)
@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.androidVNC" android:installLocation="auto" android:versionCode="13" android:versionName="0.5.0">
+    package="android.androidVNC" android:installLocation="auto" android:versionCode="14" android:versionName="0.5.0">
     <application android:icon="@drawable/icon" android:label="androidVNC" android:debuggable="false">
-       <activity android:label="@string/app_name" android:name="androidVNC" android:screenOrientation="landscape">
+       <activity android:label="@string/app_name" android:name="androidVNC" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN"></action>
                <category android:name="android.intent.category.LAUNCHER"></category>
            </intent-filter>
        </activity>
-       <activity android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden" android:name="VncCanvasActivity">
+       <activity  android:configChanges="orientation|keyboardHidden" android:name="VncCanvasActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"></action>
            </intent-filter>
Index: src/android/androidVNC/VncCanvasActivity.java
===================================================================
--- src/android/androidVNC/VncCanvasActivity.java   (revision 204)
+++ src/android/androidVNC/VncCanvasActivity.java   (working copy)
@@ -39,6 +39,7 @@
 import android.graphics.PointF;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -607,6 +608,23 @@

        vncCanvas = (VncCanvas) findViewById(R.id.vnc_canvas);
        zoomer = (ZoomControls) findViewById(R.id.zoomer);
+       
+       vncCanvas.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener(){
+           @Override
+           public void onSystemUiVisibilityChange(int visibility) {
+               if (visibility == View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+               
+               Handler handler = new Handler();
+               handler.postDelayed(new Runnable() {
+                  @Override
+                  public void run() {
+                       vncCanvas.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
+                   }
+               }, 3000);
+           }
+       });
+       vncCanvas.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
+

        vncCanvas.initializeVncCanvas(connection, new Runnable() {
            public void run() {

プロジェクトの「プロパティ」から「Android」を選んでターゲットを 4.2.2 にします。

これでビルドできるはずです。
尚、「自動でビルド」のチェックは何故か必須です。


2013/06/23

Nexus7を外部モニタ化

Nexus7をPCの外部モニタにできないかちっと調べてみた。

幾つかアプリがあるようだが以下はPC側にも専用アプリを入れなといけないようなのでパス。

後は普通にVNCクライアントが複数あるので無料の物だけ試してみた。

androidVNC が縦画面未対応な以外は普通のVNC Viewerなのでどれも似たりよったり。

VNCをセカンドモニタとして使う方法はこちらを参照してください。-> 解像度の違うモニタで VNC 表示させる

こんな感じになりました。(PCは11インチです)

画面が小さいだけで解像度は高いので表示は破綻しません。

無線LAN接続でOfficeの編集作業にストレスは感じません。
さすがに動画はカクカクです。
一応、タップは左クリックとして反応します。


しかし、問題点が1つ...
画面下部のナビゲーションバーを消せるアプリが無く解像度が中途半端になります。

  • 横画面: 1280x736
  • 縦画面: 800x1205

PC側ではこんな解像度は設定できません。
横画面は 1280x720 が有ったのでまあ良いのですが縦画面で収まるのは 768x1024 となってしまい、いまいちです。

Nexus7に依存する問題なので今後、各アプリが対応するかは不明な感じです。 => 自力対応しましたNexus7を外部モニタ化 (2)


2013/06/01

ListViewのFooterでちょいとはまったのでメモ

Android で ListView にカスタムの Adapter と Footer を併用しようとすると例外が出る。忘れそうなのでメモ。

普通にこんな感じの事をして一覧に footer を付ける。

View footerView = activity.getLayoutInflater().inflate(R.layout.list_footer, null);
listView.addFooterView(footerView);
listView.setAdapter(new MyAdapter());

この後、adapter を取り出すと例外になる。

MyAdapter adapter = (MyAdapter) listView.getAdapter();

      ↓

java.lang.ClassCastException: android.widget.HeaderViewListAdapter cannot be cast to MyAdapter

理由は ListView.setAdapter() の JavaDoc に書いてある通り、 Header/Footerを使うと Adapter がラップされるから。
#addFooter() が setAdapter() に前に必要なのはこのため。

後付でHeader/Footerが必要になった場合は有りがちな状況に思える。
とりあえずの回避方法はこれ。

MyAdapter adapter = (MyAdapter) ((WrapperListAdapter)listView.getAdapter()).getWrappedAdapter();

とりだした adapter は普通に操作して問題無い。


2013/05/12

RaspberryPiとはなんぞや

今、一緒に仕事をしている人から RaspberryPi なる物が有ることを聞いた。 ->買いました

大雑把に言うと前に紹介したスティック型アンドロイドの普通の Linux 版。

スペックは ARM11/700MHz、Mem/512M で中華Androidよりは一段下だが なんとお値段 3000円。

学習用なのでOS等全てオープンになっていて玩具としては最適だし、 なんと言っても不良品の不安が無い。

こちらの方が詳しくレビューしてくれていてフルHDの動画再生も行けるらしい。

アマゾンでも売ってるんだが何で直販より高いんだろ?


2013/03/24

AndroidでWebServerを動かしてみた。

Android でも普通に ServerSocket が使えると言う話を小耳に挟んだので調べて見ると既に jetty が普通に動いているらしい。

これをインストールして試してもあんまり面白く無いので 自前で簡易WebServerを作ってみた。

ソース:

package org.kotemaru.android.webserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends Activity {
    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            new AcceptThread(8080).start();
        } catch (IOException e) {
            postMessage(e.getMessage());
            Log.e("boot",e.getMessage());
        }
    }

    void postMessage(final String msg) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
        });
    }

    private class AcceptThread extends Thread {
        private ServerSocket ssock;

        public AcceptThread(int port) throws IOException {
            this.ssock = new ServerSocket(port);
        }

        @Override
        public void run() {
            try {
                postMessage("Server start");
                while (true) {
                    Socket sock = ssock.accept();
                    new ConnectThread(sock).start();
                }
            } catch (IOException e) {
                postMessage(e.getMessage());
                Log.e("AcceptThread",e.getMessage());
            }
        }
    }

    private class ConnectThread extends Thread {
        private Socket sock;

        public ConnectThread(Socket sock) {
            this.sock = sock;
        }
        @Override
        public void run() {
            try {
                Log.i("ConnectThread","From "+sock.getRemoteSocketAddress());

                BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
                BufferedOutputStream out = new BufferedOutputStream(sock.getOutputStream());

                String reqLine = in.readLine();
                String line = in.readLine();
                while (!line.isEmpty()) {
                    line = in.readLine(); // ヘッダーは無視 (^^;
                }

                String[] parts = reqLine.split(" ");
                String path = parts[1].replaceFirst("[?].*$", "");
                File docroot = new File(Environment.getExternalStorageDirectory(),"docroot");

                out.write("HTTP/1.0 200 OK\r\n".getBytes());
                out.write(("Content-type: "+getCType(path)+"\r\n\r\n").getBytes());
                InputStream fin = new FileInputStream(new File(docroot,path));
                try {
                    byte[] buff = new byte[1024];
                    int n;
                    while ((n=fin.read(buff))>0) {
                        out.write(buff,0,n);
                    }
                    out.flush();
                    out.close();
                } finally {
                    fin.close();
                }
            } catch (IOException e) {
                postMessage(e.getMessage());
                Log.e("ConnectThread",e.getMessage());
            } finally {
                try {
                    sock.close();
                } catch (IOException e) {
                    postMessage(e.getMessage());
                    Log.e("ConnectThread",e.getMessage());
                }
            }
        }
        private String getCType(String path) {
            path = path.toLowerCase();
            if (path.endsWith(".html")) return "text/html";
            if (path.endsWith(".jpg")) return "image/jpeg";
            if (path.endsWith(".png")) return "image/png";
            if (path.endsWith(".gif")) return "image/gif";
            if (path.endsWith(".js")) return "application/javascript";
            if (path.endsWith(".css")) return "text/css";
            return "unknown";
        }

    }
}

起動が Activity から行われている事以外はほんとにただの WebServer の実装。

DocumentRoot は内部ストレージの「docroot/」としたので コンテンツは Nexus7 をUSBで繋いでPCからコピペすればOK。

Wifi設定で Nexus7 のIPアドレスを調べてブラウザから直接URLを叩くとちゃんと表示された!

尚、AndroidManifest.xml には以下の設定が必要。

<uses-permission android:name="android.permission.INTERNET" />

但し、これを設定しても port=80 のサーバはパーミッションエラーとなる。 たぶん、root権が必要。

...

モバイル用途の端末だとサーバ化はあんまり意味無いっていうかセキュリティ上の問題がありそうだけど、 スティック型Androidの使い道は色々広がりそう。

安定動作しそうな スティック型Android 出ないかなぁ


2013/03/16

スティック型 Android が欲すい...

久々に物欲を刺激する物を発見。

スティック型アンドロイド。

要は通常の Android から液晶を外して変わりに HDMI ポートを付けたもの。
値段も1万未満と玩具として手頃。

Nexus7 は root 取らないと以外に遊べないのだが 普段使いのメールとかを登録してあるので root は取りたく無い。

で、安い Android 無いかなと思って探したらこれが出て来た。

ただ、基本 中華Android なので品質にかなり問題が有る様子...
アマゾンのコメントにはどの商品にもこんなコメントが付いてます。

WiFi設定をして、アカウント登録をして、GooglePlayをクリックしたら、、、画面が緑と黒の横縞になってフリーズ。 せめてYouTubeでもと、アプリをクリックするとシャットダウン。 初期不良でしょうか? 交換を希望しましたができないらしい。対応は返品のみ。 再度購入する勇気はないのでさようならです。

とか

起動しても何も反応がなくすぐに電源が落ちてしまいます最悪です絶対に買わないほうがいいです

とか

オンキョーのが有るので見てみたがこいつは別の意味で評判が悪い。

まあ、ダメ元で買っても良い値段ではあるのだけど既に文珍化した中華Pad を1台所持している身としては買いづらい。

ASCIIの記事 にメーカー製も発売される見たいな事が書いてあるから様子見かなぁ


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

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【android】

リンク

アーカイブ