Nend広告をjsからネイティブコード経由で呼び出すCordova（PhoneGap）プラグインを作ってみた


ここのところ、Cordovaベースのハイブリッドアプリ開発フレームワークである[Ionic Framework](http://ionicframework.com/)でアプリを作って公開しました。
マネタイズのために広告を入れる必要があったのですが、とある事情でAdMobを入れることができなかったので、[Nend広告](http://nend.net/)を導入することに決めました。

[AdMobは既存の素晴らしいCordovaプラグイン](https://github.com/floatinghotpot/cordova-admob-pro)があって、バナー広告やインタースティシャル広告をJavaScriptからネイティブコード経由で呼び出すことができます。しかし、Nendにはそのようなプラグインは公開されていなかったので、自分で作ることにしました。

**作ったプラグイン : https://github.com/TakayukiSakai/cordova-plugin-nend**

本記事では、この[プラグインのver0.1.0](https://github.com/TakayukiSakai/cordova-plugin-nend/tree/398fa29424fe088a8645809aeab23ad16d3e23fe)を開発・公開するまでの過程を細かく説明していきたいと思います。特に、 **SDKという第三者がネイティブプラットフォーム用に書いたコードをJavaScriptから呼び出せるようにする**という点で、他の記事にはない情報を提供できると思います。
Cordovaプラグイン開発の日本語記事はまだまだ少ないので、それに貢献できればいいな...。

# 本記事の対象とする人

- Cordova/PhoneGapで少しはアプリを作ったことがある
- Cordovaのプラグインの仕組みを知りたい
- Cordovaのプラグインを作ってみたい
- iOS/Android用SDK開発に携わっていて、SDKをCordova/PhoneGapにも対応させろと ~~クソ~~上司から迫られている

実際にプラグインを作るためには、対応させたいプラットフォームのネイティブコードを書く必要があります。
iOSに対応したプラグインを作るためにはObjective-Cを、AndroidのためにはJavaを多少は書かなくてはなりません。


# プラグイン作成ステップ

プラグインを公開するまでのステップはだいたい以下のようになります。


 1. プラットフォームごとにテストアプリを作って、ネイティブコードからSDKの関数を叩いてみる（動作確認・依存パッケージの確認）
 2. plugmanコマンドで雛形作成
 3. jsから呼び出す関数を設計・実装
 4. ネイティブコード（Objective-C/Java）を書いて、それぞれのプラットフォーム(iOS/Androidへ対応)
 5. plugmanコマンドでプラグインを公開



# プラットフォームごとにテストアプリを作って、ネイティブコードからSDKの関数を叩いてみる

さて、プラグインを作る以上、自分自身がネイティブコードでSDKを触ってみないことには始まりません。
また、SDKが依存しているパッケージなどを洗い出す上でも、プラットフォームごとにテストアプリを作って動作確認をするのはマストです。

当たり前のことですが、 **SDKをラップしたプラグインを開発するのは、そのSDKを使って普通のネイティブアプリを作るより難易度が高い**です。
そこで、このテストアプリ作成時点でつまづいた点は、プラグイン開発時にもつまづく可能性が高いので、ちゃんと覚えておきましょう。

無事にiOS/AndroidそれぞれのテストアプリでSDKが利用できたら、プラグイン開発の始まりです。


# plugmanコマンドで雛形作成

まず、Cordovaのプラグインを操作するコマンドである```plugman```コマンドをインストールします。

```bash:terminal
$ npm install -g plugman@0.23.0
```

ここで、わざわざ少し古いバージョンの```plugman```をインストールしているのは、[このページ](http://stackoverflow.com/questions/30133648/unable-to-get-beyond-login-error-publishing-to-cordova-plugin-registry)に書かれているように、新しいバージョンだとバグがあって、プラグイン公開時にエラーになるからです。

次に、```plugman create```コマンドでプラグインの雛形を作ります。

```bash:terminal
plugman create --name cordova-plugin-nend --plugin_id com.effers.kaky.nend --plugin_version 0.1.0
```

ここで指定した名前やidなどは ```plugin.xml``` の中に書かれており、後からいくらでも変更できるので、今は深く悩む必要はありません。


# jsから呼び出す関数を設計・実装

## 設計

まず、js側から呼び出す関数を設計します。ここで設計した関数が将来このプラグインを利用するユーザから呼ばれることになるので、なるべく使いやすさを考慮します。

また、今回のプラグインの場合はあくまでSDKのネイティブコードをjsから呼べるようにするためのものでしかないので、提供できる機能はSDKの機能に依存します。
そのため、この設計時点でSDKの機能はほぼ把握しておく必要があります。

僕が実装したNend SDKのドキュメントを読む感じ、インタースティシャル広告を表示するまでに

1. 広告IDの指定
2. 広告のプリロード（起動直後に呼ぶ用）
3. 広告の表示（任意のタイミング）

の3ステップを踏めばよいようでした。

広告のリフレッシュは、前の広告が非表示になった後にSDK側で自動でやってくれるようなので、上記の3つの機能を実現する関数だけ用意すればよさそうです。

ということで、シンプルに以下のような設計にしました。

```JavaScript
Nend.setOptions({interstitialApiKey:"YOUR_API_KEY", interstitialSpotId:"YOUR_SPOT_ID"});
Nend.createInterstitial();
Nend.showInterstitial();
```

広告のロードに失敗したとき用のコールバックなどがありませんが、とりあえず最低限の実装ということで割愛しました。


## 実装

次に、先ほど設計した関数を実装していきます。
```www/``` フォルダ以下にJavaScriptファイルの実装ファイルを置くのが通例です。

plugman createで雛形作成したときに ```cordova-plugin-nend.js``` とかいう長ったらしい名前のファイルが作成されていると思いますが、ファイル名はなんでもいいので ```nend.js``` に変更します。
このとき、```plugin.xml``` 内の以下の部分も変更します。

```plugin.xml
<js-module name="cordova-plugin-nend" src="www/nend.js"> <!-- src属性の中を変更 -->
    <clobbers target="cordova.plugins.cordova-plugin-nend" />
</js-module>
```


この ```nend.js``` ファイルの中で、```module.exports``` に定義した関数が、ユーザから使われることになります。

まずは、一番簡単な ```setOptions()``` から実装します。
この関数はユーザが設定した値を保持するだけなので、ネイティブのコードを呼び出す必要はなく、以下のようにしました。

```nend.js
var options = {};

module.exports.setOptions = function(args) {
    options = args;
};
```

次に、広告をロードする ```createInterstitail()``` を実装します。

```nend.js
module.exports.createInterstitial = function() {
  cordova.exec(
    function() {}, // 成功時のコールバック
    function() {}, // 失敗時のコールバック
    'Nend', // サービス名
    'createInterstitial', // アクション名
    [options] // オプション
  );
};
```

ここで **超重要関数 ```cordova.exec()```** が登場しました。この関数がネイティブのコードを呼び出してくれます。

[公式のドキュメント](https://cordova.apache.org/docs/en/4.0.0/guide_hybrid_plugins_index.md.html)に書いてあることをざっくり説明すると、```cordova.exec()``` はネイティブコードの中の「指定したサービス（クラス）の中に定義してあるアクション（メソッド）を呼ぶよ」という感じですかね。
これらの「サービス」や「アクション」のネイティブコードは、これから自分で実装してやる必要があります。

```showInterstitial()``` は ```createInterstitial()``` とほとんど同じなので割愛します。


# ネイティブコードの実装

さぁ、ついに山場のネイティブコードの実装です。
ネイティブコードの実装は、その中でもさらに以下の2ステップに分けられます。

1. plugin.xmlにネイティブコードの実装ファイルや今回ラップするSDKの場所、その他依存パッケージを記述
2. ネイティブコードを書く

iOS/Androidそれぞれ、上記2ステップについて順に説明していきます。

## iOS

### plugin.xmlへの記述

iOS用の実装ファイルは、```src/ios/``` 以下におきます。

cordova-plugin-nendプラグインの場合は、```src/ios``` 以下は、下記のようにファイルを配置しています。

```shell-session:src/ios
.
+-- CDVNend.h // ヘッダファイル
+-- CDVNend.m // 実装ファイル
+-- _NendAd // Nendから提供されているiOS用SDK
|   +-- NADView.h
|   +-- NADInterstitial.h
|   +-- libNendAd.a
```

そして、このディレクトリ構造に対応するように ```plugin.xml``` に記述していきます。

```plugin.xml
<platform name="ios">
    <config-file target="config.xml" parent="/*">
        <feature name="Nend"> <!-- サービス名（cordova.exec()のサービス名と同じものを使う） -->
            <param name="ios-package" value="CDVNend"/> <!-- 実装ファイルのクラス名 -->
            <param name="onload" value="true"/> <!-- アプリ起動時にプラグインも初期化するオプション -->
        </feature>
    </config-file>
    
    <!-- 自分で実装したファイル -->
    <header-file src="src/ios/CDVNend.h" />
    <source-file src="src/ios/CDVNend.m" />

	<!-- Nend SDK -->
    <header-file src="src/ios/NendAd/NADView.h" />
    <header-file src="src/ios/NendAd/NADInterstitial.h" />
    <source-file src="src/ios/NendAd/libNendAd.a" framework="true" /> <!-- .aファイルはframeworkとして登録する必要あり -->

	<!-- Nend SDKが依存しているフレームワーク -->
    <framework src="AdSupport.framework"/>
    <framework src="Security.framework"/>
    <framework src="ImageIO.framework"/>
</platform>
```

ここでは、以下の点に注意してください。

- ```<feature name="Nend">``` の部分のname属性を ```cordova.exec()``` から呼ぶときのサービス名と同じにする
- SDKなどで配布されている ```.a``` 拡張子のファイルは、```framework="true"``` 属性をつけて、フレームワークとして登録しないといけない

### Objective-Cの実装

ヘッダファイルには以下のように、cordova側から呼ばれる関数を記述します。

```objectivec:CDVNend.h
#import <Cordova/CDV.h>

#import "NADInterstitial.h" // Nend SDKのインポート

@interface CDVNend : CDVPlugin <NADInterstitialDelegate>

- (void)createInterstitial:(CDVInvokedUrlCommand *)command;
- (void)showInterstitial:(CDVInvokedUrlCommand *)command;

@end
```

なにやら引数として ```CDVInvokedUrlCommand``` なるオブジェクトが渡されています。
このオブジェクトには、jsコードから渡された引数やコールバックを呼ぶために必要な情報が含まれています。

実装ファイルは以下のようになります。（```showInterstitial()``` は割愛。）

```objectivec:CDVNend.m
#import "CDVNend.h"

@implementation CDVNend

- (void)createInterstitial:(CDVInvokedUrlCommand *)command {
    CDVPluginResult *pluginResult;
    NSString *callbackId = command.callbackId;

    // jsコードから渡された引数を取得
    NSDictionary* options = [command argumentAtIndex:0 withDefault:[NSNull null]];

	// Nend SDKの関数を実行
    [[NADInterstitial sharedInstance] loadAdWithApiKey:[options objectForKey:@"interstitialApiKey"]
                                                spotId:[options objectForKey:@"interstitialSpotId"]];

	// 実行成功フラグを立ててリターン
	// js側で成功時のコールバックなどを指定してあればそれが実行される
    pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
    [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
}

@end
```

大したことしてない割に長くなってしまいました。
正直今回の場合、以下の2行以外はおまじないと思っても構いません。

```objectivec:CDVNend.m
NSDictionary* options = [command argumentAtIndex:0 withDefault:[NSNull null]];
[[NADInterstitial sharedInstance] loadAdWithApiKey:[options objectForKey:@"interstitialApiKey"]
                                                spotId:[options objectForKey:@"interstitialSpotId"]];
```

### デバッグ方法

プラグインをテストするためのcordovaプロジェクトに[cordova-plugin-console](https://github.com/apache/cordova-plugin-console)を入れると、コンソールログをXcodeで見れるので、地道にプリントデバッグしてました。



## Android

### plugin.xmlへの記述

Android用の実装ファイルは、```src/android/``` 以下におきます。

cordova-plugin-nendプラグインの場合は、```src/android``` 以下は、下記のようにファイルを配置しています。

```shell-session:src/android
.
+-- Nend.java // 実装ファイル
+-- _NendAd // Nendから提供されているAndroid用SDK
|   +-- nendSDK-2.6.1.jar
```

そして、このディレクトリ構造に対応するように ```plugin.xml``` に記述していきます。

```plugin.xml
<platform name="android">
    <config-file target="res/xml/config.xml" parent="/*">
        <feature name="Nend"> <!-- サービス名（cordova.exec()のサービス名と同じものを使う） -->
            <param name="android-package" value="com.effers.kaky.nend.Nend"/> <!-- 実装ファイルのクラス（フルパス） -->
        </feature>
    </config-file>

	<!-- Nend SDKが必要としているパーミッション -->
    <config-file target="AndroidManifest.xml" parent="/*">
        <uses-permission android:name="android.permission.INTERNET"/>
    </config-file>

	<!-- 自分で実装したファイル（クラス名に対応したディレクトリに配置） -->
    <source-file src="src/android/Nend.java" target-dir="src/com/effers/kaky/nend" />
    
    <!-- Nend SDK（.jarファイルはlibsフォルダに配置） -->
    <source-file src="src/android/NendAd/nendSDK-2.6.1.jar" target-dir="libs" />

	<!-- Nend SDKが依存しているフレームワーク -->
    <framework src="com.google.android.gms:play-services-ads:+" />
</platform>
```

iOSの場合と違って、```source-file``` タグに、 **ソースファイルをどこに配置するかを手動で指定**する必要があります。

### Javaの実装

Javaの場合はヘッダファイルはないので、実装ファイルのみです。以下のようになりました。

```Nend.java
package com.effers.kaky.nend;

import org.apache.cordova.*;
import net.nend.android.NendAdInterstitial;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;

public class Nend extends CordovaPlugin {
    @Override
    public boolean execute(String action, JSONArray inputs, final CallbackContext callbackContext) throws JSONException {
        PluginResult result = null;
        
        // WebViewのアクティビティを取得
        final Activity activity = cordova.getActivity();

        // if文でactionを判定して処理を分岐
        if (action.equals("createInterstitial")) {
            // jsからの引数を取得
            JSONObject options = inputs.optJSONObject(0);
            
            // Nendの関数を実行
            NendAdInterstitial.loadAd(
                activity.getApplicationContext(),
                options.getString("interstitialApiKey"),
                options.getInt("interstitialSpotId")
            );
            
            callbackContext.success();

        } else if (action.equals("showInterstitial")) {
            // UIを操作する処理は、メインスレッドで
            activity.runOnUiThread(new Runnable() {
                public void run() {
                    // Nendの関数を実行
                    NendAdInterstitial.showAd(activity);
                    
                    callbackContext.success();
                }
            });

        }

        if(result != null) callbackContext.sendPluginResult( result );
        return true;
    }
}
```

Androidの場合、iOSとは違って jsの```cordova.exec()``` でアクションに何を指定しても、Java側では ```CordovaPlugin#execute``` が呼ばれます。
その引数として ```action``` 変数が文字列で渡されるので、if文で分岐して処理する感じになります。

1行目のパッケージ名などは、```plugin.xml``` に記述したものと対応しているかちゃんと確認しましょう。


### デバッグ方法

```adb logcat``` で地道にプリントデバッグ...。



# プラグインを公開

[この公式ページ](https://cordova.apache.org/docs/en/4.0.0/guide_hybrid_plugins_index.md.html)の一番下にある通りに、

```bash:terminal
$ plugman adduser
$ plugman publish /path/to/your/plugin
```

とすれば公開されます。ただし、最初の方も書きましたが2015/07/20現在、最新のplugmanコマンドだと ```plugman publish```時にエラーになるので、少し古いバージョン(0.23.0)のものをいれてください。

```config.xml``` の ```plugin```タグの```id```属性に指定したものが、このプラグインのidとして使われるので、公開する前にはちゃんとしたidをつけましょう。


# まとめ

- 使いたいSDK用のプラグインがなくても諦めちゃダメ！
- プラグイン開発には、対応したいプラットフォームのネイティブコードを書ける必要あり
- SDKの依存関係周りを ```plugin.xml``` に記述するのが意外と大変
- まだまだプラグイン開発用のデバッグ方法が確立されてるとは言えない（？）

# 最後に

せっかくなのでNendの中の人と話してみたいです。