Android ネイティブアプリでは、画面にテキストを表示するのが
アプリには、簡単な、あるいは詳細な操作方法などを記載したヘルプ画面が必要ですが、
すべてを画像で用意すると
簡単なヘルプのみ apk に置いて、詳細な情報をウェブに置こうと考えたとき、
ネイティブアプリで WebView コントロールが使用できないようであることがわかりました。
WebView コントロールが使用できれば、そこに html ページを表示できるので OK なわけですが、
ネイティブアプリでは無理みたいです。
スレッドが必要になるトーストやメッセージボックスを含め、
少なくとも Windows の
そこで、
apk サイズを小さく保つために、最小限の情報のみ apk に置いて、詳細情報はウェブサイトに置くと決め、
2021 年 4 月 5 日、 C/C++ での実装内容としては基本的には変わってはいませんが、 マナー良く DeleteLocalRef 関数を呼び出すなど、 全面的に見直しています。 また、Java コードを呼び出して実行するコードを追加しました。
このページ、および開発関連ページは、PC 向けデザインとなっております。 画面サイズの小さいスマホでは、快適な表示が得られませんので、ご了承ください。
ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
また、本サイトが初めての方は、まずこのページの注意事項をご覧ください。
投稿 April 5, 2021
ブラウザでウェブサイトを開くコードの前に、
何を
最終的には、当然、「ウェブサイトを開く」ボタンやリンクがある画面で、 そのボタンやリンクが押されたときになりますが、ここではそこまでは触れません。 ただ、テストするにも気を付けなくてはいけない点があります。
今回の見直しを行うにあたり、まずは 「VC++ 2019/2015 で新規プロジェクトを作成する」 に書いた手順で、Win10 + VS2019 で Android ネイティブアクティビティを作成しました。 そして特に何も考えずに、タッチイベントを検出する、 engine_handle_input 関数にコードを追加したところ、 タッチでブラウザを起動できるのですが、 タスク一覧からテストアプリに戻ってきても、またウェブサイトを開こうとしてしまいました。
Windows のマウスイベントは、基本的にはボタンダウンやボタンアップはそのときのみ、 マウスムーブもポインタが移動したときのみに送られてくるので、 マウスボタン押しっぱなしでは何も通知されてきません。 ポインタを移動させなければ、WM_MOUSEMOVE メッセージは送信されません。
一方、Android では、画面のタッチイベントがしょっちゅう発生しているようです。 それぞれ詳しく調べたわけではありませんが、少なくともタッチイベントでは、 タッチしている間、画面のどこがタッチされているか、その場所が変わらなくても、 何度も繰り返してタッチイベントが通知されてくるようです。 つまり、1回タッチしたつもりでも、メッセージ的には複数通知されている、というわけです。
今回の場合、そのせいか、あるいはタスク一覧から戻したときにもタッチが発生しているのか、 とにかくただコードを置いただけでは、何度もそれが呼び出されてしまう、ということです。
ですので、次のセクション以降のコードは、
画面から手が
まず、main.cpp で、 engine_handle_input 関数を探します。
/** * 次の入力イベントを処理します。 */ static int32_t engine_handle_input(struct android_app* app, AInputEvent* event) { struct engine* engine = (struct engine*)app->userData; if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { engine->state.x = AMotionEvent_getX(event, 0); engine->state.y = AMotionEvent_getY(event, 0);
これに続く、上で閉じていない if 文の中に、コードを書きますが、 ただここにコードを書くと、繰り返し実行されてしまうので、良くありません。
ここまでで、AInputEvent_getType 関数によりタッチイベントの判定が行われ、 AMotionEvent_getX 関数などで、タッチされた位置を取得しています。 これは、プロジェクト作成時に自動生成されたコードです。
このあと、次の判定文を入れて、手が離されたことを認識します。
int32_t action = AMotionEvent_getAction(event); if ((action & AMOTION_EVENT_ACTION_MASK) == AMOTION_EVENT_ACTION_UP) { : ここにコードを記述 : }
まずは AMotionEvent_getAction 関数で、 どんなタッチイベントなのか、今タッチされたのか、離されたのか、動かされたのか、 種類を取得し、いったん action に入れています。 Android Developer の Input の説明の、Enumerations の 41 にある値で、種別を判定可能です。
action の上位バイトには別の値が入っているようですから、 AMOTION_EVENT_ACTION_MASK で下位バイトだけ取り出し、 それが AMOTION_EVENT_ACTION_UP であったなら、離されたときです。
ここにテストコードを書けば、離したときに1回実行されますから、 連続で実行されてしまうことはなくなります。
投稿 April 5, 2021
改めて、少し理解が進んできた今、JNI とは何なのかを考えています。 とにかく Android アプリをネイティブコードで書こうとしているだけですので、 ニュアンスの取り違えなどがあるかもしれませんが、頭の整理ですので、ご了承ください。
まず、少なくとも、Android 開発に限った用語ではなく、 Java と C/C++ などのネイティブコードをつなぐ方法すべてを、こう呼ぶようです。
そして、Java をベースにしたコードから C/C++ コードを呼び出すのが、言ってみれば主流のようです。 Java で実行すると遅くなりがちなコード、その中でも主に画面への描画コードを、 C/C++ 側で実行して処理速度を高めよう、というのが一般のようです。
逆に、C/C++ で書かれたネイティブコードから Java の関数を呼び出すのも、 Java とネイティブコードをつなぐ、という意味で JNI と呼ばれているようです。 さらに、 プロジェクトに Java コードを置いてそれを実行する方法と、 C/C++ コードで Java の機能を実行する方法、どちらも JNI と呼ばれているような感じです。
このページでは、 C/C++ コードから、Java の機能を利用してウェブサイトを開く方法を中心としています。 また、Java コードを実行する方法についても触れています。
投稿 January 25, 2018, 見直し April 5, 2021
このあと扱うコードは、Java で書けば非常に簡単で、シンプルです。
Uri uri = Uri.parse("https://www.straightapps.com/"); Intent i = new Intent(Intent.ACTION_VIEW, uri); startActivity(i);
1行目、Uri クラスの parse 関数に開きたい URL を渡し、オブジェクト uri を初期化します。 Uri の parse 関数に 文字列 String を渡すと、 指定の URI 文字列をエンコードして Uri クラスが作成され、返されます。
2行目、Intent クラスを、コンストラクタに定数 ACTION_VIEW と uri を渡して、新しいオブジェクトを作成します。
3行目、startActivity 関数を呼び出して、実行します。
これをネイティブコードで実装するととても面倒なのですが、 何でも自由にやりたい思想のもと、完全ネイティブアプリを選んでしまったので、苦労は仕方ありません。
まずは JNI を利用するため、
JNIEnv* env; app->activity->vm->AttachCurrentThread(&env, NULL);
このあとずっと使う、env を取得します。 使い終わったら、DetachCurrentThread 関数を実行します。
NativeActivity のインスタンスは、 app->activity->clazz なので、 あとで使いやすいように、clazz に入れておきます。
jobject clazz = app->activity->clazz;
Uri クラスの parse 関数 を呼び出すためには複数の手順が必要ですが、 public static Uri parse (String uriString) と、 static で定義されているので、まだ簡単なほうです。
Java の Uri クラスは C/C++ では通用しませんので、その定義を取得します。
jclass classUri = env->FindClass("android/net/Uri");
Uri クラスは android.net.Uri として定義されていますので、 ドット区切りをスラッシュに替えた文字列を FindClass 関数に渡して、 jclass 型のクラス定義を取得します。
VC++2019 のデバッガを使って確認すると、0x19 のような値が classUri にセットされました。 FindClass 関数に渡す文字列が、スラッシュに置き換え忘れや大文字・小文字の違いなど、 1文字でも間違えている場合は 0x0 が返されますので、注意が必要です。 引数表記に使う Landroid/net/Uri; のような記述も、もちろんダメです。
Uri クラスの parse 関数(メソッド)を取得します。
jmethodID methodUriParse = env->GetStaticMethodID(classUri, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
GetStaticMethodID 関数に、 クラス定義、関数名、引数を渡すと、その関数(メソッド)にアクセスするための値が返されます。 デバッガで止めると、0x71047278 がセットされました。 あとで触れますが、このような大きな値が返されている場合は、 Windows でいうところでは、DLL に対するポインタ(アドレス)と同じと考えて良さそうです。
第1引数のクラス定義は、parse が static 関数なので FindClass 関数で得た値をそのまま渡せます。 static でない関数の場合は、コンストラクタを呼び出してオブジェクトを作成してからでないと、呼び出せません。
第2引数は関数名です。 大文字・小文字の区別がありますので、間違えると 0x0 が返されてしまいます。
第3引数は parse 関数の引数と戻り値で、これはずっと苦労します。 基本構成としては、カッコ内に引数を記述し、そのあとすぐに戻り値を記述します。 parse 関数の引数は String で、その定義は java.lang.String です。 複数の引数がある場合、カンマのような区切り文字はありません。 この場合は1つだけで、クラスなので、L ではじめ、 ; で終わるようにして、その間にドットをスラッシュに替えたクラス名を書きます。 戻り値も同じように、Uri の定義を L と ; の間に記述します。 ここは1つしか書かないので区切り不要にも思えますが、そう書くということです。
次に Intent クラスを取得します。 取得方法は、Uri クラスと同じです。
jclass classIntent = env->FindClass("android/content/Intent");
Intent クラスの定義は、android.content.Intent です。 大文字・小文字の違いはもちろん、関数の引数指定のように L や ; を付けたり、スラッシュに書き換え忘れると、正常に動作しません。 jclass は数値型で、試すと 0x5 がセットされました。 0x0 が返された場合は失敗であり、FindClass 関数に不正な文字列を渡すと、処理が戻らなくなるようです。 (ここでは行っていませんが)必ず結果をチェックしましょう。 また、これもあとで触れますが、小さい値はヒープメモリのアドレスのようなイメージみたいです。 Windows の HANDLE や HGLOBAL のような扱いと思えます。
取得した Intent クラスは定義であり、 インスタンスではないため、コンストラクタを実行して実体化が必要です。
Intent クラスのコンストラクタ、Intent( String action, Uri uri ) を取得します。 コンストラクタの関数名としては、<init> と書く決まりです。 基本的には Uri のときと同じですが、 static 定義ではないので、GetStaticMethodID ではなく、GetMethodID 関数を使用しなくてはなりません。
jmethodID methodIntentConst = env->GetMethodID(classIntent, "<init>", "(Ljava/lang/String;Landroid/net/Uri;)V");
引数 String は java.lang.String なので、 L と ; で囲み、ドットをスラッシュに置き換えて、カッコ内に記述します。 第2引数 Uri も同じように、android.net.Uri を、 引数の区切り文字なしに記述します。 戻り値はないので、void を意味する V を記述します。 基本型は英文字1字で表されるのですが、覚えきれませんので、いちいちネットで検索しています。 試してみると、0x70ec39d8 がセットされました。
コンストラクタの第1引数で渡す String として、 ACTION_VIEW 定数を取得する必要があります。 Android Developers には Constant Value: "android.intent.action.VIEW" と記載されていますので、次のように取得できるようです。
jstring jstrActionView = env->NewStringUTF("android.intent.action.VIEW");
開きたい URL 文字列を Uri の parse 関数に渡して、Uri を得ます。
jstring jstrUri = env->NewStringUTF("https://www.straightapps.com/");
URL 文字列 const char* を と等価のようです。 デバッガで止めると、0x35 がセットされました。
jobject objUri = env->CallStaticObjectMethod(classUri, methodUriParse, jstrUri);
Uri クラスの parse メソッドは static で定義されていて、戻り値は Uri クラスのオブジェクトです。 この場合は、CallStaticObjectMethod 関数で呼び出しを行います。 第1引数 Uri クラス定義の、第2引数 parse メソッドを、第3引数 jstrUri を引数に渡して実行し、 Uri オブジェクトを返してもらいます。 デバッガで止めると、0x41 がセットされました。
いよいよ、Intent i = new Intent(Intent.ACTION_VIEW, uri) の準備ができました。
jobject objIntent = env->NewObject(classIntent, methodIntentConst, jstrActionView, objUri);
新しいオブジェクトを作成する NewObject 関数に、 Intent クラスの定義、コンストラクタのメソッドを渡し、 その引数に ACTION_VIEW と uri を渡して実体化しています。 0x55 がセットされました。
最後に、startActivity 関数で、Intent を発行します。 この関数は Activity クラスにあります。
jclass classActivity = env->GetObjectClass(clazz);
ここで clazz は app->activity->clazz を置き換えただけですので、NativeActivity です。 GetObjectClass 関数は、引数で指定したオブジェクトのクラス定義を取得する関数ですので、 classActivity にクラス定義が入ります。 0x69 がセットされました。
jmethodID methodActivityStartActivity = env->GetMethodID(classActivity, "startActivity", "(Landroid/content/Intent;)V");
startActivity 関数は非 static で、Intent だけを引数で取り、戻り値はありません。 GetMethodID 関数に Activity クラスと startActivity 関数名を渡し、 第3引数のカッコ内で Intent クラスの指定を L と ; で囲んで記述、戻り値はありませんので void の V を記述します。 0x7109b4c0 がセットされました。
startActivity 関数を実行します。
env->CallVoidMethod(clazz, methodActivityStartActivity, objIntent);
static ではない関数で戻り値がないので、CallVoidMethod 関数を使います。 非 static なので、第1引数ではクラスの定義ではなく、オブジェクトを指定する必要があります。 第2引数に実行するメソッド、第3引数以降でメソッドの引数を指定します。
これで、デフォルトのブラウザが指定されている場合はそれで、 複数のブラウザがインストールされている場合はブラウザの選択が表示されることになります。
ここから、クリーンアップ作業です。 これを行わないと、メモリにゴミが残る可能性があります。
env->DeleteLocalRef(classIntent); env->DeleteLocalRef(objIntent); env->DeleteLocalRef(classUri); env->DeleteLocalRef(jstrUri); env->DeleteLocalRef(objUri); env->DeleteLocalRef(jstrActionView); env->DeleteLocalRef(classActivity);
jclass や jobject など新しく作成したものは、不要になったらシステムが削除してくれます。 同じものを複数の箇所で参照しているのにプログラムから削除されてしまうと問題がありますので、 「参照」という形を使って管理されています。 参照数が 0 になったら削除される、という仕組みです。
この DeleteLocalRef 関数は、参照を解除するものです。
私はまだ不慣れでしたので、いちいちデバッガのブレークポイントを設定し、値を確かめました。 そこからすると、メソッドのような削除不要のものは大きな値が返されており、 削除が必要な jclass や jstring などは小さな値となっています。 DeleteLocalRef 呼び出しが必要かどうかの判断は、面倒ですが、値を調べれば判断できそうです。
env の使用が終わったら、カレントスレッドからデタッチします。
app->activity->vm->DetachCurrentThread();
投稿 April 5, 2021
たった3行の Java コードを、C/C++ だけで実現するには、上記のように手間がかかります。 JNI の機能としては、Java コードをそのまま呼び出す手段も用意されています。
Java コードをネイティブプロジェクトに持たせるために、 AndroidManifest.xml の application タグにある、 android:hasCode="false" の部分を、 true に書き換えます。
<プロジェクト名>.Packaging に src フォルダを作成し、 java コードを置きます。 例えばファイル名を runBrowser.java にすると、 内容は次のようになります。
package com.straightapps.RunBrowser; import android.app.NativeActivity; import android.net.Uri; import android.content.Intent; class test { /** * ブラウザで指定の URL を開きます。 */ public static void runBrowser(NativeActivity app, String strUri) { Uri uri = Uri.parse(strUri); Intent i = new Intent(Intent.ACTION_VIEW, uri); app.startActivity(i); } }
1行目のパッケージ名には、唯一となるようなクラス名を指定する必要があります。 基本的にはプロジェクト名と同じが良さそうです。
続く import 文は、このあと Java に必要な機能の読み取りです。
class として、名前を付けます。 上記では test というクラス名にしています。
実行したい関数、ここでは runBrowser を定義しています。 アクティビティの app と、開きたい URL を受け取り、戻り値はない関数としています。 データは必要ないので、static で定義しています。
JNI で使用する env 取得のために、JVM をアタッチします。
JNIEnv* env; app->activity->vm->AttachCurrentThread(&env, NULL);
この先、前のセクションと同じ、クラスの取得やメソッドの取得などの説明は省略しています。
Activity にある getClassLoader 関数を取得、呼び出して、 Java コードを読み込むために使う ClassLoader オブジェクトを取得します。
jclass classActivity = env->GetObjectClass(app->activity->clazz); jmethodID methodGetClassLoader = env->GetMethodID(classActivity, "getClassLoader", "()Ljava/lang/ClassLoader;"); jobject objClassLoader = env->CallObjectMethod(app->activity->clazz, methodGetClassLoader);
getClassLoader 関数は、引数はなく、java.lang.ClassLoader の ClassLoader オブジェクトを返します。 static 定義ではなく、オブジェクトを返すので、CallObjectMethod 関数で実行します。
jclass classClassLoader = env->FindClass("java/lang/ClassLoader"); jmethodID methodLoadClass = env->GetMethodID(classClassLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
ClassLoader クラスの定義を取得し、 Java コードを読み込むための loadClass 関数を取得しています。 loadClass 関数は、 クラス名を渡すと、そのクラスを返してくれます。
jstring jstrClassName = env->NewStringUTF("com/straightapps/RunBrowser/test"); jobject objJavaRunBrowser = env->CallObjectMethod(objClassLoader, methodLoadClass, jstrClassName);
Java コードで指定したパッケージ名とクラス名を組み合わせた文字列を NewStringUTF 関数に渡し、Java の文字列型 jstring を得ています。 それを loadClass 関数に引数として渡し、そのクラスを取得しています。 jclass は jobject であり、CallObjectMethod 関数を使いますので、返されるのは jobject 型となります。 CallClassMethod という関数は、ありません。
jmethodID methodRunBrowser = env->GetStaticMethodID((jclass)objJavaRunBrowser, "runBrowser", "(Landroid/app/NativeActivity;Ljava/lang/String;)V");
test クラスに static で実装された、runBrowser 関数を取得します。 loadClass 関数が返した値は、実体はクラス jclass ですので、キャストしています。 引数はアクティビティと開きたい URL 文字列で、戻り値はありません。
jstring jstrUri = env->NewStringUTF("https://www.straightapps.com/"); env->CallStaticVoidMethod((jclass)objJavaRunBrowser, methodRunBrowser, app->activity->clazz, jstrUri);
開きたい URL 文字列は、NewStringUTF 関数で、jstring を作成できます。 準備ができましたので、CallStaticVoidMethod 関数を使って、呼び出します。
不要になったクラスなどのオブジェクトは、参照カウントを削除します。
env->DeleteLocalRef(classActivity); env->DeleteLocalRef(classClassLoader); env->DeleteLocalRef(objClassLoader); env->DeleteLocalRef(objJavaRunBrowser); env->DeleteLocalRef(jstrClassName); env->DeleteLocalRef(jstrUri);
env を使い終えたら、デタッチを実行して終わります。
app->activity->vm->DetachCurrentThread();
この呼び出しを汎用的にクラス化しようとしていますが、まだうまくまとまっていません。
Android 開発に関する記事をまとめた Android 開発トップ もご覧ください。