Author Archives: misohena

2016-01-14

タブレットアームスタンド購入

寝ながらタブレット、寝タブとでもいうのでしょうか。Xperia Z2 Tabletを使い始めてからというもの寝ながらアニメを見ることも多くなりました。

しかし寝ながらタブレットは姿勢が難しいんですよね。うつ伏せや横向きで長時間は辛いし仰向けだと腕が疲れてしまいます。Xperia Z2 Tabletは10.1インチ426gとそれほど重いわけではないのですが、まぁ、極端な話何も持たずに腕を上に伸ばし続けるだけでも疲れますしね。

というわけで「 タブレット アーム 」で検索して何か良いものはないか探しました。

ザッと見てみると大半がクランプ式(デスクライトでもおなじみのアレ)ですね! 机やベッドのパイプ等に固定して使うことを想定しているようです。困りました。床に布団を敷いているのでこれは使えません。

床に置いて使えるものは種類が限られますがいくつか存在します。

仰向けで使えそうなのは下の二つくらいでしょうか。アーム長は両者とも非可動部分があるので比較が難しいですが、全体ではEEA-MR002の方が長いです。鉄板の加工もEEA-MR002の方が良さそうなので、今回はこちらで。

イーサプライ iPad タブレットPC アームスタンド EEA-MR002

届いた箱はそれほど大きくなく、組み立てもとっても簡単でした(取り扱い説明書)。

肝心の寝ながら仰向けで使えるかどうかですが、ギリギリ大丈夫でした! 布団の横に置いてほぼ布団の中心まで伸ばせます(敷き布団の下に台座を挟んで端から45cmくらい)。画面の向きもジョイントがよく出来ていて下向きでも自由な向きで固定できます。

アームは固いので片手で変形というわけにはいきませんが、柔軟性と保持力を両立させるのは原理的に難しいので仕方がないですね。ドールの関節みたいなものです。どちらかといえば固い方が良いでしょう。

タブレットを掴む部分の根元にあるジョイントはボール+筒+つまみネジで十分な可動範囲と保持力を備えています。

タブレットのつけ外しも簡単ですね。レバーで簡単に調整・固定ができますし、スポンジがあるのでタブレットが滑って落ちることもありません。

というわけで、ますます寝タブがしやすくなってしまいました。あーあ。

2016-01-06

paiza Online Hackathon 7 めがね問題をSwiftで

恋愛SLG: プログラミングで彼女をつくる|paizaオンラインハッカソン7 というのを見かけたのでやってみました。

水着問題はちょっと難しかったです。

https://paiza.jp/poh/ando/share/fb746b63

「Swiftオープントップ50」というのもやってみました。 めがねがもらえる問題をSwiftで解き、平均速度とプログラムのバイト数を競うというものです。 この手の競技プログラムはやったことがありませんし、そもそもSwiftでプログラムを作ったこともありませんが、この機会に試してみました。

let
f={readLine()!}, // 1行読み込み関数
g={Int(f())!}, // 1行整数読み込み関数
h={(0..<$0).map{_ in f().characters.flatMap({Int(String($0))})}}, // 画像読み込み関数(h(size)でsize*sizeの2次元配列を返す)
n=g(),a=h(n), // 画像Nを読み込む
m=g(),b=h(m), // 画像Mを読み込む
o=n-m+1, // NとMの大きさの差を求める
s={a[$0/o+$1][$0%o..<$0%o+m] != b[$1][0..<m]}, // 画像の行に沿った1区間を比較する関数(ArraySlice同士の比較)
z=(0..<o*o).indexOf{p in (0..<m).indexOf{s(p,$0)}==nil}! // NにMを重ねたときに一致しない行が見つからなかった最初の左上座標を探す
print(String(z/o)+" "+String(z%o))

改行を削除して 0.01秒 271byte、この記事作成時点で23位。

1位の54 byteなんてどうやっているんでしょうね。ひょっとしてテストケースの性質を利用している?

最初はピクセルデータを文字列のままマッチングする方法を試したのですが、Swiftは文字列の部分取りだしが非常に面倒なようで、startIndex.advancedByなどと長い名前を打たされたあげくスピードもテストケース5で0.01を逃してしまったので方針を変更。諦めて配列を作って比較するようにしました。

Swiftいいですね。Closureは短く書く書き方もあって便利です。

Swiftの言語リファレンスやライブラリリファレンスと睨めっこしながら作成しました。古いバージョンのコードは結構動かなくなってるんですね。ぐぐって調べたコードがコンパイルできなくて難儀しました。

hの中にある"0 1 1 0"みたいな文字列を[0,1,1,0]へ変換する部分が短く書けなくて悩みました。Swiftのバージョンによっても書き方が違ってくるみたいです。

sやzのところで/や%を使っているところがありますが、このあたりの書き方次第で「Expression was too complex to be solved in reasonable time」というエラーが出てコンパイルできない場合が多々ありました。コンパイル時間(サーバの応答時間?)も書き方によって体感できるくらい差が出ます。元々sの比較処理は関数化せずにzの中にあったのですが、1つの式で書くとこのエラーが出てしまうため、式を分割しなければなりませんでした。分割するとletとreturnの文だけ文字数がかかってしまうので悩んだのですが、sを関数としてくくり出したら何とかコンパイルは通りました。

[1,2,3,4,5][2…3]==[3,4]が通るのにlet n=[1,2,3,4,5],m=[3,4];n[2…3]==mが通らないのは不思議でした。n[2…3]==[3,4]は通ります。ArraySlice<Int>と[Int]を引数に取る==演算子が無いからみたいなのですが、mは[Int]だけど[3,4]は違う?

(2015-01-07追記:なんとprint(a,b)でa bと出力されるんですね! あと細かいところを少し調整して、これだと249byte)

let
f={readLine()!},
g={Int(f())!},
h={(0..<$0).map{_ in f().characters.flatMap({Int(String($0))})}},
n=g(),a=h(n),
m=g(),b=h(m),
o=n-m+1,
s={a[$1+$2][$0..<$0+m]==b[$2][0..<m]}, // !=ではなく==にして呼び出し側で!sとした。!=だと!=の前後に空白を入れないとコンパイルエラーになるが==なら空白はいらない(-2 +1)
z=(0..<o*o).indexOf{p in (0..<m).indexOf{!s(p%o,p/o,$0)}==nil}!; //%o,/oはsからzへ移動。文字数は変わらない(-6 +6)けど、この方が分かりやすいし剰余も一回で済む。
print(z/o,z%o) //こう書けたorz(-21 +1)
2015-12-30

パンのトースト

トースターが欲しいけど置く場所が無いからオーブンレンジのトースト機能を使おうと思ったら電子レンジで作るトーストのレシピを見つけた。

電子レンジで目玉焼きトースト by ジュリさんぼ★ クックパッド

ハムやベーコンは無かったのでソーセージのスライスで代用。ビスマルク風。

うーん、十分美味しいんだけど、やっぱり食感はグニャッとしてる。

自宅の電子レンジにはトースト機能が付いているのだけど、どうもピンと来なくて使ってこなかった。電子レンジの庫内でパンを一切れ焼くのがどうにも無駄な気がしてしまって。時間もかかるんじゃ無いだろうか? いや、やってみれば案外慣れるのかもしれない。しばらく試してみるか……。

本当は専用のトースターが欲しいところだけど置く場所が無いんだよね。キッチンは半分物置になってしまっているので。

2015-12-30

釜揚げ氷見うどん

お土産の氷見うどん(乾麺)が残っている。ざるで食べるのが一番美味しいと聞いたのだけど、さすがにこの寒さ。それだとどうにも食べる気がしない。

かといってかけうどんにするのはどうにも野暮ったい。付け汁で食べたい。

そうだ、温かい付け汁にすれば良いんだ。あつもり? いや、ざるにあけて水で締めるのは冷たいから嫌だ。ざるも洗わないと行けなくなるし。

となれば水で締めない、いわゆる釜揚げに相当するのだろう。

固いのは嫌なので、ゆで時間はしっかりと。

うん、これはうまい。

やっぱり寒い日は温かい方が美味しい。

2015-10-14

2015秋の新番組

今期はどうでしょうか。今のところビビっと来るものはあまり無いかなぁと。

10/01 18:00 TX かみさまみならい ヒミツのここたま
10/01 25:29 NTV ルパン三世 新シリーズ
10/01 25:46 TBS Lance N’Masques(ランス・アンド・マスクス)
10/01 26:05 TX × きょーふ!ゾンビ猫
10/01 26:16 TBS ヤング ブラック・ジャック
10/02 23:00 MX ハッカドール (※ウルトラスーパーアニメタイム)
10/02 23:00 MX 影鰐-KAGEWANI- (※ウルトラスーパーアニメタイム)
10/02 23:00 MX ミス・モノクローム -The Animation- 3 (第3期) (※ウルトラスーパーアニメタイム)
10/02 24:30 MX ヘヴィーオブジェクト
10/02 25:55 TBS K RETURN OF KINGS
10/03 –:-- NTV Peeping Life TV シーズン1 ??
10/03 09:14 TX いとしのムーコ
10/03 17:35 NHK THUNDERBIRDS ARE GO (サンダーバード・アー・ゴー)
10/03 23:30 MX 学戦都市アスタリスク
10/04 17:00 TBS × 機動戦士ガンダム 鉄血のオルフェンズ
10/04 22:00 MX DIABOLIK LOVERS -MORE,BLOOD-(第2期)
10/04 22:15 MX 雨色ココア Rainy colorへようこそ!(第2期)
10/04 22:27 MX 小森さんは断れない!
10/04 22:30 MX コメット・ルシファー
10/04 23:00 MX コンクリート・レボルティオ 超人幻想
10/04 23:30 MX × 進撃!巨人中学校
10/04 24:30 MX 落第騎士の英雄譚
10/04 25:05 TX ワンパンマン
10/05 24:00 MX スタミュ -高校星歌劇-
10/05 25:00 MX × JKめし!
10/05 25:35 TX おそ松さん
10/06 25:05 MX × アニサン劇場
10/07 18:45 ETV 探偵チームKZ(カッズ)事件ノート
10/07 23:30 MX Dance with Devils
10/07 24:30 MX 櫻子さんの足下には死体が埋まっている
10/07 25:05 MX 対魔導学園35試験小隊
10/08 22:30 MX 俺がお嬢様学校に「庶民サンプル」としてゲッツされた件
10/08 24:55 CX すべてがFになる -THE PERFECT INSIDER-
10/09 25:23 TX × 牙狼-紅蓮ノ月- (第2期)
10/10 22:30 MX ご注文はうさぎですか?? (第2期)
10/12 24:30 MX ヴァルキリードライヴ マーメイド
10/12 25:11 MX あにトレ!EX
10/14 07:24 TX PEANUTS スヌーピー -ショートアニメ-
2015-08-05 ,

Boost.TestをAndroid NDKで使う(android-ndk-r10e)

Android上でBoost.Testを使ったテストをしてみました。

Android NDKでは通常のAndroidアプリケーションではない、main()から始まるネイティブの実行ファイル(つまりWindowsで言うところのコンソールアプリと言えば良い?)をビルドできるようになっています。これを使えばAndroid端末上でテストを実行できます。

以下に例を書いておきます。hello_testというディレクトリを作って、以下のファイルを用意し、run.shを実行するとテストをビルドして端末へアップロードして実行します。ライブラリをビルドするのは面倒くさいので、Boostはヘッダーだけの使用にとどめてあります。

hello_test/jni/Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_CPP_FEATURES += exceptions
LOCAL_CPP_FEATURES += rtti
LOCAL_MODULE := hello_test #←実行ファイル名になる。

# ソースファイルはワイルドカードで取得してみる。
# LOCAL_PATHはjniディレクトリを指している。
# カレントディレクトリは一つ上なので注意。
# LOCAL_SRC_FILESにはjniディレクトリからの相対パスで指定する必要がある。
SRC_FILES = $(wildcard $(LOCAL_PATH)/*.cpp)
LOCAL_SRC_FILES := $(SRC_FILES:$(LOCAL_PATH)/%=%)

# Boost C++ Libraryヘッダーファイルへのパスを指定する。
LOCAL_C_INCLUDES += c:/boost_he_no_path_wo_shiteisuru/boost_1_56_0

# これが無いとAndroid 5.0以降で実行できない。
LOCAL_CFLAGS += -pie -fPIE
LOCAL_LDFLAGS += -pie -fPIE

include $(BUILD_EXECUTABLE) #実行ファイルをビルドする。

実行ファイルを生成するには、最後に include $(BUILD_EXECUTABLE) を指定するのがミソです。

LOCAL_MODULE は実行ファイルの名前になります。

Boostのヘッダーへのパスは LOCAL_C_INCLUDES で指定してください。

実行時に"error: only position independent executables (PIE) are supported" というエラーが出ることがあります。Android 5.0以降だと出るようです。 -pie -fPIE というオプションを指定すると大丈夫なようです。

hello_test/jni/Application.mk

APP_ABI := all
APP_STL := c++_static
APP_PLATFORM := android-14 #低いとコンパイルが通らない

APP_PLATFORM を指定しなかったり、APIレベルが低いと必要な関数が無いためにコンパイルエラーにとなります。

hello_test/jni/main.cpp

#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>

hello_test/jni/test_hoge.cpp

#define BOOST_TEST_NO_LIB
#include <boost/test/unit_test.hpp>

BOOST_AUTO_TEST_SUITE(hoge_test)

BOOST_AUTO_TEST_CASE(test1)
{
    int a = 1;
    int b = 2;
    BOOST_CHECK_EQUAL(a + b, 3);
    BOOST_CHECK_EQUAL(a - b, -1);
    BOOST_CHECK_EQUAL(a / b, 0);
}

BOOST_AUTO_TEST_CASE(test2)
{
    double a = 1.0;
    double b = 2.0;

    BOOST_CHECK_EQUAL(a + b, 3.0);
    BOOST_CHECK_EQUAL(a - b, -1.0);
    BOOST_CHECK_EQUAL(a / b, 0.5);
}

BOOST_AUTO_TEST_SUITE_END()

hello_test/run.sh

#!/usr/bin/env bash

dir=/data/local/tmp/hello_test
bin=hello_test

# Build
ndk-build

# Detect Device's ABI
abi=$(adb shell getprop ro.product.cpu.abi | cut -d, -f1 | tr -d '\r')
echo "abi=$abi"

# Make directory
echo "mkdir $dir"
adb shell "mkdir $dir 2>/dev/null"

# Upload
adb push libs/$abi/$bin $dir

# Execute
adb shell "chmod 755 $dir/$bin"
adb shell $dir/$bin

# Delete
adb shell rm $dir/$bin
adb shell rmdir $dir

接続している端末のABIを取得するには、 adb shell getprop ro.product.cpu.abi を実行すればよいみたいです。カンマ区切りで複数列挙される場合があります。

/data/local/tmp というディレクトリにアップロードして実行し、終わったら削除します。

2015-07-24

2015夏の新番組

なんとか1~2話程度見終わりました。

2015-06-28 ,

C++によるAndroid NDK NativeActivityサンプル

Android NDKのnative-activityサンプルってC言語で書かれているので分かりづらいですよね、ということで、C++に書き直してみました。

Win32のメッセージループまわりをクラス化したことがある人なら馴染みのある形だと思います。

まずは表示まわりと加速度センサーをログ出力する部分を除いた純粋なアプリケーションクラスだけの例です。

#include <android/log.h>
#include <android_native_app_glue.h>

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "nativeactivitytest", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "nativeactivitytest", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "nativeactivitytest", __VA_ARGS__))

class ThisApp
{
    android_app *app_;

    struct SavedState
    {
    };
    SavedState state_;
public:
    explicit ThisApp(android_app *app)
        : app_(app)
    {
        app->userData = this;
        app->onAppCmd = handleCmdStatic;
        app->onInputEvent = handleInputStatic;
    }
    ThisApp(const ThisApp &) = delete;
    ThisApp &operator=(const ThisApp &) = delete;
    ~ThisApp()
    {
        app_->userData = nullptr;
        app_->onAppCmd = nullptr;
        app_->onInputEvent = nullptr;
    }

private:
    static void handleCmdStatic(android_app *app, int32_t cmd)
    {
        reinterpret_cast<ThisApp *>(app->userData)->handleCmd(cmd);
    }
    static int32_t handleInputStatic(android_app *app, AInputEvent *event)
    {
        return reinterpret_cast<ThisApp *>(app->userData)->handleInput(event);
    }

    void handleCmd(int32_t cmd)
    {
        switch(cmd){
        // Window
        case APP_CMD_INIT_WINDOW: LOGI("handleCmd(APP_CMD_INIT_WINDOW)"); break;
        case APP_CMD_TERM_WINDOW: LOGI("handleCmd(APP_CMD_TERM_WINDOW)"); break;
        case APP_CMD_WINDOW_RESIZED: LOGI("handleCmd(APP_CMD_WINDOW_RESIZED)"); break;
        case APP_CMD_WINDOW_REDRAW_NEEDED: LOGI("handleCmd(APP_CMD_WINDOW_REDRAW_NEEDED)"); break;
        case APP_CMD_CONTENT_RECT_CHANGED: LOGI("handleCmd(APP_CMD_CONTENT_RECT_CHANGED)"); break;

        case APP_CMD_GAINED_FOCUS: LOGI("handleCmd(APP_CMD_GAINED_FOCUS)"); break;
        case APP_CMD_LOST_FOCUS: LOGI("handleCmd(APP_CMD_LOST_FOCUS)"); break;

        // Activity State
        case APP_CMD_START: LOGI("handleCmd(APP_CMD_START)"); break;
        case APP_CMD_RESUME: LOGI("handleCmd(APP_CMD_RESUME)"); break;
        case APP_CMD_PAUSE: LOGI("handleCmd(APP_CMD_PAUSE)"); break;
        case APP_CMD_STOP: LOGI("handleCmd(APP_CMD_STOP)"); break;
        case APP_CMD_DESTROY: LOGI("handleCmd(APP_CMD_DESTROY)"); break;

        case APP_CMD_SAVE_STATE:
            LOGI("handleCmd(APP_CMD_SAVE_STATE)");
            backupState();
            break;
        }
    }
    int32_t handleInput(AInputEvent *event)
    {
        LOGI("handleInput(type=%d, deviceId=%d, source=%d)",
             AInputEvent_getType(event),
             AInputEvent_getDeviceId(event),
             AInputEvent_getSource(event));

        switch(AInputEvent_getType(event)){
        case AINPUT_EVENT_TYPE_KEY:
            LOGI("handleKeyEvent(action=%d, flags=0x%x, keycode=%d, scancode=%d, meta=%d, repeatcount=%d)",
                 AKeyEvent_getAction(event),
                 AKeyEvent_getFlags(event),
                 AKeyEvent_getKeyCode(event),
                 AKeyEvent_getScanCode(event),
                 AKeyEvent_getMetaState(event),
                 AKeyEvent_getRepeatCount(event));
            //return 1;
            break;
        case AINPUT_EVENT_TYPE_MOTION:
            LOGI("handleMotionEvent(action=%d, flags=0x%x, meta=%d, buttonstate=%d, edgeflag=%d, p0x=%f, p0y=%f",
                 AMotionEvent_getAction(event),
                 AMotionEvent_getFlags(event),
                 AMotionEvent_getMetaState(event),
                 AMotionEvent_getButtonState(event),
                 AMotionEvent_getEdgeFlags(event),
                 AMotionEvent_getX(event, 0),
                 AMotionEvent_getY(event, 0));
            //size_t AMotionEvent_getPointerCount
            //d AMotionEvent_getPointerId
            //d AMotionEvent_getToolType
            //f AMotionEvent_getRawX
            //...etc(see:<ndkdir>/platforms/android-*/arch-*/usr/include/android/input.h)
            //return 1;
            break;
        }
        return 0;
    }

    void backupState()
    {
        LOGI("backupState");
        app_->savedStateSize = sizeof(SavedState);
        app_->savedState = malloc(sizeof(SavedState));
        *reinterpret_cast<SavedState *>(app_->savedState) = state_;
    }

    void restoreState()
    {
        LOGI("restoreState");
        if(app_->savedState && app_->savedStateSize == sizeof(SavedState)){
            state_ = *reinterpret_cast<const SavedState *>(app_->savedState);
        }
    }


    int getNextEventDelayTimeMilli()
    {
        // 遅くとも一定時間後までに processInEventLoop() が呼ばれるようにするには、
        // ここでミリ秒を返す。
        // それより早い時間で呼ばれることもある。
        return -1;
    }
    void processInEventLoop()
    {
        // なんらかのイベントが処理された後または、
        // getNextEventDelayTimeMilli()が返した時間経過したときに呼ばれる。
    }

    int looperIdNext_ = LOOPER_ID_USER;
    int getNewLooperId()
    {
        return looperIdNext_++;
    }
    void processUserLooperId(int looperId)
    {
        LOGI("processUserLooperId looperId=%d", looperId);
        // ユーザー定義のLooper ID(LOOPER_ID_USER以上の値)を持つイベントが発生したときに呼ばれる。
    }


    void destroyApplication()
    {
        LOGI("destroyApplication");
    }

public:
    void run()
    {
        // Restore app state
        restoreState();

        // event loop
        for(;;){
            int ident;
            int events;
            android_poll_source* source;

            while((ident = ALooper_pollAll(getNextEventDelayTimeMilli(), NULL, &events, (void**)&source)) >= 0){
                // Process this event.
                if(ident >= 0 && ident < LOOPER_ID_USER){
                    // Dispatch LOOPER_ID_MAIN, LOOPER_ID_INPUT
                    if (source != NULL) {
                        source->process(app_, source);
                    }
                }
                else if(ident >= LOOPER_ID_USER){
                    processUserLooperId(ident);
                }

                // Check if we are exiting.
                if(app_->destroyRequested != 0){
                    destroyApplication();
                    return;
                }
            }

            processInEventLoop();
        }
    }
};

void android_main(android_app *state)
{
    app_dummy();

    ThisApp thisApp(state);
    thisApp.run();
}
  • アクティビティの状態変更やタッチ入力など、イベントの発生時にログを出力するようにしました。どのタイミングでどの関数が呼ばれるのかを把握することが大切です。
  • アクティビティの状態を保存・復元するコードのひな形も一応入ってます。複雑なアプリの場合はちゃんとしたシリアライズの仕組みを入れるべきでしょう。
  • イベントループのタイムアウトを決定する仕組みのひな形が入ってます。このあたりを使うと独自のタイマークラスなんかが作れるんじゃないかと思います。
  • ユーザー定義のLooper IDという概念がandroid_native_app_glueにあるのですが、それを処理する仕組みも入ってます。native-activityサンプルでは加速度センサーイベントの処理にユーザー定義のIDを使っています。でも、加速度センサーの場合はコールバックを使っても実装できるので、必ずしもユーザー定義のIDを使わなくても良い気がします。

加速度センサー

native-activityサンプルのように加速度センサーの値をログへ出力するには、上のソースを次のように改変します。元のサンプルではLOOPER_ID_USERを使用していましたが、調べたところASensorManager_createEventQueueのコールバック引数を使えば使わずに済みそうだったのでそのようにしてみました。

#include <android/sensor.h>

//..略...

class SensorMonitor
{
    ASensorManager *sensorManager_ = nullptr;
    const ASensor *accelerometerSensor_ = nullptr;
    ASensorEventQueue *sensorEventQueue_ = nullptr;
public:
    SensorMonitor(){}
    SensorMonitor(const SensorMonitor &) = delete;
    SensorMonitor &operator=(const SensorMonitor &) = delete;
    ~SensorMonitor()
    {
        destroy();
    }
    void init(android_app *app)
    {
        if(!sensorEventQueue_){
            sensorManager_ = ASensorManager_getInstance();
            accelerometerSensor_ = ASensorManager_getDefaultSensor(sensorManager_, ASENSOR_TYPE_ACCELEROMETER);
            sensorEventQueue_ = ASensorManager_createEventQueue(sensorManager_, app->looper, ALOOPER_POLL_CALLBACK, handleSensorStatic, this);
        }
    }
    void destroy()
    {
        stop();
        if(sensorEventQueue_){
            ASensorManager_destroyEventQueue(sensorManager_, sensorEventQueue_);
            sensorEventQueue_ = nullptr;
        }
        accelerometerSensor_ = nullptr;
    }
    void start()
    {
        LOGI("Sensor start");
        if(accelerometerSensor_){
            ASensorEventQueue_enableSensor(sensorEventQueue_, accelerometerSensor_);
            ASensorEventQueue_setEventRate(sensorEventQueue_, accelerometerSensor_, (1000L/60)*1000);
        }
    }
    void stop()
    {
        LOGI("Sensor stop");
        if(accelerometerSensor_){
            ASensorEventQueue_disableSensor(sensorEventQueue_, accelerometerSensor_);
        }
    }
private:
    static int handleSensorStatic(int fd, int events, void *data)
    {
        reinterpret_cast<SensorMonitor *>(data)->handleSensor();
        return 1;
    }

    void handleSensor()
    {
        LOGI("Sensor process");
        if(accelerometerSensor_){
            ASensorEvent event;
            while(ASensorEventQueue_getEvents(sensorEventQueue_, &event, 1) > 0){
                LOGI("accelerometer: x=%f y=%f z=%f",
                     event.acceleration.x,
                     event.acceleration.y,
                     event.acceleration.z);
            }
        }
    }
};

class ThisApp
{
    //...略...
    SensorMonitor sensor_;

public:
    explicit ThisApp(android_app *app)
        : app_(app)
    {
        //...略...
        sensor_.init(app);
    }
    //...略...

    void handleCmd(int32_t cmd)
    {
        //...略...
        case APP_CMD_GAINED_FOCUS:
            LOGI("handleCmd(APP_CMD_GAINED_FOCUS)");
            // When our app gains focus, we start monitoring the accelerometer, etc.
            sensor_.start();
            break;
        case APP_CMD_LOST_FOCUS:
            LOGI("handleCmd(APP_CMD_LOST_FOCUS)");
            // When our app loses focus, we stop monitoring the accelerometer, etc.
            // This is to avoid consuming battery while not being used.
            sensor_.stop();
        //...略...
    }
    //...略...

OpenGL ES

OpenGL ES2.0を使って画面に三角形を表示するには次のように改変します。

#include <memory>
#include <string>
#include <EGL/egl.h>
#include <GLES2/gl2.h>

...略...

// OpenGL Utilities

template<typename FunGetIv, typename FunGetLog>
std::string getGLLogStr(GLuint obj, FunGetIv funGetIv, FunGetLog funGetLog)
{
    GLint len = 0;
    funGetIv(obj, GL_INFO_LOG_LENGTH, &len);
    if(len > 1){
        std::unique_ptr<char[]> infoLog(new char[len]);
        funGetLog(obj, len, nullptr, infoLog.get());
        return std::string(infoLog.get());
    }
    else{
        return std::string();
    }
}
std::string getGLShaderLogInfo(GLuint obj)
{
    return getGLLogStr(obj, glGetShaderiv, glGetShaderInfoLog);
}
std::string getGLProgramLogInfo(GLuint obj)
{
    return getGLLogStr(obj, glGetProgramiv, glGetProgramInfoLog);
}

GLuint loadShader(GLenum type, const char *shaderSrc)
{
    GLuint shader = glCreateShader(type);
    if(shader == 0){
        return 0;
    }

    glShaderSource(shader, 1, &shaderSrc, nullptr);

    glCompileShader(shader);

    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if(!compiled){
        LOGI("glCompileShader failed.\n Error:(%s)\n", getGLShaderLogInfo(shader).c_str());
        glDeleteShader(shader);
        return 0;
    }

    return shader;
}

/**
 * OpenGLES 2.0の初期化からフレームレンダリングまでをカバーしたフレームワークです。
 *
 * このクラスを継承して必要な仮想関数をオーバーライドしてください。
 * リソースの読み込み、解放を行うloadResources()、termResources()やinitializeContextState()、フレームのレンダリングを行うdrawFrame()をオーバーライドしてください。
 *
 * そしてネイティブウィンドウ(ANativeWindow *)が有効になった時点でinitWindow()を、無効になった時点でtermWindow()を呼び出すようにします。
 * ウィンドウへの描画が必要になった時点でpresentFrame()を呼び出してください。
 */
class GLEnvironment
{
    EGLDisplay display_;

    EGLSurface surface_; //window surface
    EGLConfig config_; //frame buffer configuration
    EGLint screenWidth_;
    EGLint screenHeight_;

    EGLContext context_;
public:
    GLEnvironment()
        : display_(EGL_NO_DISPLAY),
          surface_(EGL_NO_SURFACE),
          config_(nullptr),
          screenWidth_(), screenHeight_(),
          context_(EGL_NO_CONTEXT)
    {}
    ~GLEnvironment()
    {
        destroy();
    }
    GLEnvironment(const GLEnvironment &) = delete;
    GLEnvironment &operator=(const GLEnvironment &) = delete;

    bool create(ANativeWindow *window)
    {
        return initContext(window);
    }
    void destroy()
    {
        disconnectDisplay();
    }
    bool recreate(ANativeWindow *window)
    {
        destroy();
        return create(window);
    }

    //
    // Attach/Detach ANativeWindow
    //

    static const bool preserveEGLContextOnPause = true;
    bool initWindow(ANativeWindow *window)
    {
        LOGI("initWindow");
        if(preserveEGLContextOnPause && hasDisplay() && !hasSurface() && hasContext() && isContextInitialized()){
            // Resume (Try create window surface by same config)
            LOGI("Trying to resume");
            if(createSurface(window, config_) && makeContextCurrent(window)){
                LOGI("resume succeeded");
                return true;
            }
            LOGI("resume failed");
        }
        // Recreate all objects.
        return recreate(window);
    }

    void termWindow()
    {
        if(!preserveEGLContextOnPause){
            destroyContext();
        }
        destroySurface();
    }


    //
    // Presentation
    //

    bool presentFrame(ANativeWindow *window)
    {
        if(!isContextInitialized()){
            LOGE("Context not initialized");
            return false;
        }

        drawFrame();
        if(!swap()){
            recreate(window);
            drawFrame();
            if(!swap()){
                LOGE("Swap failed twice");
                return false;
            }
        }
        return true;
    }

private:
    bool swap()
    {
        if(hasSurface()){
            LOGI("swap");
            if(!eglSwapBuffers(display_, surface_)){
                LOGI("eglSwapBuffers failed");
                return false;
            }
        }
        return true;
    }

    virtual void drawFrame(){}

    //
    // Display
    //
private:
    bool hasDisplay() const {return display_ != EGL_NO_DISPLAY;}

    bool connectDisplay()
    {
        if(display_ == EGL_NO_DISPLAY){
            // Get current display.
            const EGLDisplay uninitializedDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
            if(uninitializedDisplay == EGL_NO_DISPLAY){
                // Do not terminate display.
                LOGE("eglGetDisplay failed");
                return false;
            }
            if(!eglInitialize(uninitializedDisplay, 0, 0)){
                LOGE("eglInitialize failed");
                return false;
            }
            display_ = uninitializedDisplay;
        }
        return true;
    }
    void disconnectDisplay()
    {
        destroyContext();
        destroySurface();

        if(display_ != EGL_NO_DISPLAY){
            LOGI("eglTerminate");
            eglTerminate(display_);
            display_ = EGL_NO_DISPLAY;
        }
    }

    //
    // Config
    //
private:
    static EGLConfig chooseConfig(EGLDisplay display)
    {
        struct DesiredConfig
        {
            int depth;
            DesiredConfig(int depth):depth(depth){}
        };
        const DesiredConfig desiredConfigs[] = {DesiredConfig(24), DesiredConfig(16)};
        const DesiredConfig * const dcEnd = desiredConfigs + sizeof(desiredConfigs)/sizeof(desiredConfigs[0]);
        const DesiredConfig *dc;
        EGLConfig config = nullptr;
        for(dc = desiredConfigs; dc != dcEnd; ++dc){
            const EGLint desiredAttribs[] = {
                EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //Request opengl ES2.0
                EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
                EGL_BLUE_SIZE, 8,
                EGL_GREEN_SIZE, 8,
                EGL_RED_SIZE, 8,
                EGL_DEPTH_SIZE, dc->depth,
                EGL_NONE };

            EGLint numConfigs;
            if(!eglChooseConfig(display, desiredAttribs, &config, 1, &numConfigs)){
                LOGE("eglChooseConfig failed");
                return nullptr;
            }
            if(numConfigs == 1){
                break;
            }
        }
        if(config == nullptr){
            LOGE("no EGL config");
            return nullptr;
        }
        LOGI("match config depth=%d", dc->depth);
        return config;
    }


    //
    // Window Surface
    //
private:
    bool hasSurface() const { return surface_ != EGL_NO_SURFACE;}

    bool createSurface(ANativeWindow *window, EGLConfig desiredConfig = nullptr)
    {
        if(!connectDisplay()){
            return false;
        }

        if(surface_ == EGL_NO_SURFACE){
            const EGLConfig config = desiredConfig ? desiredConfig : chooseConfig(display_);
            if(!config){
                LOGE("chooseConfig failed");
                return false;
            }

            // ANativeWindow_setBuffersGeometry
            EGLint format;
            eglGetConfigAttrib(display_, config, EGL_NATIVE_VISUAL_ID, &format);
            ANativeWindow_setBuffersGeometry(window, 0, 0, format);

            // Create surface.
            surface_ = eglCreateWindowSurface(display_, config, window, nullptr);
            if(surface_ == EGL_NO_SURFACE){
                LOGE("eglCreateWindowSurface failed. error=%d", eglGetError());
                return false;
            }
            config_ = config;

            eglQuerySurface(display_, surface_, EGL_WIDTH, &screenWidth_);
            eglQuerySurface(display_, surface_, EGL_HEIGHT, &screenHeight_);
            LOGI("screen size %d x %d", screenWidth_, screenHeight_);
        }
        return true;
    }
    void destroySurface()
    {
        //detachCurrentContext(); // 後続のunloadResourcesのために今はdetachしないことにする。

        if(surface_ != EGL_NO_SURFACE){
            LOGI("eglDestroySurface(window surface)");
            eglDestroySurface(display_, surface_);
            surface_ = EGL_NO_SURFACE;
        }
    }
public:
    int getScreenWidth() const {return screenWidth_;}
    int getScreenHeight() const {return screenHeight_;}

    //
    // Context
    //
private:
    bool hasContext() const{ return context_ != EGL_NO_CONTEXT;}
    bool createContext(ANativeWindow *window)
    {
        if(!createSurface(window)){ //Needs display_ & config_
            return false;
        }

        if(context_ == EGL_NO_CONTEXT){
            const EGLint attribs[] = {
                EGL_CONTEXT_CLIENT_VERSION, 2, //Request opengl ES2.0
                EGL_NONE };
            context_ = eglCreateContext(display_, config_, nullptr, attribs);
            if(context_ == EGL_NO_CONTEXT){
                LOGE("eglCreateContext failed");
                return false;
            }
        }
        return true;
    }
    void destroyContext()
    {
        termContext();

        if(context_ != EGL_NO_CONTEXT){
            LOGI("eglMakeCurrent null");
            eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            LOGI("eglDestroyContext");
            eglDestroyContext(display_, context_);
            context_ = EGL_NO_CONTEXT;
        }
    }

        // Current Context

    bool makeContextCurrent(ANativeWindow *window)
    {
        if(!createSurface(window)){
            return false;
        }
        if(!createContext(window)){
            return false;
        }

        LOGI("eglMakeCurrent");
        if(!eglMakeCurrent(display_, surface_, surface_, context_)){
            LOGE( "eglMakeCurrent failed" );
            return false;
        }
        return true;
    }
    void detachCurrentContext()
    {
        if(display_ != EGL_NO_DISPLAY){
            LOGI("detachCurrentContext");
            eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        }
    }
    bool isContextCurrent()
    {
        return context_ != EGL_NO_CONTEXT && eglGetCurrentContext() == context_;
    }

        // Context Initialization

    bool contextInitialized_ = false;
protected:
    bool isContextInitialized() const {return contextInitialized_;}
private:
    bool initContext(ANativeWindow *window)
    {
        if(!makeContextCurrent(window)){
            return false;
        }
        if(!contextInitialized_){
            if(!loadResources()){
                return false;
            }
            initializeContextState();
            contextInitialized_ = true;
        }
        return true;
    }
    void termContext()
    {
        if(contextInitialized_){
            unloadResources();
            contextInitialized_ = false;
        }
    }

    virtual bool loadResources(){return true;}
    virtual void unloadResources(){}
    virtual void initializeContextState(){}
};



class ThisAppGraphics : public GLEnvironment
{
    GLuint vertexShader_;
    GLuint fragmentShader_;
    GLuint programObject_;
public:
    ThisAppGraphics()
        : GLEnvironment(),
          vertexShader_(0),
          fragmentShader_(0),
          programObject_(0)
    {
    }
private:
    virtual bool loadResources() override
    {
        LOGI("loadResources");

        const char vShaderStr[] =
            "attribute vec4 vPosition;"
            "void main(){"
            "  gl_Position = vPosition;"
            "}";
        const char fShaderStr[] =
            "precision mediump float;"
            "void main(){"
            "  gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);"
            "}";
        vertexShader_ = loadShader(GL_VERTEX_SHADER, vShaderStr);
        fragmentShader_ = loadShader(GL_FRAGMENT_SHADER, fShaderStr);
        programObject_ = glCreateProgram();

        glAttachShader(programObject_, vertexShader_);
        glAttachShader(programObject_, fragmentShader_);
        glBindAttribLocation(programObject_, 0, "vPosition");
        glLinkProgram(programObject_);

        GLint linked = 0;
        glGetProgramiv(programObject_, GL_LINK_STATUS, &linked);
        if(!linked){
            LOGI("glLinkProgram failed.\n Error:(%s)\n", getGLProgramLogInfo(programObject_).c_str());
            return false;
        }

        return true;
    }
    virtual void unloadResources() override
    {
        LOGI("unloadResources");
        if(programObject_){
            glDeleteProgram(programObject_);
            programObject_ = 0;
        }
        if(vertexShader_){
            glDeleteShader(vertexShader_);
            vertexShader_ = 0;
        }
        if(fragmentShader_){
            glDeleteShader(fragmentShader_);
            fragmentShader_ = 0;
        }
    }
    virtual void initializeContextState() override
    {
        glEnable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);
        glViewport(0, 0, getScreenWidth(), getScreenHeight());
    }

    virtual void drawFrame() override
    {
        LOGI("drawFrame");
        GLfloat vertices[] = {
            0.0f, 0.5f, 0.0f,
            -0.5f, -0.5f, 0.0f,
            0.5f, -0.5f, 0.0f
        };

        glClearColor(0, 0, 1, 1);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(programObject_);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices);
        glEnableVertexAttribArray(0);
        glDrawArrays(GL_TRIANGLES, 0, 3);
    }
};

class ThisApp
{
    ...略...
    ThisAppGraphics gl_;
    ...略...

    void handleCmd(int32_t cmd)
    {
        switch(cmd){
        case APP_CMD_INIT_WINDOW:
            LOGI("handleCmd(APP_CMD_INIT_WINDOW)");
            // The window is being shown, get it ready.
            if(app_->window){
                gl_.initWindow(app_->window);
                gl_.presentFrame(app_->window);
            }
            break;
        case APP_CMD_TERM_WINDOW:
            LOGI("handleCmd(APP_CMD_TERM_WINDOW)");
            // The window is being hidden or closed, clean it up.
            gl_.termWindow();
            break;

    ...略...

他のプロジェクトでも使い回せるようなコードはGLEnvironmentクラスに収め、このプロジェクトに固有な描画コードはThisAppGraphicsクラスに収めてみました。ThisAppクラスからはapp_->windowが有効になったときにinitWindow()やpresentFrame()を、app_->windowが無効になったときにtermWindow()を呼び出すだけです。

気になった点をいくつか。

  • エミュレータだとPauseしてResumeすると画面が真っ暗になります。コンテキストがロストしているのかもしれませんが特にエラーが起きている様子も無いので原因がよく分かりません。Teapotサンプルも同じ現象が起きます。上ソースのpreserveEGLContextOnPauseをfalseにすれば(EGLContextをPause時に破棄してResume時に作り直せば)解消します。
  • preserveEGLContextOnPauseをfalseにするとポーズ・レジュームのたびにリソースを作り直します。opengl/java/android/opengl/GLSurfaceView.java - platform/frameworks/base - Git at Google を見ると、Q3Dimension MSM7500のときは必ずコンテキストを作り直すようにする等のコードが入っているので、そのような判定も追加すべきかもしれません。
  • 画面を回転させるとNativeActivityは終了して再起動します。これはそういうものっぽいです。嫌ならNativeActivityを使わずにJavaで作ったActivityから呼び出す形で何とかするしかないかも。(2015/08/05追記: APIレベルによるみたい? KOBE GDG: 回転時にActivityを破棄させない方法 )
  • ndk_helperのGLContextクラスを使おうかなと思ったのですが、ソースを追っていて気になるところがあった(EGLオブジェクトを破棄せずに作り直しているパスがある気がする)ので自分で実装しました。
2015-06-12

暑い

今日、窓を開けていても少し暑いと感じました。梅雨っぽいはっきりしない天候で終始曇っていたと思うのですが、風が冷たくなくなったのでしょうか。少し湿気も感じます。やれやれ、もう夏はすぐそこなのですね。

2015-06-12 , ,

Android SDK プロジェクトの作り方, バージョン管理, local.propertiesアップデート, キーの生成, NDKまで

Android SDKのコマンドラインを使ったAndroidアプリプロジェクトの作成方法を記しておきます。しばらく離れているとすぐに忘れるので備忘録として(というかこうして書くことでより記憶に定着させるため)。

前提: SDK, JDK, ant等必要な物をインストールしてパスを通してあること

プロジェクトの作成

android --help create project

で、ヘルプが出るので参考に。

android※1 create project -n MyAwesomeApp -t 10※2  -p ./MyAwesomeApp※3 -k com.example.my_awesome_app -a MyAwesomeApp※4
  • ※1: Cygwin等からはandroid.batとするとよい。
  • ※2: -tの値(target ID)は環境によって異なる? android list targets で表示される。
  • ※3: カレントディレクトリへ作るなら単にピリオドのみでOK。
  • ※4: Activity名。デフォルトでアプリアイコンの下に表示される名前になる。

参考: http://developer.android.com/tools/help/android.html

コンパイルは次のようにする。

cd MyAwesomeApp
ant debug

インストールはAVDか端末を接続した上で ant debug install とする。

バージョン管理に入れる(Subversionやgit等)

バージョン管理では次のファイルを無視する(.gitignore等)。

  • local.properties
  • bin
  • gen
  • libs
  • obj (NDK使用時)

バージョン管理から取り出したときの作業

リポドシリから取り出したときはlocal.propertiesが無いので生成する必要がある。

cd MyAwesomeApp
android update project -p .

キーストアの情報が必要ならlocal.propertiesへ追記する。

キーの生成方法

キーの生成は次のようにする。(keytoolはJDKのbinの中)

keytool -genkey -v -keystore my_awesome_app.keystore -alias my_awesome_app -keyalg RSA -keysize 2048 -validity 10000

local.propertiesにキーの情報を追加する。

key.store=my_awesome_app.keystore
key.alias=my_awesome_app

次のようにしてリリースビルドできるようになるはず。

cd MyAwesomeApp
ant release

参考: http://developer.android.com/tools/publishing/app-signing.html

NDK

NDKを使いたい場合はjniディレクトリを作る。NDKのsamplesディレクトリを参考にすると良い。

プロジェクトディレクトリ下にjniディレクトリを作り、次のファイルを配置する。

  • Android.mk
  • Application.mk
  • *.cpp, *.h等

ビルドはプロジェクトディレクトリトップで ndk-build とし、成功したら、普通に ant debug 等。(参考: https://developer.android.com/ndk/guides/ndk-build.html)

AndroidManifest.xml は必要に応じて修正すること。特にNativeActivityを使う場合は修正が必要。

各ファイルの例を次に記す。(android-ndk-r10e時点。バージョンによってオプションは変わっていくかも?)

jni/Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_CPP_FEATURES += exceptions
LOCAL_CPP_FEATURES += rtti
LOCAL_MODULE := my-awesome-app-jni
LOCAL_SRC_FILES := my-awesome-app-jni.cpp

include $(BUILD_SHARED_LIBRARY)
# include $(BUILD_EXECUTABLE) ←main()から実行する場合はこちら。

例外やRTTIのないC++なんて98年以前の感じですね。今更耐えられないので有効。(r10e現在デフォルト無効)

現在のバージョンではC++11はデフォルトで使えるらしいです。ライブラリはまた別の話ですが。

参考: https://developer.android.com/ndk/guides/android_mk.html

jni/Application.mk

APP_ABI := all
APP_STL := c++_static

stlport(stlport_static)にはstd::to_stringが無かったのでclangのライブラリ(c++_static)を指定しました。

参考: https://developer.android.com/ndk/guides/application_mk.html

jni/my-awesome-app-jni.cpp

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_my_1awesome_1app_MyAwesomeApp_nativeExampleFun(
    JNIEnv *env,
    jobject thisj,
    jstring exampleStr,
    int exampleInt,
    jobject exampleObj)
{
    struct UTFChars
    {
        JNIEnv *env;
        jstring str;
        const char *chars;
        UTFChars(JNIEnv *env, jstring str):env(env), str(str), chars(env->GetStringUTFChars(str, nullptr)){}
        ~UTFChars(){if(chars){env->ReleaseStringUTFChars(str, chars);}}
        const char *get() const {return chars;}
    };
    const std::string result = UTFChars(env, exampleStr).get() + std::to_string(exampleInt);
    return env->NewStringUTF(result.c_str());
}

関数名はパッケージ名やクラス名に合わせる。パッケージ名に_(アンダースコア)が入っているときは_の後に1が必要なので注意。_のままだとランタイムでエラーになる。

JNIに詳しくないんですが、こういうことをするための文字列ライブラリってないんですか?

src/com/example/my_awesome_app/MyAwesomeApp.java

//...略...
import android.graphics.Bitmap;
import android.widget.Toast;

public class MyAwesomeApp extends Activity
{
    //...略...

    // ネイティブ関数の宣言
    static {
        System.loadLibrary("my-awesome-app-jni"); //LOCAL_MODULEで指定した名前
    }
    native String nativeExampleFun(String exampleStr, int exampleInt, Bitmap exampleObj);

    //...略...
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // ネイティブ関数を呼び出してみる
        String result = nativeExampleFun("test", 123, null);
        Toast.makeText(this, result, Toast.LENGTH_LONG).show(); //test123と表示される
    }
}

AndroidManifest.xml (NativeActivity使用時の例)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example.my_awesome_app"
      android:versionCode="1"
      android:versionName="1.0">

  <uses-sdk android:minSdkVersion="14" /><!--←SDKのバージョン-->
  <uses-feature android:glEsVersion="0x00020000"></uses-feature><!--←OpenGL ESを使う場合、そのバージョン-->

  <application android:label="@string/app_name" android:icon="@drawable/ic_launcher" android:hasCode="false"><!--←Javaのコードを使わない場合はhasCodeをfalseにすること-->
        <activity android:name="android.app.NativeActivity"
                  android:label="@string/app_name"
                  android:configChanges="orientation|keyboardHidden"><!-- nameはNativeActivityを使うならこの値で固定。configChangesは端末回転時にActivityを破棄しない設定らしいけど、APIレベルによっては効かないらしい? -->

            <meta-data android:name="android.app.lib_name"
                       android:value="my-awesome-app-jni" /><!-- ←valueはAndroid.mkのLOCAL_MODULEで指定したモジュール名 -->

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>