KeyHoleTV開発者のブログ

日々の質問や開発の日記

Android, WindowsCE KeyHoleTV

開発の動機

WindowsCE (Windows Mobile 6.0)の端末(iPaq)が手元にあって、それでKeyHoleTVを動作させていた。 iOS用のKeyHoleTVをアップデートして、GUIを Android 、iPhoneで同じようなインタフェースにしたので、WindowsCEでも同様なインターフェースにするようにした。

f:id:KeyHoleTV:20181208044315p:plain f:id:KeyHoleTV:20181208044845p:plain f:id:KeyHoleTV:20181208045048p:plain f:id:KeyHoleTV:20181208045123p:plain

   Windows CE KeyHoleTV      Android KeyHoleTV

 共有GUIライブラリ

WindowsCEのGUIには、Android と同じGUIのライブラリを利用している。 このライブラリは、 Androidでは、OpenGL ES 2.0 を使い、Linuxでは、 OpenGL 2.1/3.0, Windows CEでは、フレームバッファに直接描画するようになっている。 この下層部分だけをOSにより切り分ければ、GUIの部品例えば、ボタンや、インプットバーなどは、全てのプラットフォームで、共通に使える。 更に、GUIライブラリがLinuxで動作できることから、Android, Windows CEで動作するKeyHoleTVが同じソースコードで、Linux上でも動作するようになる。 アプリやGUIライブラリのデバックも、Linux上できるためGDB が利用可能になる。 また、fprintf を入れることで、アプリやライブラリの動作のデバックが容易に行える。 

例えば、Android の場合、 Android Studio を利用して、シミュレータでデバックできるが、とにかく遅い。 起動するの時間がかかるし、アプリの実行にも時間がかかる。 LinuxでGUIライブラリを利用すると、 make してから、実行まで、数秒もかからずできてしまう。 

GUIライブラリや、GUIを持ったアプリのデバックでは、デバック対象に移行するまで、なんらかの操作を行う。 Android Studio や XCode, Visual Studio には、イベントを自動発生させて、デバックするような機構が用意されている。 しかし、イベントを自動発生させる処理を記述するにも、時間がかかる。 特に、アプリが不具合で停止しないで、挙動がおかしい場合には、その発見が結構大変で、何度もコンパイル・実行を繰り返す必要がある。 この時、実行までの時間が短いと、試行錯誤で、繰り替えしてデバックできる。

 

f:id:KeyHoleTV:20181208051640p:plain f:id:KeyHoleTV:20181208051750p:plain f:id:KeyHoleTV:20181208051957p:plain f:id:KeyHoleTV:20181208052036p:plain

  CE 用Debug Linux 240x320 GUI          Android 用 Debug Linux 400x700 GUI

Androidでのデバイス回転の不具合

Android KeyHoleTVで、デバイスを回転(横にしたり、縦にしたりする)すると、アプリが停止する不具合があった。 ログを出力するようにしたり、アプリの異常停止のログをみても、原因がわからない。 アプリに終了イベントが送られ、再び起動する処理になっている。 Androidの場合、ホームボタンを押し、すぐさまアプリの起動をしているように見える。 

原因は、マニフェストにあった。 Androidアプリは、マニフェストに、どのような動作をするのかを記述する。 今までは、Activityに

<activity

....

   android:configChanges="orientation|keyboardHidden”/>

と記述していた。 これで、デバイス回転時には、終了のイベントが発生しないで、連続してアプリが処理できた。 11月1日より、Googleから、Android アプリは、 SDKレベルを28以上にしないと、Play Store に載せられないとの要請がきたので、KeyHoleTVのSDKレベルを

<uses-sdk android:targetSdkVersion="28" android:minSdkVersion="9" />

とした。 この結果、 Activity を規程するマニフェストの android:configChanges が

"orientation|keyboardHidden" だけでは、一旦アプリが終了して、Activity を起動するような設定になっていたようだ。 これが分かるまで、結構な時間がかかった。 

targetSdkVersion="28" にすると、 configChangesは、

android:configChanges="orientation|keyboardHidden|screenSize"

としないと、回転後に終了イベントが送られる。

さて、マニフェストは、XMLで記述する。 XMLは、タグと属性は、任意に記述できて、それを解釈する機構に依存する。 このconfigChangesの属性の値を考えた人の設計が誤っているとしか思えない。 SDKのバージョンを変えると、configChangesの意味が変わるような設計は、正しいとは思えない。 それなら、回転に関する属性を設けて、その動作をYES/NOで表した方が、明解になる。

更に、Native-Activity で構築しているアプリケーションの多くは、OpenGL ESを使っている。 OpenGL ESで利用するオブジェクトは、Windowに依存する。 Window は、Activityに付随している。 すなわち、Activity が終了すると、Windowもなくなり、それに依存しているOpenGLのオブジェクトも利用できなくなる。

さて、OpenGLのオブジェクトは、アプリのプロセスが完全に終了するまで、メモリが確保されたままになる。たとえ、アプリがOpenGLESの関数を使って開放しても、メモリ自身は残ってしまう。 すなわち、 Activity が終了して、再びActivityが作られるような構造では、回転するたびに、OpenGL ESで利用するオブジェクトのメモリ消費を加算してしまう。 Javaで記述したアプリでもOpenGL ESを利用したアプリでは、同様な問題が発生するかもしれない。

WindowsCEで、ナビゲーションボタンの操作(実機と、シミュレータの違い)

WindowsCEには、ナビゲーションボタンがついている。 ナビゲーションボタンは、画面の下に、上、下、右横、左横、真ん中に押せるボタンがある。 画面をタップしたり、ナビゲーションボタンで、アプリを操作できるようになっている。

f:id:KeyHoleTV:20181209142519p:plain

ナビゲーションボタン

KeyHoleTVもナビゲーションボタンやキーボードで、操作できるように設計してあり、ボタン等に下線が引かれているものや、表示が濃くなっているものが現在、選択されている表示オブジェクトである。 選択されている状態で、エンター(リターン)キーをヒットすると選択されている表示オブジェクトがあたかもタップされてたような動作をする。 選択対象は、タブキー(もしくはシフトタブ)で、選択対象が移動する。 これは、Windows,Mac,LinuxのKeyHoleTVと同等な動作になっている。

WindowsCEのKeyHoleTVは、ナビゲーションボタンの左が、アンドロイドのバックボタン、上、下が番組表が表示されているとき、番組選択を移動、右がタブキーに割り当てている。 真ん中のボタンがエンターキーにするようにしている。 シミュレータでは、真ん中ボタンを押した操作が、うまく動作したが、実機(iPaq)では、どうしても動作しない。 さらに、WindowsCEでは、実機でのVisual Studio を使ったデバックができにくいので(おそらく出来ない可能性が高い)、一体どのようなコードが入力されているのかが、分からない。

そこで、GUIライブラリのインプットバーにグリフ(文字の形を表したもの)が定義されていないコード(例えば,HEX 009DやHEX 009Eなどは、グリフがないので、四角が表示される。) に対して、16進数の表示ができるようにした。 なお,ASCIIで定義されているコードに関しては、UNICODEのU+2400 以降にグリフがあるので、それを利用している。 例えば、 U+2400では、

             

と表示される。 グリフがない文字は、16ビットのHEXで表示されるようにしたので、009Eの表示結果は、

f:id:KeyHoleTV:20181209010504p:plain

HEXでの表示

となり、何が入力されいるのかが分かるはず。 で実機でためしたらみたら、 00 0D と表示された。 00 0Dは、CRのはずで、それが表示されなければ、ならない。 で、GUIライブラリのコードを調べてみたら、Control+CRが入力されていることが分かった。 シミュレータでは、CRが入力され、実機では、Control+CRが入力されていた。 

WindowsCEのイベント処理は、Windowにイベントプロシジャーを登録して行う。 イベントプロシージャで、 case WM_KEYDOWN:  で切り分けている。 キーボードで押された全てのキーがコードで、wparam に渡される。 当然、シフトキーも、コントロールキーも押されたとき、このイベントが発生する。 従って、シフトキーを押しながら、英字キーを押して、大文字にする処理は、自分でプログラムを記述する必要がある。 実機では、コントロールキーを押してから、リターンキーが押されているようなイベントが発生していた。

共有GUIライブラリのデバイス依存部分

AndroidやLinuxのOpenGL (AndroidではES)は、テキスチャバッファをライブラリないで保持し、テキスチャに対して、書き込みを行う。 書き込みが終了すると、 テキスチャをGPUに送って、4点のポリゴン(三角形が二つ)にテキスチャを貼り付ける命令をGUPの送っている。 OpenGL ES 2.0では、 Vertex Shader と Fragment Shader を用意した。 

 

const char* vertex_shader =
        "attribute vec4 position;\n"
        "attribute vec2 texcoord;\n"
       "varying vec2 texcoordVarying;\n"
       "void main() {\n"
             "gl_Position = position;\n"
             "texcoordVarying = texcoord;\n"
       "}\n";

 

const char* fragment_shader =
        "precision mediump float;\n"
        "varying vec2 texcoordVarying;\n"
        "uniform sampler2D texture;\n"
         "void main() {\n"
               "gl_FragColor = texture2D(texture, texcoordVarying);\n"
         "}\n";

 

これらの Shader で、利用する texture とposition, texcoord をライブラリで用意して、GPUに渡してやれば、textureに描かれた内容が、 4点で示した矩形に貼り付けられる。

glTexSubImage2D で、texture の絵をGPUに送信する。 その後、glDrawArraysで三角形二つを描画させ、glFlushで、描画内容をフレームバッファに送ると、画面に絵が表示される。

具体的には、OpenGL ESの初期化処理の中で、

const GLfloat vertices = {
-1.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f
};

static GLfloat texcoords = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 0.0f,
1.0f, 1.0f
};texcoords

      position = glGetAttribLocation(program, "position");

      texcoord = glGetAttribLocation(program, "texcoord");

      textures[0] = glGetUniformLocation(program, "texture");

Shader 中の position, texcoord, texture とGUIライブラリ中の変数 position, texcoord, textures[0] を関連づける。  positionには、点の座標( vertices ) をいれ、

glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 0, vertices);

texcoordには、テキスチャの座標(texcoords) をいれる。

glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 0, texcoords);

 

描画処理の中で、

       glBindTexture(GL_TEXTURE_2D, textures[0]);

       glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,Width, Height, 

                                          GL_RGB,GL_UNSIGNED_SHORT_5_6_5, (void *)Buffer);

Buffer に入っている絵データをGL_TEXTURE_2Dに送り込む。  送り込まれた絵データは、Shader プログラム中の texture にとして、GPUで処理される。

   glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

で、二つの三角形を描いて、テキスチャを貼り付ける。

 

 Android の機種によっては、 OpenGL ES 2.0 であっても、Textureのサイズがが4の倍数でないと表示されない。 OpenGL ES 2.0 では、一応、テキスチャの大きさは、任意でよいとなっている。 また、Android も iOSも、表示する絵は、16ビットの深さである。

 

WindowsCEの場合、フレームバッファのポインタがもらえるので、テキスチャバッファの内容をフレームバッファのポインタに送り込んでやれば、画面に絵が表示される。 

hdc = GetDC (hWnd);
if (hdc) {
     if (ExtEscape (hdc, GETRAWFRAMEBUFFER, 0, 0,
                                  sizeof (RawFrameBufferInfo), (char *) & rfbi)) {
                  if (rfbi.wFormat == FORMAT_565){
                               m_framebufwidth = rfbi.cxPixels;
                               m_framebufheight = rfbi.cyPixels;
                               m_xpitch = rfbi.cxStride;
                               m_ypitch = rfbi.cyStride;
                               m_cbpp = rfbi.wBPP;
                               m_framebuf = (unsigned char *)rfbi.pFramePointer;

                              ReleaseDC (g_hWnd,hdc);
                  }

      }

}

m_framebufが、フレームバッファのポインタである。 FORMAT_565は、16ビット深度のバッファで、WindowsCEの場合、FORMAT_565となる。

 

Linuxで利用しているOpenGL の場合、Vertex Shader と Fragment Shader は、OpenGL ES 2.0 と基本は同じである。 しかし、Linuxで実装されているグラフィックスカードによっては、

OpenGL2.1とOpenGL3.0 になっており、Shader のプログラムが異なっている。

const char *vert_code_array =
"#version 130\n"
"in vec3 VertexPosition;\n"
"in vec2 VertexTexCoord;\n"
"uniform mat4 RotationMatrix;\n"
"out vec2 TexCoord;\n"
"void main (){\n"
    "gl_Position = RotationMatrix * vec4(VertexPosition, 1);\n"
    "TexCoord = VertexTexCoord;\n"
"}\n";

const char *vert_code_array120 =
"#version 120\n"
"attribute vec3 VertexPosition;\n"
"attribute vec2 VertexTexCoord;\n"
"uniform mat4 RotationMatrix;\n"
"void main (){\n"
    "gl_Position = RotationMatrix * vec4(VertexPosition, 1);\n"
     gl_TexCoord[0].xy = VertexTexCoord;\n"
"}\n";

 #version 130がOpenGL3.0で#version 120がOpenGL2.1 となる。 version は、OpenGLの関数 ss = (const char *)glGetString ( GL_SHADING_LANGUAGE_VERSION );

で取得して、ss の値を文字列比較で、バージョンをみる。

具体的には、

  if( strcmp(ss,"1.20") == 0 )

で、OpenGL 2.1 の場合、vert_code_array120 を使って、Shaderのコンパイル・ロードを行う。

GUIライブラリの不具合で、KeyHoleTVの停止

Android版KeyHoleTVは、GUIライブラリの不具合で、処理がループしてしまう不具合があった。 この不具合は、特定の領域で、文字列を表示する際、自動折りたたみ処理の不具合であった。 当然、この不具合は、WindowsCE版のKeyHoleTVでも同じGUIライブラリを用いているので、発生する。

自動折りたたみは、ある領域に文字列を表示する際、文字列がその領域をはみ出してしまう場合、改行して次に行に表示する機能で、KeyHoleTVでは、番組の詳細の表示に利用している。

この機能は、英語・日本語に対応しているが、英語と日本語が混ざった文字列では、折りたたみの処理で、ループしてしまった。 英語の場合、スペース、カンマ、ピリオド、ハイフォンの次の文字でしか、折りたたむ(改行する)ことができない。 一方日本語の場合、行の先頭に点(、)や丸(。)があってはいけないが、これ以外どこでも折りたためる。 このルールを実装したが、折りたたむ必要がある文字が英字の場合、前のスペース等の文字まで、戻って、折りたたんでいる。 

f:id:KeyHoleTV:20181210041938p:plain

上記の例では、赤線が境界で、赤線を越えて表示しないで改行を行う。 しかし、改行する文字が 'A'であるため、GUIライブラリが英語と判断してしまって ’’ まで戻って、改行していた。そのため、'ABCDEDFG’ の先頭から計算を行っていた。 しかし、結局改行できず、繰り返して計算を行っていた。

修正は、スペース等まで、戻る処理の中で、英数字以外の場合、折りたたみができるように変更して、ループから抜けるようにした。 上記の例では、'' で改行する。

この不具合は、デバイスの大きさと文字列に依存しており、再現するのに、Linux版のGUIライブラリがないと、対処出来にくかったと思う。 Linux版のGUIライブラリは、OpenGLで表示するウィンドウの大きさを任意に変更できるので、容易の再現できた。

 

なお、KeyHoleTVのダウンロード・プレミアムモジュールキーの購入は、

www.oiseyer.com で。