libGDX アプリケーションのライフサイクル

概要

libGDX アプリケーションのライフサイクルの説明です。 言語は Kotlin、Android/iOS アプリで動作確認をしています。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

libGDX のアプリケーションを作成するには ApplicationListener インターフェイスを実装したクラスを作成する。 ApplicationAdapter クラスは ApplicationListener インターフェイスを実装し、各メソッドのデフォルト実装を提供する。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.java.name
    }

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug(TAG, "create")
        //Gdx.graphics.isContinuousRendering = false
    }

    override fun pause() {
        Gdx.app.debug(TAG, "pause")
    }

    override fun resume() {
        Gdx.app.debug(TAG, "resume")
    }

    override fun resize(width: Int, height: Int) {
        Gdx.app.debug(TAG, "resize")
    }

    override fun render() {
        Gdx.app.debug(TAG, "render")
    }

    override fun dispose() {
        Gdx.app.debug(TAG, "dispose")
    }
}

ライフサイクルの図は The life cycle · libgdx/libgdx Wiki · GitHub を参照。 各操作に応じて呼ばれるメソッドは以下のとおり。

操作 Android iOS
アプリ起動 create, resize create, resize
ホーム pause pause
タスクリスト pause pause
pause状態でアプリ起動 resume, resize resume
タスクリストでアプリ起動 resume, resize resume
タスクリストでアプリ終了 dispose
戻る(アプリ終了) pause, dispose 操作不可
回転 resize resize, resize

(Android はタスクリストでアプリを終了しても dispose が呼ばれないのは仕様なのか?バグなのか? 呼ばれているがログに出ていないだけなのか?)

render メソッドは連続して呼ばれる。 使用しているハードウェアに応じて 1 秒間に 30-50-80 回の頻度で呼ばれる。 連続して呼び出される状態ではバッテリー消費が増える。 Gdx.graphics.isContinuousRendering = false により連続呼び出しを無効にできる。 無効にした場合は、以下のタイミングでのみ render メソッドが呼ばれる。

  • 入力イベントが発生した
  • Gdx.graphics.requestRendering メソッドが呼ばれた
  • Gdx.app.postRunnable メソッドが呼ばれた

UI Action (フェードイン、フェードアウトなど) では render メソッドが連続して呼ばれる。 デフォルトでは有効になっている。 stage.actionsRequestRendering = false により無効にできる。

(Java の場合、isContinuousRendering, actionsRequestRendering は set メソッド呼び出しの形式で書く。)

変更履歴

2016-11-05

以下のようにしていましたが、kotlin-refrect を追加すると APK サイズが大きくなるとの意見を見かけたので kotlin-reflect を使わないようにしました。 kotlin-reflect を追加すると APK サイズが 0.7MB 増えます。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.qualifiedName
    }

    ...
}

上記のコードを実行するためには build.gradle に kotlin-reflect への依存を追加する必要がある。 MyGdxGame::class.qualifiedName が kotlin-reflect を必要とする。

dependencies {
    ...
    compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

libGDX ロギング

概要

libGDX のロギング機能の使い方です。Android/iOS アプリで動作確認をしています。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

libGDX のログレベルは 3 段階。

  • ERROR
  • INFO
  • DEBUG

各ログレベルに対応したログ出力を行うメソッドが存在する。

  • ERROR : Gdx.app.error
  • INFO : Gdx.app.log
  • DEBUG: Gdx.app.debug

logLevel プロパティにより出力するログレベルを変更することができる。 実行環境によってデフォルトのログレベルが異なっている。 (logLevel を設定せずに実行すると Android/iOS で出力結果が異なる。)

使用例。

import com.badlogic.gdx.Application
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx

class MyGdxGame : ApplicationAdapter() {

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug("MyTag", "Debug level message")
        Gdx.app.log("MyTag", "Info level message")
        Gdx.app.error("MyTag", "Error level message")

        Gdx.app.logLevel = Application.LOG_ERROR
        Gdx.app.debug("MyTag", "Debug level message") // 出力されない
        Gdx.app.log("MyTag", "Info level message") // 出力されない
        Gdx.app.error("MyTag", "Error level message")
    }
}

ios-moe の出力結果。

MyTag 3 Debug level message
MyTag 4 Info level message
MyTag 6 Error level message
MyTag 6 Error level message

android の出力結果。logcat に出力される。

D/MyTag: Debug level message
I/MyTag: Info level message
E/MyTag: Error level message
E/MyTag: Error level message

Kotlin で iOS アプリを作る (libGDX 編)

概要

Intel Multi-OS Engine を使うことにより Android StudioiOS アプリを作成できます。 しかし、UI 部分は各 OS によってライブラリが異なるため、共通化できる部分は限られます。 そこで、ゲーム用フレームワーク libGDX を使って描画の共通化を計ります。 本記事では、プロジェクトの作成とサンプルアプリの起動までを行います。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

手順は下記のとおり。

  1. libGDX の Setup App (gdx-setup.jar) を libgdx からダウンロード
  2. Setup App を実行してプロジェクトを作成
  3. Android Studio にプロジェクトをインポート
  4. プロジェクトの設定を変更
  5. Android/iOS アプリ (Java版) を実行
  6. Kotlin に変換
  7. Android/iOS アプリ (Kotlin版) を実行

プロジェクトの作成

プロジェクトの作成では、Setup App (gdx-setup.jar) を起動して DestinationAndroid SDK のパス、Sub projects を指定する。 Destination は何処でも構わない。 今回は Android Studio にインポートするため、AndroidStudioProjects の配下に置くことにした。 Android SDK は適当なプロジェクトを開いた上で File > Project Structure... > SDK Location と辿ればパスを知ることができる。 Sub projects は AndroidIos-moe を選択する。

Generate を選択するといくつかのダイアログが表示される。

You have a more recent version of android build tools than recomended. Do you want to use your more recent version?

Android Build Tools のバージョンが推奨するバージョンよりも新しいが、新しいバージョンを使用するか? とのことなので「はい」を選択した。

You have a more recent Android API than the recomended. Do you want to use your more recent version?

Android API のバージョンが推奨するバージョンよりも新しいが、新しいバージョンを使用するか? とのことなので「はい」を選択した。

ダウンロードが行われるなど、暫く待つことになる。 遅いのは最初の1回だけ。

プロジェクトのインポート

作成されたプロジェクトの build.gradle を指定してインポート。

プロジェクトの設定変更

本記事執筆時点では、インポートすると ios-moe/java フォルダ配下の IOSMoeLauncher でエラーが発生する。 (libGDX version 1.9.4 で発生するが、1.9.5 になれば修正されると思われる。) エラーの理由は Pointer クラスのパッケージが異なることによる。 libGDX では com.intel.moe.natj.general.Pointer だが、MOE では org.moe.natj.general.Pointer に変更されている。 プロジェクトの build.gradle で gdxVersion を 1.9.4 から 1.9.5-SNAPSHOT に変更すればエラーはなくなる。

アプリの実行

Android Emulator / SimulatorApp でそれぞれ実行する。

Kotlin に変換

build.gradle に下記を追加する。

build.gradle (Project)

buildscript {
    ext.kotlin_version = '1.0.4'
    ... 
    dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

...

build.gradle (Module: android)

apply plugin: 'kotlin-android'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

build.gradle (Module: core)

apply plugin: 'kotlin'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

build.gradle (Module: ios-moe)

apply plugin: 'kotlin'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

android, core, ios-moe の各モジュールの java ディレクトリを選択して Code > Convert Java File to Kotlin File を実行する。

core モジュールの MyGdxGame クラスでエラーが発生する。 internalprivate lateinit に変更する。 internal lateinit でも構わないがアクセス修飾子はできる限り狭い範囲のみアクセスできるようにする。

class MyGdxGame : ApplicationAdapter() {
    private lateinit var batch: SpriteBatch
    private lateinit var img: Texture

    ...
}

ios-moe モジュールの IOSMoeLauncher クラスでは警告が発生する。 constructor の protected は削除する。 Kotlin では クラスが final 扱いとなるためである。 IOSMoeLauncher::class.java!!.getName()IOSMoeLauncher::class.java.name でよい。 get メソッドは Kotlin ではプロパティを使う。 !! は null があり得る場合に使用するが、null となることはないので不要。

再びアプリの実行

Android Emulator / SimulatorApp でそれぞれ実行する。 AndroidiOS で縦向き、横向きが異なる。 AndroidManifest.xml にて screenOrientation="sensor" とすれば Android も縦向きとなる。

Kotlin で iOS アプリを作る

概要

Kotlin で iOS アプリを作成します。 開発環境には Android Studio を使用し、Intel Multi-OS Engine を使って iOS アプリに変換して実行します。

確認環境

参考情報

解説

前提

Xcode, Android Studio, Kotlin Plugin はインストール済みとする。

Multi-OS Engine のインストール

Android Studio に Multi-OS Engine (MOE) Plugin をインストールする。

プロジェクトの作成

プロジェクトの作成手順は通常の Android プロジェクトと同じ。

Multi-OS Engine モジュールの作成

iOS アプリは Multi-OS Engine モジュールとして作成する。

モジュールを作成したら、実行可能な状態になっているので Run 'ios' を選択して実行する。 実機での実行は Developer Team ID が必須な様子(How to configure the Development Team id with MOE 1.2.0 - Support - Multi-OS Engine)。 Simulator では実行された。

Kotlin に変換

Multi-OS Engine モジュールのソースコードJava で書かれている。 Java を Kotlin に変換する。

まずは、build.gradle に Kotlin の設定を加える。 以下を行うとプロジェクトと app (Android アプリ) モジュールに Kotlin の設定が追加される。

ios (Multi-OS Engine モジュール) の build.gradle にはエディタで以下を追記する。

apply plugin: 'kotlin'

dependencies {
    ...
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

次に、Javaソースコードを Kotlin に変換する。

app (Android アプリ) モジュールは java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換される。

ios (Multi-OS Engine モジュール) でも java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換されるが、一部編集が必要。

変換前の Java ファイルと変換後の Kotlin ファイルを記載する。

Main.java

@RegisterOnStartup
public class Main extends NSObject implements UIApplicationDelegate {

    public static void main(String[] args) {
        UIKit.UIApplicationMain(0, null, null, Main.class.getName());
    }

    @Selector("alloc")
    public static native Main alloc();

    protected Main(Pointer peer) {
        super(peer);
    }

    private UIWindow window;

    @Override
    public boolean applicationDidFinishLaunchingWithOptions(UIApplication application, NSDictionary launchOptions) {
        return true;
    }

    @Override
    public void setWindow(UIWindow value) {
        window = value;
    }

    @Override
    public UIWindow window() {
        return window;
    }
}

Main.kt

@RegisterOnStartup
class Main protected constructor(peer: Pointer) : NSObject(peer), UIApplicationDelegate {

    companion object {

        @JvmStatic fun main(args: Array<String>) {
            UIKit.UIApplicationMain(0, null, null, Main::class.java.name)
        }

        @Selector("alloc")
        @JvmStatic external fun alloc(): Main
    }

    private var window: UIWindow? = null

    override fun applicationDidFinishLaunchingWithOptions(application: UIApplication?, launchOptions: NSDictionary<*, *>?): Boolean {
        return true
    }

    override fun setWindow(value: UIWindow?) {
        window = value
    }

    override fun window(): UIWindow? {
        return window
    }
}

alloc 関数では override を削除し @JvmStatic external を追加する。 window 関数の戻り値の型が UIWindow になっているため UIWindow? に変更する。

AppViewController.java

@org.moe.natj.general.ann.Runtime(ObjCRuntime.class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
public class AppViewController extends UIViewController {

    @Owned
    @Selector("alloc")
    public static native AppViewController alloc();

    @Selector("init")
    public native AppViewController init();

    protected AppViewController(Pointer peer) {
        super(peer);
    }

    public UILabel statusText = null;
    public UIButton helloButton = null;

    @Override
    public void viewDidLoad() {
        statusText = getLabel();
        helloButton = getHelloButton();
    }

    @Selector("statusText")
    @Property
    public native UILabel getLabel();

    @Selector("helloButton")
    @Property
    public native UIButton getHelloButton();

    @Selector("BtnPressedCancel_helloButton:")
    public void BtnPressedCancel_button(NSObject sender){
        statusText.setText("Hello Intel Multi-OS Engine!");
    }
}

AppViewController.kt

@org.moe.natj.general.ann.Runtime(ObjCRuntime::class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
class AppViewController protected constructor(peer: Pointer) : UIViewController(peer) {

    @Selector("init")
    external override fun init(): AppViewController

    val statusText: UILabel
        get() = getStatusTextSel()

    val helloButton: UIButton
        get() = getHelloButtonSel()

    override fun viewDidLoad() {}

    @Selector("statusText")
    @Property
    external fun getStatusTextSel(): UILabel

    @Selector("helloButton")
    @Property
    external fun getHelloButtonSel(): UIButton

    @Selector("BtnPressedCancel_helloButton:")
    fun BtnPressedCancel_button(sender: NSObject) {
        statusText.setText("Hello Intel Multi-OS Engine!")
    }

    companion object {

        @Owned
        @Selector("alloc")
        @JvmStatic external fun alloc(): AppViewController
    }
}

alloc 関数は override を削除して @JvmStatic external とする。 実装の無い Selector は external を付ける。 関数名を getStatusText とするとプロパティの statusText と衝突してしまうため getStatusTextSel としている。 自動変換では getLabel 関数が label プロパティに変換されてしまうので修正する。 statusText, helloButton プロパティは var から val に変更し、get をオーバーライドしている。 Java と同様に viewDidLoad 関数が呼ばれたタイミングでフィールドにオブジェクトを保持したい場合は lateinit var を使えば良い。

以上で変換は完了となるため、再度実行して動作することを確認する。

共通モジュールの作成

app (Android モジュール) と ios (Multi-OS Engine モジュール) で共有するモジュール (Java モジュール) を作成する。 app と同様に Kotlin に変換できる。

app と ios モジュールの依存関係に common モジュールを追加する。

以上により共通モジュールのクラスを app と ios モジュールから使用できる。

今後の予定

  • iOS アプリの UI 編集方法の検証
  • 通化できる範囲の検証

Kotlin の書籍

Android の実行環境

概要

Android のアプリを動かしている仕組みを整理します。 全体像を概観することを目的としています。 細かな部分は推測になっている所があります。 (間違いがあれば教えてください。)

参考情報

解説

Android の全体構成

全ての土台は Android 用に拡張した Linux Kernel。 Linux Kernel が持つメモリ管理、プロセス管理、権限によるセキュリティモデル、ドライバモデル、共有ライブラリの仕組みを使う。 ディスプレイ、カメラ、USB などのデバイスドライバ、共有メモリドライバ(Shared Memory Driver)、電源管理(Power Management)、プロセス間通信(Inter-Process Communication; IPC)のための Binder Driver を含む。 Binder Driver については KMC Staff Blog:AndroidのBinderによるプロセス間のメソッド呼び出し(メモ) が参考になる。 プロセス間通信は、startActivity, sendBroadcast などにより他のアプリのコンポーネントと通信する際に使われる。 Android におけるプロセス構成については後述。

Linux Kernel に Libraries 層が積み重なり、Linux Kernel の機能を使って様々な機能を実現する。 Libc (C 言語用の基本機能)、WebKit (HTML レンダリングエンジン)、SQLite (リレーショナルデータベース)、OpenGL|ES (組み込み向け 3 次元グラフィックス)、Surface Manager (描画)、Audio Manager (音源再生)などの機能を含む。 Libc は GNU Libc ではなく Bionic Libc。 Bionic Libc は組み込み向けの Libc で、GNU Libc とは互換性がない。

Libraries の一部に Android Runtime が含まれる。 以前は Dalvik 仮想マシン (Dalvik VM) を使用していたが、Android 5.0 からは ART (Android RunTime) に変更されている。 本記事中では、Dalvik VM と ART を総称して Android Runtime としている。 Android Runtime については後述。

Libraries 層に Application Framework 層が積み重なり、Java API を提供する。 Libraries はネイティブコードで提供されており、Application Framework 層が Java コードから各機能を使用できるようにする。

これら様々な層(機能)を土台として Applications は動作している。

Android のプロセス構成

プロセスはプログラムの実行単位。 基本的に、メモリ領域はプロセス単位で確保され、プロセスが異なるメモリ領域にはアクセスできない。 Android の主要なプロセスと各プロセスを構成するモジュールは下図のとおり。

Anatomy & Physiology of an Android の内容を基に、推測により一部編集を加えている。 Service Manager は個別のプロセスではなく Runtime Process 内で動作すると推測。 Surface Flinger, Audio Flinger は赤色で示しているページがあり Driver の一種と推測し、Libraries 層としては Surface Manager, Audio Manager とする方が適切と判断。 黄色の Runtime は元資料では Dalvik VM だが ART も同様であると推測し、黄色の Rutime として総称。 Home, Contacts は Application の一例であるため、Application で総称。 外側の角四角はプロセス、内側の丸四角はモジュールと推測。

プロセスの起動順序

ブートローダーは Linux Kernel をロードし、Linux Kernel は Init プロセスを起動する。 Init プロセスはバックグラウンドサービスを提供する様々な Daemon プロセスを起動する。 例えば、USB Daemon, Android Debug Bridge Daemon, Debugger Daemon など。

続いて、Init プロセスは Zygote プロセスを起動する。 Zygote プロセスは起動時にアプリケーション実行に必要なライブラリ一式をロード(メモリ領域に展開)する。 Zygote プロセスは Android Runtime のインスタンスを保持し、アプリの起動要求に応じて自身を複製して新しい Android Runtime インスタンスを生成する。 これにより、Android Runtime インスタンスの生成を高速化する。 また、copy-on-write の仕組みにより、メモリへの書き込みが発生する場合のみメモリ領域の複製を行うため footprint の最小化 (メモリ使用量の削減) につながる。 Zygote の仕組みが無い場合にはアプリの起動要求の度にライブラリ一式がロードされるため、ロード時間が必要となり、ロードされたライブラリ一式がメモリ領域を消費する。Zygote については KMC Staff Blog:AndroidでのJavaプログラムの起動やZygoteまわりのメモ が参考になる。

Zygote プロセスの次は Runtime プロセスを起動する。 Runtime プロセスは Service Manager を初期化する。 Service Manager はサービスの登録と検索を担う。

Runtime プロセスは Zygote プロセスに起動要求を送信し、Android Runtime インスタンス(新規プロセス)を生成する。 生成されたプロセスで各種 System Service を起動する。 Surface Manager, Audio Manager などのネイティブシステムサービス、 Activity Manager, Window Manager, Location Manager などの Android システムサービスが System Service に含まれる。 (各種 System Serivce が同一プロセスで動作するのか、別プロセスで動作するのかは読み取れなかったが、1 つの角四角に含まれるので同一プロセスと推測。)

以上で、システムの起動が完了。

アプリの起動は、Zygote プロセスに起動要求を送信し Android Runtime インスタンスを生成することにより行われる。 つまり、アプリ毎にプロセスが生成される。

アクセス権

Android では Linux のマルチユーザーシステムを用いてアクセス権を管理する。

Android システムはアプリ毎に Linux ユーザー ID を割り振る。 アプリのファイルとプロセスは割り振られたユーザーを所有者とする。 ファイルのアクセス権は Android システムにより、プロセスとファイルの所有者が異なる場合にはアクセスできないように設定される。

プロセスはアプリ毎に生成されており、異なる所有者が設定されている。 これにより、他のアプリのファイルやメモリ領域にはアクセスできないようになり、安全性が高まる。

Android Runtime

Android Runtime は Android アプリの実行環境を意味する。 Android 4.x までは Dalvik VMAndroid 5.0 以降は ART が Android Runtime に該当する。

Android Runtime は Zygote から複製された後、アプリの共有ライブラリをロードする。 ロードされるファイルは Dalvik VM と ART で異なる。

緑四角はネイティブコード、青四角は非ネイティブコード、緑文字は環境依存、青文字は環境非依存を意味する。

odex ファイルは機種に合わせて最適化した dex ファイル。 odex ファイルの内容はネイティブコードではないが、機種に依存した内容となる。 ネイティブコードへは実行時に JIT (Just In Time) コンパイルされる。

インテント (Intent) とインテントフィルタ (intent-filter)

概要

Android の基礎的な仕組みである Intent について整理します。 下記のページの内容を私なりに整理したものです。

developer.android.com

解説

コンポーネント

Intent はコンポーネント間の通知に使われる。 Intent の日本語訳は、意図や意思。 Intent はコンポーネント間を伝わる意思ということ。

Android におけるコンポーネントは下記の 4 種類 (アプリケーションの基礎 | Android Developers 参照)。

  • アクティビティ (Activity)
  • サービス (Service)
  • ブロードキャストレシーバー (BroadcastReceiver)
  • コンテンツプロバイダー (ContentProvider)

ContentProvider を除く 3 種類のコンポーネントは Intent によりアクティベート(起動、機能を有効化)される。 本記事では便宜上、Intent によりアクティベートされる 3 種類をコンポーネント、アクティベートを起動と呼ぶ。

コンポーネントを起動する

コンポーネント (Activity, Service, BroadcastReceiver) を起動するメソッドは下記のとおり。

  • Activity
    • startActivity
    • startActivityForResult : Activity から結果を受け取りたい場合に使用
  • Service
    • startService
    • bindService : Service と接続して通信したい場合に使用
  • BroadcastReceiver
    • sendBroadcast
    • sendOrderedBroadcast
    • sendStickyBroadcast

各メソッドは引数に Intent を受け取る。 Intent によって起動するコンポーネントを指定する。

Intent オブジェクトの生成

Intent クラスのコンストラクタは下記の 6 つ。

  • Intent()
    • 万能
  • Intent(Intent o)
    • Intent オブジェクトのコピー
  • Intent(String action)
  • Intent(String action, Uri uri)
  • Intent(Context packageContext, Class<?> cls)
  • Intent(String action, Uri uri, Context packageContext, Class<?> cls)

最初の 2 つを除くコンストラクタを分類すると 3 通りになる。

  • Context packageContext, Class<?> cls を引数にとるコンストラクタ
    • 明示的 Intent として生成する
  • String action を引数にとるコンストラクタ
    • アクションを指定して生成
  • Uri uri を引数にとるコンストラクタ
    • データの URI を指定して生成

明示的 Intent としての指定、アクション指定、データの URI 指定はコンストラクタ以外でも指定可能である。

Intent に指定する情報

  • コンポーネント
    • クラスの完全修飾名 (例えば、"com.example.project" + "PingPongActivity")
    • コンストラクタの cls で指定
    • setComponent、setClass、setClassName メソッドで指定
    • 指定すると明示的 Intent として扱われる
  • アクション
    • 実行する内容を意味する文字列 ("パッケージ名.アクション名" とする。例えば、"com.example.project.ACTION_FLY")
    • 一般的なアクションは定義済み (Intent | Android Developers 参照)
    • コンストラクタの action で指定
    • setAction メソッドで指定
  • データ
    • データの参照先 URI
      • アクション毎に指定できる URI が決まっている (どこみればわかる?)
    • コンストラクタの uri で指定
    • setData メソッドで指定
    • データには MIME タイプを指定する場合がある
      • データの MIME タイプを指定する場合には setType を使う
      • URIMIME タイプの両方を指定する場合は必ず setDataAndType を使う (setData と setType を別々に使うのではなく)
      • MIME タイプは URI から推測されて自動的に指定される場合がある
  • カテゴリ
    • コンポーネントの種類を表す文字列 ("パッケージ名.カテゴリ名" とする。例えば、"android.intent.category.LAUNCHER")
    • 一般的なカテゴリは定義済み (Intent | Android Developers 参照)
    • addCategory メソッドで指定
    • startActivity, startActivityForResult では、Intent に "android.intent.category.DEFAULT" が自動的に指定される
  • エクストラ
    • 任意の追加情報 (キーと値のペア)
      • アクション毎に指定できるキー(と値の型)が決まっている
      • キーは "パッケージ名.キー名" とする (例えば、"com.example.project.EXTRA_INTO_RIVER")
    • 一般的なキーは定義済み (Intent | Android Developers 参照)
    • putExtra, putExtras, put[Type]ArrayListExtra メソッドで指定
  • フラグ

Intent のフラグ

startActivity, startActivityForResult に渡す Intent で指定するフラグ

  • FLAG_ACTIVITY_BROUGHT_TO_FRONT
  • FLAG_ACTIVITY_CLEAR_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
  • FLAG_ACTIVITY_FORWARD_RESULT
  • FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
  • FLAG_ACTIVITY_MULTIPLE_TASK
  • FLAG_ACTIVITY_NEW_DOCUMENT
  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_NO_ANIMATION
  • FLAG_ACTIVITY_NO_HISTORY
  • FLAG_ACTIVITY_NO_USER_ACTION
  • FLAG_ACTIVITY_PREVIOUS_IS_TOP
  • FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
  • FLAG_ACTIVITY_REORDER_TO_FRONT
  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_TASK_ON_HOME

sendBroadcast, sendOrderedBroadcast, sendStickyBroadcast に渡す Intent で指定するフラグ

  • FLAG_RECEIVER_REGISTERED_ONLY

共通のフラグ?

  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_GRANT_WRITE_URI_PERMISSION
  • FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  • FLAG_GRANT_PREFIX_URI_PERMISSION
  • FLAG_DEBUG_LOG_RESOLUTION
  • FLAG_FROM_BACKGROUND

Intent の種類

明示的 Intent

コンポーネントが指定された Intent は明示的 Intent となる。 明示的 Intent では指定したコンポーネントが起動される。

暗黙的 Intent

コンポーネントが指定されていない Intent は暗黙的 Intent となる。 Intent に指定したアクション、データ、カテゴリと各コンポーネントの intent-filter でマッチングを行い、起動するコンポーネントを実行時に決定する。 マッチングを行った結果、コンポーネントが見つからない場合や複数見つかる場合がある。

Service では暗黙的 Intent を使用せず、intent-filter を定義しないこと。 Service で暗黙的 Intent と intent-filter を使用すると、ユーザーは使用される Service を把握できず、セキュリティ上の危険を伴う。 bindService メソッドに暗黙的 Intent を渡すと例外がスローされる (API Level ≧ 21)。

コンポーネントが見つからない場合

startActivity, startActivityForResult を呼び出し、コンポーネントが見つからなかった場合にはアプリがクラッシュする。 クラッシュしないようにするために resolveActivity メソッドにより、Intent に応答できるコンポーネントの存在を確認する。

val sendIntent = Intent(Intent.ACTION_SEND)
...
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(sendIntent)
}

Service は暗黙的 Intent では使用されず、BroadcastReceiver ではいずれの BroadcastReceiver も通知を受け取らない。

コンポーネントが複数見つかる場合

startActivity, startActivityForResult を呼び出し、コンポーネントが複数見つかった場合には下記のいずれかの振る舞いをする。

  • アプリチューザ (アプリ選択ダイアログ) が表示され、ユーザーに Activity の選択を促す
  • ユーザーがアクションに対して選択したデフォルトの Activity が選択される

アクションに対するデフォルトの Activity が選択されている場合でも、強制的にアプリチューザを表示することができる。

val sendIntent = Intent(Intent.ACTION_SEND)
...
val chooser = Intent.createChooser(sendIntent, "Chooser Title");
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(chooser)
}

Service は暗黙的 Intent では使用されず、BroadcastReceiver ではマッチングに成功した全ての BroadcastReceiver が通知を受けとる。

マッチングの仕組み

AndroidManifest.xml の各コンポーネント(activity, receiver 要素)に intent-filter 要素を追加することで、各コンポーネントが受信する Intent を指定する。 BroadcastReceiver の intent-filter は Context クラスの registerReceiver, unregisterReceiver メソッドで動的に変更できる。 動的に変更することでアプリ実行中のみ受信できる。 Service は暗黙的 Intent では使用しないため、intent-filter 要素を追加しないこと。

intent-filter 要素は各コンポーネント(activity, receiver 要素)に複数指定できる。 複数指定した場合は or 条件として、Intent がいずれかの intent-filter にマッチした場合にコンポーネントが起動される。

intent-filter 要素には、action, category, data 要素を含むことができる。 各要素は intent-filter 要素内に複数指定できる。

<activity android:name="PingPongActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="com.example.project.ACTION_FLY"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.LAUNCHER" />
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*" android:scheme="http"/>
        <data android:scheme="content" android:host="project.example.com" android:port="200" android:path="folder/subfolder/etc"/>
    </intent-filter>
</activity>

action, category, data 要素のマッチングは複雑であり、文章で説明すると曖昧になるため擬似コードでの整理を試みる。

intent match filter ::=
(
    (
        intent.action == filter.action[0]
        or
        intent.action == filter.action[1]
        or
        ...
    )
    and
    intent.category[0] match filter.category
    and
    intent.category[1] match filter.category
    and
    ...
    and
    (
        intent.data match filter.data[0]
        or
        intent.data match filter.data[1]
        or
        ...
    )
)

intent.category[i] match filter.category ::=
(
    intent.category[i] == filter.category[0]
    or
    intent.category[i] == filter.category[1]
    or
    ...
)

intent.data match filter.data[i] ::=
(
    (
        intent.data.mimeType == null and filter.data[i].mimeType == null
        or
        intent.data.mimeType != null and intent.data.mimeType match filter.data[i].mimeType
    )
    and
    (
        intent.data.uri == null and filter.data[i].uri == null
        or
        intent.data.uri != null and intent.data.uri match filter.data[i].uri
    )
)

intent.data.uri match filter.data[i].uri ::=
(
    (filter.data[i].uri.scheme == null or intent.data.uri.scheme == filter.data[i].uri.scheme)
    and
    (filter.data[i].uri.host == null or intent.data.uri.host == filter.data[i].uri.host)
    and
    (filter.data[i].uri.port == null or intent.data.uri.port == filter.data[i].uri.port)
    and
    (filter.data[i].uri.path == null or intent.data.uri.path match filter.data[i].uri.path)
)

intent-filter の補足事項。

data 要素の path, mimeType 属性では、* (ワイルドカード) 指定が可能。 部分一致が可能なので、上記の擬似コードでは == ではなく match としている。

data 要素の scheme, host, port, path 属性は下記に示す組み合わせのみ有効。

data に mimeType 属性のみが指定されている場合、scheme 属性に content, file が指定されているとして扱われる。 つまり、下記の 2 つは同じ。

<data mimeType="text/plain">
<data mimeType="text/plain" scheme="content">
<data mimeType="text/plain" scheme="file">

Intent の補足事項。

startActivity, startActivityForResult では Intent に CATEGORY_DEFAULT が追加されるので、 activity の intent-filter の category に CATEGORY_DEFAULT を指定する必要がある。

Intent で MIME タイプ (mimeType) を指定していなくても、data の URI から推測されて指定されることがある。

コンポーネントを探す

Intent を受け入れることができるコンポーネントは PackageManager クラスの下記のメソッドで探せる。

  • queryIntentActivities(Intent intent, int flags)
  • queryIntentServices(Intent intent, int flags)
  • queryBroadcastReceivers(Intent intent, int flags)

Intent に応答できる最適なコンポーネントは PackageManager クラスの下記のメソッドで探せる。 (最適とは?)

  • resolveActivity (Intent intent, int flags)
  • resolveService (Intent intent, int flags)

PendingIntent

別のアプリケーションに Intent を実行させる場合には PendingIntent を使う。 別のアプリケーションの代表例は、通知、ウィジェット、アラーム。

PendingIntent は下記のメソッドにより取得する。

  • Activity
    • PendingIntent.getActivity
  • Service
    • PendingIntent.getService
  • Broadcast
    • PendingIntent.getBroadcast

使用例。

val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val notification = NotificationCompat.Builder(context).apply {
    setContentIntent(pendingIntent)
    ...
}.build()

補足

マッチングの仕組みの導出

コードでの整理に辿り着くまでの過程。

アクションのテストから。

Intent で指定されたアクションが、フィルタにリストされたアクションのいずれかに一致する必要があります。(*1)

フィルタのリストにアクションが 1 つもない場合は、インテントに一致するものがないため、すべてのインテントがテストに失敗します。(*2)

Intent がアクションを指定していない場合は、テストに合格します(フィルタに少なくとも 1 つのアクションが含まれている必要があります)。(*3)

filter.action intent.action match 引用との対応
null null false *3, 実際に試すとダメ
null not null false *2
not null null false *1
not null not null いずれかに一致 *1

カテゴリのテストから。

インテントがカテゴリのテストに合格するには、Intent 内のすべてのカテゴリが、フィルタ内のカテゴリに一致する必要があります。

カテゴリのないインテントは、フィルタで宣言されているカテゴリに関係なく、常にこのテストに合格することになります。

(intent.category[0] ...) and (intent.category[1] ...)。

インテント フィルタでは、Intent で指定されたカテゴリよりも多くのカテゴリが宣言されている場合もあるため、すべてが Intent に一致しなくてもテストには合格します。

(intent.category[0] == filter.category[0]) or (intent.category[0] == filter.category[1])。

検証。

filter.category intent.category match
null null true
null A false
A null true
A A true
A A,B false

データのテストから。

スキームが指定されていない場合、ホストは無視されます。

ホストが指定されていない場合、ポートは無視されます。

スキームとホストの両方が指定されていない場合、パスは無視されます。

全ての組み合わせを網羅して、無視される部分を () で囲むと

scheme, host, port, path
scheme, host, port
scheme, host, path
scheme, host
scheme, (port), path
scheme, (port)
scheme, path
scheme
(host), (port), (path)
(host), (port)
(host), (path)
(host)
(port), (path)
(port)
(path)

無視される場合は指定されていないことになると解釈している。 無視される場合を整理すると

scheme, host, port, path
scheme, host, port
scheme, host, path
scheme, host
scheme, path
scheme

インテントURI をフィルタの URI 仕様に比較するときは、フィルタに含まれる URI の一部でのみ比較されます。

フィルタでスキームのみが指定されている場合、そのスキームを持つすべての URI がフィルタに一致します。

フィルタでスキームと認証局が指定されていて、パスが指定されていない場合、パスにかかわらず同じスキームと認証局を持つすべての URI がフィルタを通過します。

フィルタでスキーム、認証局、パスが指定されている場合、同じスキーム、認証局、パスを持つ URI のみがフィルタを通過します。

intent.data = content::example.com:200/folder/subfolder として検証。

scheme host port path match
content example.com 200 /folder/subfolder true
content example.com 200 null true
content example.com null /folder/subfolder true
content example.com null null true
content null 200 /folder/subfolder IDE error
content null 200 null IDE error
content null null /folder/subfolder IDE error
content null null null true
null example.com 200 /folder/subfolder IDE error
null example.com 200 null IDE error
null example.com null /folder/subfolder IDE error
null example.com null null IDE error
null null 200 /folder/subfolder IDE error
null null 200 null IDE error
null null null /folder/subfolder IDE error

先の整理した内容と比較すると scheme, path の組み合わせのみ期待と異なる。

scheme, host, port, path
scheme, host, port
scheme, host, path
scheme, host
scheme, path // 指定できない
scheme

intent-filter で指定した属性が 1 つでも一致しなかった場合には、data 要素全体として一致しなかったことになる。

intent.data = content::example.com:200/folder/subfolder として検証。

scheme host port path match
content example.com 200 /mismatch false
content example.com 80 /folder/subfolder false
content mismatch.com 200 /folder/subfolder false
http example.com 200 /folder/subfolder false

URIMIME タイプも含まないインテントは、フィルタで URIMIME タイプが指定されていない場合のみテストをパスします。

URI を含んでいて MIME タイプを含んでいないインテント(明示的にも含まれておらず、URI からも推測できない)場合は、URI がフィルタの URI 形式に一致し、フィルタが MIME タイプを指定していない場合のみテストをパスします。

MIME タイプを含んでいて、URI を含んでいないインテントは、フィルタのリストに同じ MIME タイプがあり、URI 形式が指定されていない場合のみテストをパスします。

URI と MINE タイプの両方を含む(明示的か、URI からの推測)インテントは、MIME タイプがフィルタのリストにあるタイプに一致した場合のみ、テストの MIME タイプのパートをパスします。 テストの URI のパートは、URI がフィルタの URI に一致するか、content: URI か file: URI があって URI が指定されていない場合にパスできます。つまり、フィルタにリストに MIME タイプのみがある場合、コンポーネントは content: データと file: データをサポートすると推定されます。

次の条件で検証。

結果。

filter.mimeType filter.uri intent.mimeType intent.uri match
not null not null not null not null 共に一致すれば
not null not null not null null false
not null not null null not null false
not null null not null not null false
null not null not null not null false

更に別の条件で検証。

  • intent
    • uri = "content://example.com:200/folder/subfolder"
    • mimeType = "text/plain"
  • filter
    • uri = scheme="content", host="example.com", port="200", path="/folder/subfolder"
    • mimeType = "text/plain"

結果。

filter.mimeType filter.uri intent.mimeType intent.uri match
not null not null not null not null 共に一致すれば
not null not null not null null false
not null not null null not null false
not null null not null not null true
null not null not null not null false

filter.uri を省略した場合は、scheme="content" と scheme="file" が指定されている扱いとなる。

Android で SQLite を使う

概要

AndroidSQLite を使う方法です。 サンプルにはデータの参照、挿入、トランザクションを含みます。

参考情報

サンプルコード

解説

  • Database.kt
    • Database : SQLiteOpenHelper を実装して汎用的な処理を行うクラス
  • SQLiteDatabaseExtensions.kt : update, insert メソッドの引数を省略できるようにするための拡張関数
  • SampleDB.kt
    • SampleDB : サンプルデータベースの定義
    • SampleDB.Dao : サンプルデータベースが持つ DAO の一覧
  • SampleTableDao.kt
    • SampleTableDao : DAO (Data Access Object)
  • Client.kt
    • Client : 上記のクラスを使用して処理を行うクラス

AndroidSQLite を使うには SQLiteOpenHelper を使用する。 Database にて SQLiteOpenHelper を使用している。 データベースファイルが存在しない場合には onCreate メソッドが呼ばれるため、onCreate メソッドが呼ばれたらテーブルを作成する。 テーブルの作成は SampleDB.Dao に処理を委譲して実現している。 SampleDB.Dao は SampleTableDao に処理を委譲する。 複数の DAO がある場合には、複数の DAO に処理を委譲する。 データベースのバージョンが変更された場合には onUpgrade メソッドが呼ばれるが、今回のサンプルでは未実装。

// Database
private val helper = object : SQLiteOpenHelper(context, def.name, def.cursorFactory, def.version) {

    override fun onCreate(db: SQLiteDatabase) {
        def.create(db).onCreate()
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
}

CRUD 操作は、SQLiteOpenHelper から readableDatabase と writableDatabase を取得して行う。 readableDatabase と writableDatabase は共に SQLiteDatabase のインスタンス。 SQLiteDatabase は query メソッドや insert メソッドを持つ。 query メソッドや insert メソッドの呼び出しは、SampleTableDao の仕事。 query メソッドと insert メソッドは多くの引数を持ち、全ての引数を指定する必要のない場合がある。 引数を省略できるようにするために SQLiteDatabaseExtensions.kt で拡張関数を定義している。

// Database
helper.readableDatabase.use {
    ...
} // use 関数により readableDatabase は close() される

helper.writableDatabase.use {
    ...
} // use 関数により writableDatabase は close() される
// SampleTableDao
fun insert(seqId: Int, time: Int, type: Int) {
    val values = ContentValues().apply {
        put("seq_id", seqId)
        put("time", time)
        put("type", type)
    }
    db.insert(TABLE_NAME, values) // SQLiteDatabaseExtensions.kt で定義された拡張関数
}

Client では CRUD 操作を行う関数を定義し、Database の update メソッドに渡す。 関数のブロックがトランザクションに対応する。 例外を発生させることなく関数を終えた場合にはコミットされ、例外が発生した場合にはロールバックされる。 トランザクション管理は Database で行っている。 update メソッドでは、関数実行前に beginTransaction() を実行、実行後に endTransaction() を実行している。 例外が発生しなかった場合には setTransactionSuccessful() を実行してコミットされるようにする。

// Database
// it は SQLiteDatabase のインスタンス
it.beginTransaction()
try {
    val result = procedure(def.create(it))
    it.setTransactionSuccessful()
    return result
}
finally {
    it.endTransaction()
}

update メソッドが受け取る関数では、引数に SampleDB.Dao を受け取る。 SampleDB.Dao が保持する SampleTableDao に処理を委譲することで CRUD 操作が行われる。 テーブルが増えた場合には SampleDB.Dao に各テーブルの DAO を加える。

// Client
db.update { // トランザクション開始
    // it は SampleDB.Dao のインスタンス
    val seqId = it.sampleTable.nextSeqId()
    it.sampleTable.insert(seqId, time, type)
} // トランザクション終了 (例外が発生しなければコミットされる)

Kotlin 補足

Kotlin は全く知らないという方のための補足。

use 関数

下記のコードは同じこと。

// Kotlin
helper.writableDatabase.use {
    it.beginTransaction()
    ...
}
// Java
try (SQLiteDatabase it = helper.getWritableDatabase()) {
    it.beginTransaction()
    ...
}