【Titanium】ti.cloudpushを利用してプッシュ通知を実装する

先月末にTitaniumが3.2にアップデートされました。
このタイミングで新しいアプリの開発を始めたので、
バージョン3.2に対応した形で開発を進めていました。

普段Objective-CでiOS向けアプリを作っているのでプッシュ通知に関しては仕組みは理解していたつもりでした。
更に、Titanium SDK3.1.2の頃に、Android向けのプッシュ通知も実装していたので、
さくさく出来るだろうと思っていたのですが。。

ドハマりしてしまいました。
ハマったのは案の定Android側です。
以前のプロジェクトで利用していたモジュールを再利用しようとするとR.javaが生成されないというエラーが発生しました。

ちなみに利用していたモジュールはGuti/Google-Cloud-Messaging–Titanium-です。

せっかくTitanium側でプッシュ通知の機能を用意してくれてるんだし今後もTitanium使うなら避けては通れないだろうなと思ったので、
今回からti.cloudpushを利用することにしました。

初期設定

まずは、tiapp.xmlのmodulesに下記を追記。

<module platform="commonjs">ti.cloud</module>
<module platform="android">ti.cloudpush</module>

GUIで作業すると自動的にTi.CloudのAppsと紐付けされるので便利です。

次にGoogle Cloud Messaging for AndroidをONにして、Project IdとAPI Keyを取得します。
この辺りを参考にして下さい。
ここで取得したProjectIdとAPI KeyをTitaniumクラウドのSettingsのAndroid Push Configurationに登録します。
(Sender IDにProjectIdを入力すれば問題無いです。開発環境と本番環境が同じ場合は、Production、Development共に入力しておきます)

iOS側はApple Provisioning PortalのApp IDsに.certSigningRequestファイルの登録とTitaniumコンソールのApple iOS Push Certificates欄にp12ファイルを登録しておきます。
この状態が反映されたプロビジョニングファイルを利用する必要があります。

Users登録

ti.cloudpushでは通常通りのデバイストークン(Androidならregistration_id)に加えて、
ti.cloudのUsersにユーザー登録する必要があります。

プッシュを送りたいだけなのに、ユーザー登録が必要なんてなかなか面倒臭いですが、
今回は会員登録機能も用意していたので会員登録完了後にTi.Cloud.Usersも追加する形で対応しました。

      //会員登録後にti.Cloud.Usersに追加
      var createUser = {
        email:email,
        first_name:first_name,
        last_name:last_name,
        password:password,
        password_confirmation:password_confirmation
      };
      
      var Cloud = require('ti.cloud');
      Cloud.Users.create(createUser, function(e) {
        if (false == e.success) {
          alert('Error:' + ((e.error && e.message) || JSON.stringify(e)));
        }
      });

デバイストークンの取得

デバイストークンの取得方法は下記の通りです。

//index.js
//iOSのデバイストークンを取得
if (OS_IOS) {
  Ti.Network.registerForPushNotifications({
    types: [Titanium.Network.NOTIFICATION_TYPE_BADGE, Titanium.Network.NOTIFICATION_TYPE_ALERT, Titanium.Network.NOTIFICATION_TYPE_SOUND],
    success: function(e) {
      Ti.API.info("deviceToken : " + e);
      
      Alloy.CFG.deviceToken = e.deviceToken;
    },
    error: function(e) {
      alert(e.error);
      Ti.API.info('Push error');
    },
    callback: function(e) {
      Ti.API.info(e);
      alert(e);
    }
  });
//Androidのデバイストークンを取得
} else if (OS_ANDROID) {
    var CloudPush = require('ti.cloudpush');
    CloudPush.addEventListener('callback', function (evt) {
        alert(evt.payload);
    });
    CloudPush.addEventListener('trayClickLaunchedApp', function (evt) {
        Ti.API.info('Tray Click Launched App (app was not running)');
    });
    CloudPush.addEventListener('trayClickFocusedApp', function (evt) {
        Ti.API.info('Tray Click Focused App (app was already running)');
    });
    CloudPush.retrieveDeviceToken({
        success: deviceTokenSuccess,
        error: deviceTokenError
    });
 
    function deviceTokenSuccess(e) {
        Ti.API.info('Device Token: ' + e.deviceToken);
        Alloy.CFG.deviceToken = e.deviceToken;
    
        CloudPush.enabled = true;
    }
    
    function deviceTokenError(e) {
        alert('Failed to register for push! ' + e.error);
    }
}

私はなかなかAndroid側でこのデバイストークンが取得出来ませんでした。
どうやら実装してから既にビルド済みのapkに上書きしてもダメだったらしく、
一旦端末のアプリを削除してから再ビルドすると取得出来ました。

Ti.Cloudに登録

会員登録・デバイストークンの取得が完了するとそれらの情報をTi.Cloudに登録します。

//alloy.js
//OS名を取得
Alloy.Globals.osName = function() {
	var osname = '';
	
	if ( OS_IOS ) {
		osname = 'ios';
	} else if (OS_ANDROID) {
		osname = 'android';
	}
	
	return osname;
};
//Ti.Cloudに情報を登録
Alloy.Globals.subscribe = function() {
  if ( '' == Alloy.CFG.deviceToken ) {
    return;
  }
  var Cloud = require('ti.cloud');
  Cloud.Users.showMe(function(e) {
    if (e.success) {
      Cloud.PushNotifications.subscribe({
          channel: '<CHANNEL_NAME>', //プッシュを送信する際にチャンネルを登録する事でプッシュの種類を分別することが出来ます
          device_token:Alloy.CFG.deviceToken, 
          type:Alloy.Globals.osName() //Androidで入力しないとエラーになった
      }, function (e) {
          if (e.success) {
              Ti.API.info(e);
          } else {
              alert('Subscribe failed:\n' + ((e.error && e.message) || JSON.stringify(e)));
          }
      });
    } else {
      alert("ShowMe failed:" + e.message);
    }
  });
};

今回は会員登録完了時、ログイン完了時にこのAlloy.Globals.subscribeを呼ぶ事でデバイストークンをTi.Cloudに登録しました。

これでTitaniumクラウドのコンソールを確認すると、
UsersとPush NotificationsのSubscribed Devicesに情報が登録されていることが確認出来ます。
登録されていなければどこかでミスしている可能性があるので、
流れを見直したりやり直す等してみて下さい。

プッシュを送信する

今回はPHPからTitaniumクラウドのAPIを叩いてプッシュを送信してみました。

//送信するデバイストークンを配列に格納
$deviceTokens = array(...);

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, "https://api.cloud.appcelerator.com/v1/push_notification/notify_tokens.json?key=" . "<INPUT Titanium APP Key>"; //APP Keyを入力して下さい
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'channel'   => '<CHANNEL_NAME>', //上記で設定したchannel名
    'to_tokens' => implode(',', $deviceTokens),
    'payload'   => 'プッシュに表示されるメッセージ',
)) );

$result = curl_exec($ch);
var_dump($result); //レスポンス確認

curl_close($ch);

若干のタイムラグ等あるかと思いますが、無事iOS、Android共にプッシュ通知が確認出来ました。
ここまでくるまで2週間程時間を費やしたので、なんとか解決出来てよかったです。。

ちなみにカスタムフィールドを埋め込みたい場合は、

//curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query(array(
//    'channel'   => '<CHANNEL_NAME>', //上記で設定したchannel名
//    'to_tokens' => implode(',', $deviceTokens),
//    'payload'   => 'プッシュに表示されるメッセージ',
//)) );
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'channel'   => '<CHANNEL_NAME>', //上記で設定したchannel名
    'to_tokens' => implode(',', $deviceTokens),
    'payload'   => json_encode(array(
        'alert' => 'プッシュに表示されるメッセージ',
        'custom' => array(
            'field1' => 'foo',
            'field2' => 'bar',
        ),
    ))
)) );

と、することで送信する事が出来ます。

//iOS
callback: function(e) {
  Ti.API.info(e.data.custom.field1, e.data.custom.field2);
}

//Android
CloudPush.addEventListener('callback', function (evt) {
    if (null != evt.payload) {
      var payload = JSON.parse(evt.payload);
      
      Ti.API.info(payload.custom.field1, payload.custom.field2);
    }
});

受信時は上記の様にすれば項目が取得出来るかと思います。