このページの本文へ

スマホと車・バイクを連携させる新規格SDLのすべて 第4回

初心者でもできるSDLアプリの作り方、初級編その1

まずはSDLの車載機シミュレーター上に画面を出してみよう

2019年10月16日 09時00分更新

文● 深見浩和 編集●アスキー編集部

提供: SDLコンソーシアム

  • この記事をはてなブックマークに追加
  • 本文印刷

 クルマとスマホをつなぐ規格である「SDL」。賞金総額100万円の大規模なアプリコンテスト「SDLアプリコンテスト2019」が今年も開催されるが、実はSDL対応アプリを作るのは、それほど難しいことではない。
 極めて簡単で、1行もプログラムを書いたことがない人でも楽勝!
 ――とまでは言えないが、本連載ではこれから、SDLとはどういったものなのか、対応アプリはどうやって作ればいいのかを解説する。そのなかで、基本的な要素のサンプルコードは掲載していくので、それらを参考に(もっと言えばコピペ)しつつ、ぜひ自分の思い付いたアイデアを実現していただきたい。

 SDL対応アプリの開発に何が必要になるのかは、前回の記事で解説しました。ここからは、実際のコードを紹介しつつ、AndroidでのSDL対応アプリの作り方を解説していきます。

 アプリの基本的な部分は、本記事およびGitHubで公開しているコードをそのままコピー&ペーストすれば作れますので、そこへご自分なりのアイデアを追加してみて下さい!

プロジェクトを作り、SDLのライブラリーを追加する

 本記事では、開発環境として「Android Studio」を、また言語としては「Kotlin」を使用します。もっとも、基本的な内容ですので、Javaで開発する際も、本記事の内容はほぼ応用できます。

 まず最初に、通常のスマートフォン/タブレット向けAndroidアプリと同様のプロジェクトをAndroid Studio上に作成します。プロジェクトテンプレートは「Empty Activity」を選ぶようにしましょう。

 Android向けSDLのライブラリーは、「JCenter」で公開されています。まず、プロジェクトルート直下にある「build.gradle」を確認し、「repositories」セクションに「jcenter()」の記述を確認します。なければ追加しておきましょう。

allprojects {
    repositories {
        google()
        jcenter() // この行があるのを確認
    }
}

 次に、app配下にあるbuild.gradleの「dependencies」に、次の行を加えます。□□□の部分は、SDL Androidバージョン番号を記述します。

dependencies {
    // 中略
    implementation 'com.smartdevicelink:sdl_android:□□□'
}

 例として、本稿執筆時(2019年8月中旬)の最新版は4.9.0なので、次のように記述します。

dependencies {
    // 中略
    implementation 'com.smartdevicelink:sdl_android:4.9.0'
}

 このようにbuild.gradle を編集したら、「Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly」というメッセージがエディターの上部に表示されるので、「Sync now」をクリックします。

図1 右端の「Sync now」ボタンをクリックする

 ライブラリーがダウンロードされますが、パソコンがインターネットにつながっていないとエラーになるので、ご注意ください。


パーミッションを追加する

 通常、ライブラリー側にパーミッションの記述がある場合、依存関係に追加することで、アプリ側にもパーミッションが追加されます。しかしAndroid向けSDLライブラリーには、なぜかパーミッションの記述がありません。そのため、アプリの「AndroidManifest.xml」に次のパーミッションを追加する必要があります。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.company.mySdlApplication">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    ...
</manifest>

サービスを追加する

 SDLは、スマートフォンアプリとSDL Core(車載機側)との間でメッセージをやりとりすることで動作します。Androidの場合は、アプリがバックグラウンドになっているときも動作することが望ましいため、メッセージのやりとりをAndroidのサービスとして行うように設定します。

 Android Studioで「MainActivity.kt」が入っているパッケージを選択し、「File」→「New」→「Kolin File/Class」で「MySDLService」というクラスを追加しましょう。MySDLServiceは「android.app.Service」(Android SDK標準のサービス)を継承させます。

import android.app.Service
class MySDLService : Service() {
    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}

 MySDLServiceクラスをプロジェクトに追加しただけでは、Android OSにサービスとして認識されません。「AndroidManifest.xml」に登録しておきましょう。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="{パッケージ名}">
    <!-- 中略 -->
    <application
        ...
        >
        <service
                android:name="{パッケージ名}.MySDLService"
                android:enabled="true"
                android:exported="false">
        </service>
        <!-- 中略 ->
    </application>
</manifest>

フォアグラウンドサービスとして起動する

 現時点のMySDLServiceは、バックグラウンドで動作するサービスになっています。そのため、メモリーが足りなくなったりすると、Android OSによって強制停止させられてしまいます。サービス内で車載機との接続を行うので、サービスが停止すると、車載機との接続も切れてしまいます。

 これでは困るので、MySDLServiceをフォアグラウンドサービスとして起動するようにします。フォアグラウンドサービスとは、通知欄にサービスが実行中であることを示すことで、メモリー不足などの時でも停止されなくなるサービスのことです。

 MySDLServiceをフォアグラウンドサービスとして起動するには、MySDLServiceのonCreate()で次のような処理を実行します。


class MySDLService : Service() {
    companion object {
        private const val CHANNEL_ID = "demoChannel"
    }
    override fun onCreate() {
        super.onCreate()
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(CHANNEL_ID, "SDL Service", NotificationManager.IMPORTANCE_LOW)
            manager.createNotificationChannel(channel)
        }
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("SDLSample")
            .setSmallIcon(R.drawable.notification_icon)
            .setContentText("車載機に接続中")
            .build()
        startForeground(1, notification)
    }
}

 サービスをフォアグラウンドサービスとして起動するためには、通知オブジェクトも必要です。また、Android 8.0以降では通知を表示するために、事前に通知チャネルを作成しておく必要もあります。

 そして、サービスが停止したタイミングでフォアグラウンド実行も停止されるように、onDestroy()を次のようにしておきます。

class MySDLService : Service() {
    ...
    override fun onDestroy() {
        super.onDestroy()
        stopForeground(true)
    }
}

サービスを起動する

 そして、サービスの起動を設定しましょう。通常であればスマートフォンが車載機に接続された時点でサービスが起動する仕組みを用意しますが、今回はサンプルなので、アプリ起動のタイミングでサービスを起動するようにします。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val it = Intent(this, MySDLService::class.java)
        startService(it)
    }
}

車載機と通信するまでの流れ

 SDLを通じて車載機と通信するには、次の手順を行います。各手順はライブラリーで、非同期処理として実装されています。

1. SdlManagerオブジェクトを作る
2. HMIの状態を取得する
3. 車載機側のUIを作成する

 以下で、順に説明していきます。


SdlManager オブジェクトを作る

 まずは、車載機との通信を担当してくれる「SdlManager」オブジェクトを作ります。サービスが最初のコマンドを受け取ったタイミングで作るようにします。

class MySDLService : Service() {
    ...
    private var sdlManager: SdlManager? = null
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (sdlManager == null) {
            this.startSdlManager()
        }
        // 以下、intentの内容に応じた処理を記述
        return super.onStartCommand(intent, flags, startId)
    }
    private fun startSdlManager() {
        val port = 10868
        val ipAddr = "m.sdl.tools"
        val autoReconnect = true
        val transport = TCPTransportConfig(port, ipAddr, autoReconnect)
        // アプリの種類
        val appType = Vector<AppHMIType>()
        appType.add(AppHMIType.MEDIA)
        // アプリアイコン
        val iconFileName = "appIcon.png"
        val appIcon = SdlArtwork(iconFileName, FileType.GRAPHIC_PNG, R.drawable.app_icon, true)
        // SdlManagerの組み立て
        val appId = "testAppId"
        val appName = "MySDLApp"
        val builder = SdlManager.Builder(this, appId, appName, listener)
        builder.setAppTypes(appType)
        builder.setTransportType(transport)
        builder.setAppIcon(appIcon)
        sdlManager = builder.build()
        // 開始!
        sdlManager?.start()
    }
    private val listener =  object : SdlManagerListener {
        // 接続が成功した
        override fun onStart() {
            addHmiStatusListener()
        }
        // 切断された
        override fun onDestroy() {
            this@MySDLService.stopSelf()
        }
        // 何かエラーが起きた
        override fun onError(info: String, e: Exception) {
            Log.v("SDL", "onError() $info")
        }
    }
}

 startSdlManager()の中を順に説明しましょう。

 まず、「transport」オブジェクトを作成します。今回はWeb上のシミュレーターを使用するので「TCPTransportConfig」を使います。引数では、シミュレーターのIPアドレス(ホスト名)とポート番号、自動で再接続するか否かを指定します。具体的に指定するIPアドレスとポート番号は、後で説明します。

 次に、アプリの種類を指定します。「List<AppHMIType>」ではなく「Vector<AppHMIType>」を使用する点に注意しましょう。

 そして、車載機側で表示されるアプリアイコンを用意します。画像自体はAndroidの画像リソースとして用意します。「SdlArtwork」のコンストラクタの第1引数は、車載機側でどのファイル名で保存するかを指定します。

 ここまで準備ができたら、最後に「SdlManager.Builder」を使って組み立てます。「appId」の指定がありますが、今回はシミュレーターで動かすだけなので、適当な値をセットしておけば大丈夫です。

 組み立てが完了したら、「start()」で開始します。車載機に接続できた際、「SdlManager.Builder」のコンストラクタでセットしたリスナーの「onStart()」が呼ばれます。


HMIの状態を取得する

 車載機との接続が完了したら、次はHMI(Human Machine Interface)、つまり車載機の状態を取得します。

class MySDLService : Service() {
    ...
    private fun addHmiStatusListener() {
        sdlManager?.addOnRPCNotificationListener(FunctionID.ON_HMI_STATUS, object: OnRPCNotificationListener() {
            override fun onNotified(notification: RPCNotification?) {
                val status = notification as OnHMIStatus
                if (status.hmiLevel == HMILevel.HMI_FULL && status.firstRun) {
                    showUI()
                }
            }
        })
    }
}

 HMIの状態が取得できたら、「addOnRPCNotificationListener()」の引数で渡したコールバックの「onNotified()」が呼ばれます。結果の「hmiLevel」を確認し、フル機能が使える状態かを調べましょう。

車載機側のUIを作成する

 HMIの状態を確認し、利用可能であることがチェックできたら、車載機側のUI作成にとりかかります。SDLでは、車載機側のUIは「ScreenManager」経由で作成します。

class MySDLService : Service() {
    ...
    private fun showUI() {
        val manager = sdlManager?.screenManager
        if (manager == null) {
            return
        }
        // 画像の準備
        val fileName = "primary.png"
        val primaryGraphic = SdlArtwork(fileName, FileType.GRAPHIC_PNG, R.drawable.moke, true)
        // UI更新トランザクションの開始
        manager.beginTransaction()
        manager.textField1 = "Hello SDL world"
        manager.textField2 = "アプリ作ろう"
        manager.primaryGraphic = primaryGraphic
        manager.commit(object: CompletionListener {
            override fun onComplete(success: Boolean) {
                if (success) {
                    Log.v("showUI", "画面の設定に成功したよ")
                }
            }
        })
    }
}

 まず、SdlManager経由で「ScreenManager」オブジェクトを取得します。次に「beginTransaction()」で UI 更新トランザクションを開始します。トランザクション開始後、「textField1」や「primaryGraphic」などを設定し、最後に「commit()」を呼びます。

 現状、SDL車載機のWebシミュレーターで利用できる画面レイアウトはテンプレートのみで、ボタンやテキストなどを自由に配置したりはできません。テンプレートを変更するには、次のような処理を行います。

class MySDLService : Service() {
    ...
    private fun setLayout() {
        val request = SetDisplayLayout()
        request.displayLayout = PredefinedLayout.TEXTBUTTONS_WITH_GRAPHIC.toString()
        request.onRPCResponseListener = object: OnRPCResponseListener() {
            override fun onResponse(correlationId: Int, r: RPCResponse?) {
                val response = r as? SetDisplayLayoutResponse
                if (response == null) {
                    return
                }
                if (response.success) {
                    // レイアウトの変更が成功したので、値をセット
                    showUI()
                } else {
                    Log.v("SetLayout", "onError")
                }
            }
        }
        sdlManager?.sendRPC(request)
    }
}

 まず、「SetDisplayLayout」オブジェクトを作り、「displayLayout」に、どのテンプレートに変更するかを指定します。指定可能なテンプレートは「PredefinedLayout」で定義されていますが、一部のテンプレートは使用できません。詳しくはこちらをご覧ください。

 「onRPCResponseListener」で、レイアウトの変更処理が終わった後の処理を記述します。最後にSdlManagerの「sendRPC()」を呼び、レイアウトの変更を開始します。


シミュレーターの起動

 Androidアプリ側をここまで作成したところで、実際にシミュレーターで動作させてみましょう。今回はSDLコンソーシアムが用意している「Manticore」というWebベースのシミュレーターを使います。

 まず、ブラウザーでこちらを開きます。次に「LAUNCH MANTICORE」をクリックします。するとシミュレーターが起動し、次のような画面がブラウザに表示されます。

図2 Web上のシミュレーター「Manticore」の画面

 大事な部分は右側のURLとPORT NUMBERの部分です。この2つの値を「TCPTransportConfig」のコンストラクタの引数にセットすることで、手元のAndroidアプリからWeb上のシミュレーターにつなぐことができます。なお、PORT NUMBERの部分はManticoreを起動するたびに変わります。

図3 URLおよびポート番号は、このように表示される

 Androidアプリ側のURLとPORT NUMBERの設定が完了したら、早速Androidアプリを起動してみましょう。フォアグラウンドサービスが起動し、シミュレーター上でアプリアイコンが表示されれば成功です。

図4 アプリがインストールされ、Apps画面に登録された

 シミュレーターでアプリをクリックすると、「showUI()」で指定した画面が表示されます

図5 起動したアプリ画面

まとめ

 今回はSDL対応Androidアプリの基本的な作成方法と、SDLを用いた車載機側画面の作成方法を説明しました。次回は、車載機側からガソリンの残量といった情報を受け取り、アプリ側で処理する方法を説明します。


「クルマとスマホをなかよくする SDLアプリコンテスト2019」

主催:SDLアプリコンテスト実行委員会(事務局:角川アスキー総合研究所)
協力:SDLコンソーシアム日本分科会、株式会社ナビタイムジャパン
後援(予定): 独立行政法人国立高等専門学校機構、一般社団法人コンピュータソフトウェア協会ほか
応募締切:2019年10月31日(木)24:00
募集内容:エミュレーターか開発キット上で開発したSDL対応アプリ(既存アプリの移植、新規開発)
募集対象:年齢、性別、国籍等不問。個人・チームどちらでも応募可
応募方法:プレゼンシートと動作解説動画をWebフォームで応募
審査:審査員が新規性、UX・デザイン、実装の巧みさ等で評価
最終審査会:2019年11月22日(金)
審査員:暦本純一(東京大学情報学環教授)、川田十夢(AR三兄弟長男)ほか
グランプリ:賞金50万円+副賞
特別賞(5作品):賞金各10万円
公式サイト:http://sdl-contest.com/

■関連サイト

(提供:SDLコンソーシアム)

カテゴリートップへ

この連載の記事

注目ニュース

ASCII倶楽部

プレミアムPC試用レポート

ピックアップ

ASCII.jp RSS2.0 配信中

ASCII.jpメール デジタルMac/iPodマガジン