THETA X でOpenStreetMapを利用する（後編）

## はじめに

リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>, <b>RICOH THETA Z1</b>, <b>RICOH THETA X</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。


前回の記事「[THETA X でOpenStreetMapを利用する（前編）](https://qiita.com/KA-2/items/ab2508c268967c1104bb)」では、OpenStreetMapという地図をプラグインに埋め込む方法をお伝えしました。
しかし、「一般の開発者は、日本モデルにおいて、ネットワーク接続するプラグインを開発できない（＝地図をダウンロードするプラグインを作れない）」という制限があります。
また、「いちいち自分のプラグインに地図を埋め込むほどではない」という方もいらっしゃるかと思います。

そこで、<b>[ストアに公開済の地図プラグイン(Current Location)](https://pluginstore.theta360.com/plugins/skunkworks.currentlocation/)は、一般開発者が作成したプラグインから呼び出して利用でき、呼び出し元のプラグインに戻れる仕掛けが入っています。</b>

今回は、プラグイン連携する仕掛けの「作り方」と「使い方」について説明します。

プラグイン連携が動作している様子は以下ツイートの通りです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>ストア公開済の地図プラグインは、他のプラグインから呼び出して利用できます<br>一般開発者でも地図利用できるプラグインの作り方と利用方法を紹介します！<a href="https://t.co/ztqUOLtY8a">https://t.co/ztqUOLtY8a</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/THETAX?src=hash&amp;ref_src=twsrc%5Etfw">#THETAX</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/android?src=hash&amp;ref_src=twsrc%5Etfw">#android</a> <a href="https://twitter.com/hashtag/OpenStreetMap?src=hash&amp;ref_src=twsrc%5Etfw">#OpenStreetMap</a> <a href="https://twitter.com/hashtag/osmdroid?src=hash&amp;ref_src=twsrc%5Etfw">#osmdroid</a> <a href="https://t.co/MDA7q8jvnb">pic.twitter.com/MDA7q8jvnb</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1590163135063023618?ref_src=twsrc%5Etfw">November 9, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Androidで他のアプリを呼び出す方法

### 明示的インテントと暗黙的インテント

Androidにおいて、他アプリケーションを呼び出す方法には、明示的インテントと暗黙的インテントがあります。

Googleのドキュメントは[こちら](https://developer.android.com/guide/components/intents-filters?hl=ja#Types)。
大切な所を引用すると以下となります。

- 明示的インテント<br>ターゲット アプリのパッケージ名またはコンポーネントの完全修飾クラス名を指定して、インテントを実行するアプリを指定します。
- 暗黙的インテント<br>アクションを指定し、それを実行できる端末上のアプリを起動させます。

明示的インテントは、「パッケージ名またはコンポーネントの完全修飾クラス名を指定」ですので、公開してしまった後に呼び出しに関係する内部構造を変えることができなくなってしまいます。そもそも「内部構造を公開するのはどうかな？」ということがありますので、今回のようなケースには向かない方法だと思います。

暗黙的インテントは、今回のように「地図を開きたい」というアクションのみを指定します。するとAndroidが指定されたアクションに対応するアプリケーションを起動します。
対応するアプリケーションが複数ある場合、chooserと呼ばれる一覧が表示され、どれを起動するかをユーザーが指定できます。

これはTHETA Xも同じです。以下のようにchooserが表示されます。

![chooser_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/b40f4709-bbaa-722e-c69b-39f5bd4f0ebc.png)

現在は、THETA PLUG-IN STOREに地図プラグインが1つですが、今後、他の地図プラグインが増える可能性もあるので、この方法を採用しました。


### geo URIについて

暗黙的インテントで他のアプリを呼び出す時、特に地図アプリについては、パラメータの渡し方にある程度作法があるようです。
[Goole Mapの呼び出し方](https://developer.android.com/guide/components/intents-common#Maps)が基本にあり、他の地図アプリもそれに倣った呼び出し方にある程度対応しているという状況のようです。決まり事ではなく、広く知れ渡った方法に後続の他社が倣っていったようにみえます。
（一応、[geo URI](https://en.wikipedia.org/wiki/Geo_URI_scheme)というフォーマットで座標を渡すのですが、Google独自のアレンジが濃いようです）

以下の記事に読みやすくまとまっていました。
[「Androidアプリのgeoインテント対応状況」](https://qiita.com/j_catfish/items/819046a03c7619504c58)

今回作成した地図プラグインも、Googleの形式にできる限り倣って実装しました。

### THETAプラグインの制約
通常のAndroidアプリにおいて、暗黙的インテントで他のアプリを呼び出すと、
呼び出し元アプリはポーズの状態となり、呼び出されたアプリを閉じると呼び出し元アプリはレジュームで復帰します。

しかし、前回記事でも引用した以下、THETAプラグインの約束事「[Plug-in Policy](https://github.com/ricohapi/theta-api-specs/blob/main/ricoh-theta-plugin/policy.md)」の中でも、特に以下事項

> 2 . User experience
> Plug-ins must not be enabled to operate in the background.
>
> 3 . Compatibility with hardware
> Developers must use the SDK provided by RICOH for RICOH THETA plug-ins.

を守ると、開発者がポーズするように実装したところで、THETAのシステムは呼び出し元のプラグインを終了させてしまいます。呼び出し元のプラグインに戻れなくなってしまうのです。
そこで、THETAプラグインの特性にあわせた独自の工夫が必要となりました。

具体的には、
- 呼び出し元のパッケージ名を地図プラグインに渡した場合、地図プラグインを終了した際、<b>呼び出し元のMainActivityに戻れる</b>

という仕掛けを入れました。

地図プラグイン側は、明示的インテントを利用して呼び出し元のプラグインへ戻します。
Plug-in Policyに「Developers must use the SDK provided by RICOH for RICOH THETA plug-ins.」という記載がありますので、「呼び出し元プラグインの内部構造は判らないけれど、MainActivityは必ずある」という考えの元に仕掛けが成り立っています。


## 暗黙的インテントに対応したTHETAプラグインの作り方

### ソースコード

AndroidManifest.xmlに以下を追加し、geo URIのインテントを受け取れるアプリケーションであることを宣言します。
今回作成したプラグインの該当箇所は[こちら](https://github.com/theta-skunkworks/theta-plugin-current-location/blob/main/app/src/main/AndroidManifest.xml#L27-L31)です。

```AndroidManifest.xml
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:scheme="geo" />
            </intent-filter>
```

あとは、起動した後に、以下コードを書くことで、インテントのパラメータ（getURIの文字列）を取得できますので、文字列を解釈してパラメータに沿った振る舞いのコードを書くだけです。

```MainAvtivity.java
        Intent intent = getIntent();
        Uri data = intent.getData();
```

なお、「 intent.getData();」の結果がnullの場合はインテントでの起動ではなく、通常起動であると判断します。


今回作成した地図プラグインでは、[setGeoUri()というメソッド](https://github.com/theta-skunkworks/theta-plugin-current-location/blob/main/app/src/main/java/skunkworks/currentlocation/MainActivity.java#L300-L377)に上記処理をまとめ、[onResume()にて、setGeoUri()を呼び出しています](https://github.com/theta-skunkworks/theta-plugin-current-location/blob/main/app/src/main/java/skunkworks/currentlocation/MainActivity.java#L163)。


### 通常起動とインテント起動の振る舞いの違い

今回作成した地図プラグインでは、「通常起動した場合」と「インテントで起動した場合」で以下のように振る舞いが変わるようにしてあります。
これは、地図プラグインの使い勝手を考えて実装した処理で、必ずそうしなければならないものではありません。

|-|通常起動|インテントで起動|
|---|---|---|
|無線LANの状態|プラグイン起動前後で状態を維持する。|OffまたはAPモードであった場合、CLモードにする。<br>CLモードに切り替えた場合、地図プラグイン終了時にOffにする。<br>（日本モデルにおいて、一般開発者のプラグインに戻ったときのことを考慮してこの振る舞いとしています）|
|シャッターボタン押下時の振る舞い|測位できた場合、現在位置を画面の中心とする。|指定座標を画面の中心とする|
|地図プラグイン終了時の情報保存|最後に表示していた座標とズーム倍率を保存し、次回通常起動時の初期値に使う。|なにも保存しない。|

ご参考まで。


## 地図プラグインの利用の仕方

[GitHubのREADME](https://github.com/theta-skunkworks/theta-plugin-current-location#6-calls-from-other-plugins-with-implicit-intents)にも記載していますが、もう少し細かく説明しておきます。

以下二つがポイントとなります。
- 呼び出したい箇所でfinishする
- onStop()でインテントを投げる

### ソースコード

THETA Plug-in SDKのViewに、ボタンを配置し（画面にボタンを配置する所の説明は、通常と変わらないので省略します）
「ボタンを押すと、地図プラグインを呼び出す（指定した座標にピンを立て、ズーム倍率も指定して地図プラグインを起動、地図プラグインを終了するとMainActivityに戻る）」
という振る舞いをさせるケースのソースコードの書き方は以下の通りです。

表示したい座標とズーム倍率を固定値で以下のように定義しておきます。
あと、onStop()で地図起動をする/しないを切り替えるフラグを定義しておきます。
```MainAvtivity.java
    double lat = 35.710211;
    double lng = 139.810874;
    double zoom = 20.0;

    boolean bootMapEna = false;
```

ボタンを押したときの処理を以下とします。
ここでインテントを投げてしまうと、地図プラグインが起動せずに呼び出し元のプラグインが終了してしまうので、インテントを投げるところを onStop()に渡すためのフラグを立て、finishしています。

```MainAvtivity.java
        button = (Button) findViewById(R.id.buttonMap);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bootMapEna=true;
                finish();
            }
        });
```

onStop()を以下のように記述します。
```MainAvtivity.java
    @Override
    protected void onStop() {
        Log.d(TAG, "onStop()");

        if (bootMapEna) {
            String retPackageName = this.getPackageName() ;
            String uri = String.format("geo:?q=%06f,%06f&z=%01f&package=%s", lat,lng,zoom,retPackageName);
            Log.d(TAG, "geoUri=" + uri );
            Intent intent =  new Intent( Intent.ACTION_VIEW );
            intent.setData( Uri.parse(uri) );
            if ( intent.resolveActivity(getPackageManager()) != null ) {
                Log.d(TAG, "Activity=" + intent.resolveActivity(getPackageManager()).toString() );
                startActivity(intent);
            } else {
                Log.d(TAG, "No Activity");
            }
        }

        super.onStop();
    }
```

これだけです。

参考として、パラメータの与え方によって地図プラグインの振る舞いが以下のように変わります。
- ピンを立てたくない場合「geo:[lat,lng]」で座標指定する（「q = lat, lng」を使わない）。
- パッケージ名を省略すると。呼び出し元に戻らない（製品画面に戻る）

### 一般開発者が日本モデルで連携のデバッグをするときの注意点

前回記事の[こちら](https://qiita.com/KA-2/items/ab2508c268967c1104bb#theta-x%E3%81%A8theta-z1-51gb%E3%81%AE%E6%97%A5%E6%9C%AC%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E9%96%8B%E7%99%BA%E3%81%AE%E5%88%B6%E9%99%90)に記載した、以下事項に注意してデバッグしてください。

>（THETA XとTHETA Z1 51GBの日本モデルにおいて、一般開発者の方は、開発者モードにしているときにCLモードにできなくなります。普段利用をするときは開発者モードを解除し、プラグイン開発をするときは開発者モードにする、という都度都度の手間が生じますのでご注意を）

プラグイン連携の動作確認をするときには、一旦開発者モードを解除する必要があります。
開発者モードのまま地図プラグインを起動しても地図プラグイン動作中にCLモードにならず、キャッシュされてない地点やズーム倍率の地図が表示されません。


## おわりに

THETAプラグインの約束事「[Plug-in Policy](https://github.com/ricohapi/theta-api-specs/blob/main/ricoh-theta-plugin/policy.md)」を守りつつ、地図プラグインを他のアプリケーションから呼び出して利用し、地図プラグインを終了すると呼び出し元に戻れる仕掛けができました。
これは、一般開発者の方が日本モデルで動作させることもできます。

今回紹介した方法を使えば、「THETA Xに保存されている画像ファイルのExif情報から撮影地点の位置を読み、地図に表示させる」というTHETAプラグインを作成することも可能です。

また、地図に限らず、他のプラグインから利用価値があるTHETAプラグインというのは沢山考えられると思います。そのようなサービスを思いついたら、是非THETAプラグインを作成してみてください。


## RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡