実装背景
2019年4月10日までにGCMからFCMに切り替えないとプッシュ通知が受け取れなくなるという話は知ってたのですが、対応を後回しにしてたら3月になってしまい、急いで対応することとなりました。
とりあえずAndroidで通知を受け取れるようにせねばということで、今回はAndroidのみ実装してみました。
私自身、詰まりまくったので冗長な説明になってる箇所が多いと思いますが
「ここわかるぜ」って方は流し読みしたり、逆にいい手順を教えていただければと思います。
(長ったらしい記事が一つくらいあってもいいと思うんですよね)
やりたいこと
・FCMを利用してAndroidに対し音付きプッシュ通知を打てるようになる
・通知受け取り時に通知アイコンを設定したい。
・本番用と開発用のアプリを同時にインストールでき、別々にプッシュ通知を打てるようにする。
・[重要]とりあえずプッシュ通知が打てるようになる
失敗談
[失敗談]や[なんでじゃ案件]、[なるほどわからん案件]とか沢山あったのですが、時間かかりそうなのでざっくりと先に書いちゃいます!
実装部分を読んでからじゃないと意味がわからなかったり、皆さんには関係なかったりするので、流し読み程度でOK(最後に書けって話ですね^^;)
###【Unityパッケージじゃ音プッシュできない】
UnityでFCM対応をする場合、Unityパッケージをインポートすることを勧められると思うのですが、それを利用しても音付きプッシュは打てませんでした。
プッシュ自体は打てたので、打つだけならパッケージのインポートのみでいいかもしれません。
あと、通知アイコンの設定の仕方がわからなかった。。。
のでUnityパッケージを使わないことにしました。
###【FCMコンソールからプッシュを打つことによる挙動の変化】
FCMコンソールからプッシュを打つと、フォアグラウンド(アプリ起動中)とバックグラウンド(アプリ停止中)で挙動が変わるので、コマンドでプッシュを打つ形になりました。
Android FCM(GCM) バックグラウンドでonMessageReceivedを通らない時は
(コマンド内容後述します)
###【トピックにトークンを追加するコードがなぜか動かない】
ネイティブ側で
FirebaseMessaging.getInstance().subscribeToTopic("topic_name");
を呼び出した時に[メソッドがないよエラー]が出てたかな、たしか。
(subscribeToTopic自体はあるけど、中の中で無いメソッドがあったっぽい)
こちらのように実装しても[voidは返り値ないから]みたいなエラーが出たりしてお手上げでした。
...多分入れるライブラリ間違えてるんだよね。
放置はいけないと思いつつ、時間もなかったのでトピックへの追加もコマンド・csで行うようにしました。
(コマンド内容後述します)
###【Resolverを実行したら64k問題が出てきた】
64k問題とは
もともと多数ライブラリの入ったプロジェクトだったので、Firebaseのライブラリを突っ込むためにResolverを実行したら多数更新がかかってしまい大変でした。
とりあえず、Firebase以外のライブラリは[追加]のみを許し[更新]は無かったことにしました。
最終的に動いたから良かったですけど、更新するべきものしないべきものをきちんと判断しなきゃなぁ...
それでは実装タイム
筆者プラグイン導入初心者なので、おてやわらかに。
実装環境
【PC】
・MacOS:HighSierra 10.13.6
【Unity】
・ver:2017.2.2p3
・PackageName:com.push.sound.dev
【Android Studio】
・ver:3.1.2
・PackageName:com.push.liblary
FCMプロジェクトの作成
まずこれをしなきゃ始まらない。
FCMプロジェクトをサクッと作っちゃいます。
・Firebase Consoleにアクセス。
・プロジェクトを追加をクリック
・好みのプロジェクト名を入力し、同意チェックを押し、「プロジェクト作成」をクリック!
私はプロジェクト名を「SoundPushTest」としました。
Unityに必要なライブラリを入れる
必要なライブラリをひとつひとつ調べるのが面倒だったので、下記手順でライブラリを用意しました。
###【firebase_unity_sdk_5.6.0.zipをダウンロード】
・FCMプロジェクト内の左上の歯車をクリックして「プロジェクトの設定」を選択。
・画面をスクロールすると、「プロジェクトにはアプリがありません」の枠が出るのでUnityマークを選択
・こんな画面が出てきます(出てこない場合は、画面をリロードすると出てくるようになるはずです)
・今回はAndroidのみ対応したいので「Register as Android app」にチェックを入れ
Unityプロジェクトのパッケージ名を入力します。
入力したら「アプリを登録」をクリック。
・google-services.jsonのダウンロードを勧められますが、今回の対応では必要ないのでダウンロードしなくても問題ないです。
「次へ」をクリック。
・Firebase Unity SDKのダウンロードを勧められます。これはダウンロードしましょう。
(300MBと書かれてますが、850MBぐらいあります。)
・ダウンロードが終わったらとりあえず「次へ」をクリック。
・最後に「コンソールへ進む」をクリックして一旦完了!
###【Unityプロジェクトの作成 or 起動】
流石にUnityでのプロジェクト作成はわかると思いますので省略します。
FCMプロジェクトで登録したPackagenameと同じ内容を使うことだけ間違えないでください。
作成後、Unityプロジェクトを開いておいてください。
###【ライブラリのインポート】
・ダウンロードしたfirebase_unity_sdk_5.6.0.zipを展開する。
・firebase_unity_sdk/dotnet3/FirebaseMessaging.unitypackage をダブルクリックする
└使用してるUnityVerによってdotnet3かdotnet4かが変わります。
私はなぜかdotnet4で正常に動かなかったのでdotnet3を使用しました(余裕あればそのうち調査します...)
└Unity プロジェクトに Firebase を追加する
・パッケージのインポート画面が現れるので、とりあえず全選択してimportを選択!
・インポート後、Assets -> Play Service Resolve -> Android Resolver -> Resolve を実行し、ライブラリを追加する。
└[Play Service Resolve]メニューが追加されない時は、プロジェクトを開き直してみてください。
└ライブラリは Assets/Plugins/Android 以下に追加されます。
・!!!!? Resolverを実行したらこんなエラーが。
大丈夫、SwitchPlatformでAndroidを設定した後、Unityを再起動したらできるようになります。
└念のためSwitchPlatformのやり方おいときますねっっっ
10秒でOK!Unityでプラットフォームを変更(切り替え)する方法
・ライブラリ以外の更新を無かったことにする
FirebaseMessaging.unitypackageをインポートしなかったことにします。
Assets/Plugins/Android 以外の更新をリセットしちゃいましょう。
(なんなら空プロジェクトでResolver実行後、Plugins/Androidをコピーしてきてもいいかもしれないです。)
・ライブラリの追加完了!
(実はこの後詰まりますが、詳しくは後述の開発と本番を同時に入れるの項目で)
ネイティブ側の実装
ここではAndroidStudioを使います。
###【AndroidStudioプロジェクトのモジュールを作成・起動】
・AndroidStudioで、プロジェクトを新規作成してください(独自で作ってるプラグインを作ってる方はそれを開いてください
・どのアクティビティを追加するか聞かれるので「Add No Activity」を選択してください。
これでプロジェクトが作成されます。
・次にモジュールを作成します。
・File -> New -> New Module を選択してください。
・AndroidLibraryを選択してください。
・ライブラリ名,モジュール名,パッケージ名を設定してください。
これでモジュールの作成が完了です
・プロジェクト作成後、ウィンドウ左上側の「1:Project」をクリックすることでプロジェクト内容が表示されます。
###【AndroidPlayerActivityの拡張】
・下準備としてAndroidPlayerActivityの拡張クラスを作成します。
(すでに拡張済みの人はスルーしてください)
・パッケージ名を右クリックして new -> JavaClass を選択し「UnityBrigde」というクラスを作成してください。
・UnityBridgeを開き、下記を入力してください
//(パッケージ名は環境に合わせてください)
package com.push.liblary;
public class UnityBridge extends UnityPlayerActivity{
}
このままだと「UnityPlayerActivity」の部分がエラーになってしまうので
これから「classes.jar」というものを追加します。
「classes.jar」の格納場所については下記リンクに記述されています。
https://docs.unity3d.com/ja/current/Manual/AndroidUnityPlayerActivity.html
UnityPlayerActivity を拡張するには、Unity に付属の classes.jar を見付けてください。これは、インストールフォルダー(通常 C:\Program Files\Unity\Editor\Data [Windows] または /Applications/Unity [Mac]) のサブフォルダーである PlaybackEngines/AndroidPlayer/Variations/mono または il2cpp/Development or Release/Classes/ 内にあります。次に、新しいアクティビティのコンパイルに使用されるクラスパスに classes.jar 追加します。アクティビティのソースファイルをコンパイルして JAR または AAR パッケージに含め、それをプロジェクトフォルダーにコピーしてください。
「classes.jar」を見つけたら、{プロジェクト名}/{モジュール名}/libs/ 以下に格納してください。
そして必ずリネームを行なってください。
私は「unity-classes.jar」にリネームしました。
(ビルド後不要になるのでclasses.jarを削除するのですが、この名前をそのまま使用してると必要なファイルまで削除してしまうので。)
格納後 File -> Sync Project with Gradle Files を実行すると、エラーが解決します。
ただ、このままだとUnityでのビルド時にエラーが出てしまうので
build.gradle(Module:{モジュール名})をダブルクリックで開く。
下記を入力する。
android.libraryVariants.all { variant ->
variant.outputs.each { output ->
output.packageLibrary.exclude('unity-classes.jar')
}
}
↓こんな感じ。
これで、ライブラリのビルド時にclasses.jarが除外される(のかな?)
下準備完了。
###【Firebaseをプロジェクトに追加】
・Tools/Firebase を選択
・ウィンドウがでてくるので、CloudMessagingの「→Set up Firebase Cloud Messaging」をクリック
・さらにウィンドウが出てくるので「Add FCM to your app」をクリック
(「Connect to Firebase」は押さなくてもいいです)
・こんなウィンドウが出てくるので、TargetModuleに{モジュール名}を選択して、右下の「AcceptChanges」をクリック。
これでFirebaseの追加が完了します。
・↓だがしかし、すぐにエラーが出ます
File google-services.json is missing. The Google Services Plugin cannot function without it.
Searched Location:
/{pcパス}/SoundPushNative/SoundPushLibrary/src/debug/google-services.json
/{pcパス}/SoundPushNative/SoundPushLibrary/google-services.json
訳:google-services.jsonが指定箇所にないぞ
今回は使わないファイルなので、google-services.jsonを読み込まないようにします。
build.gradle(Module:{モジュール名})をダブルクリックしてください。
そして、開いたファイルの一番下の「apply plugin: 'com.google.gms.google-services'」を削除してください。
その後、Build/Clean Project を押すと、エラーが消えます。
###【UnityBridge.javaに処理を書いてく】
・内容
Firebaseの初期化
Android8以降用にChannelGroupの初期化
deviceTokenの取得(プッシュを受け取るために必要なもの)
テキストをクリップボードにコピーする処理。
・UnityBridge.javaに何も考えず下記を突っ込みなさい(説明はソースコメントを読んでね)
//パッケージ名は環境に合わせてください
package com.push.liblary;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.iid.FirebaseInstanceId;
import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;
public class UnityBridge extends UnityPlayerActivity {
static final String TAG = "UnityBridge";
static final String group_name = "SoundPush";
private static UnityBridge instance;
private static Boolean initializedFCM = false;
public static UnityBridge getInstance(){
return instance;
}
/**
起動時処理
*/
@Override protected void onCreate (Bundle savedInstanceState)
{
Log.d(TAG, "Bridge めざめた: ");
super.onCreate(savedInstanceState);
instance = this;
initializedFCM = false;
try {
// 起動時にチャンネルグループを削除する
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = getApplicationContext().getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(group_name);
}
}catch (Exception e){}
}
/**
* Firebaseの初期化を行う(Unity側から呼び出す)
*/
public void initializeFirebase(String appId, String apiKey,String senderId){
if(initializedFCM){
// 二度初期化を行うとエラーが出るのでreturn;
Log.d(TAG, "FCM initialize Already exception: ");
return;
}
initializedFCM = true;
// Firebaseの初期化
Log.d(TAG, "FCM initialize start: ");
FirebaseOptions.Builder builder = new FirebaseOptions.Builder()
.setApplicationId(appId)
.setApiKey(apiKey)
.setGcmSenderId(senderId);
FirebaseApp.initializeApp(getInstance(), builder.build());
Log.d(TAG, "FCM initialize end: ");
}
/**
tokenを取得する
Unityから呼び出す
*/
public void receiveSenderID() {
getInstance().tokenRefresh();
}
/**
tokenを取得する
Unityに通知する
*/
public void tokenRefresh() {
Log.d(TAG, "FCM tokenRefresh start: ");
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d(TAG, "FCM tokenRefresh end: ");
if(refreshedToken==null || refreshedToken.equals("")){
// UnityPlayer.UnitySendMessagの第三引数が空だとアプリが死ぬので何か入れる
refreshedToken="default";
}
// Unityへトークンを通知する
UnityPlayer.UnitySendMessage("FcmCallback", "Fcm_CallBack", refreshedToken);
}
/**
textをクリップボードにコピーする
今回はトークンをコピーするために使います。
*/
public void copyToClipboard(String text) {
final Activity currentActivity = UnityPlayer.currentActivity;
ClipboardManager clipboardManager = (ClipboardManager)currentActivity.getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) {
return;
}
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text));
}
}
説明しなきゃいけないのは↓ここ↓の引数についてですかね。
public void initializeFirebase(String appId, String apiKey,String senderId){
//~略~
// Firebaseの初期化
Log.d(TAG, "FCM initialize start: ");
FirebaseOptions.Builder builder = new FirebaseOptions.Builder()
.setApplicationId(appId)
.setApiKey(apiKey)
.setGcmSenderId(senderId);
FirebaseApp.initializeApp(getInstance(), builder.build());
Log.d(TAG, "FCM initialize end: ");
}
Unity側が呼び出す際に利用するので、その時がきたらまた説明しますね。
###【通知受け取り処理を実装】
・パッケージ名を右クリックして new -> JavaClass を選択し「MyFcmListenerService」というクラスを作成してください。
・MyFcmListenerServiceに何も考えず下記を突っ込みなさい(説明はソースコメントを読んでね)
//パッケージ名は環境に合わせてください
package com.push.liblary;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFcmListenerService extends FirebaseMessagingService {
private static final String TAG = "MyFcmListenerService";
/**
* 通知が来た時に呼ばれる
*/
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
String message = remoteMessage.getData().get("message");
String sound = remoteMessage.getData().get("sound");
Log.d(TAG+"FCM", "FCM Message: " + message);
Log.d(TAG+"FCM", "FCM Sound: " + sound);
Bundle data = new Bundle();
data.putString("message",message);
data.putString("sound",sound);
sendNotification(data);
}
/**
* FCMからのメッセージを受け取り、通知を表示する
*
* @param data FCMからのメッセージデータ
*/
private void sendNotification(Bundle data) {
String channel_id = "updates";
Log.d("Unity","sendNotification start");
String message = data.getString("message");
Uri sound_uri = null;
// サウンドが届いた時
if (data.containsKey("sound")) {
String sound_name = data.getString("sound");
Log.d(TAG, "sound: " + sound_name);
// チャンネル作成
createSoundChannel(sound_name);
// リソースから取り出す
final int id = getResources().getIdentifier(sound_name, "raw", getPackageName());
// リソースが存在する
if (id != 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
channel_id = sound_name;
}
sound_uri = Uri.parse("android.resource://" + getPackageName() + "/" + id);
Log.d(TAG, "sound uri: " + sound_uri);
}
}
Intent intent = new Intent(this, MyFcmListenerService.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
NotificationCompat.Builder notificationBuilder;
notificationBuilder = new android.support.v4.app.NotificationCompat.Builder(this);
// 通知設定
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
// バージョンOreo以上
Log.d("Unity", "sendNotification Oreo");
try {
notificationBuilder
.setContentIntent(pendingIntent)
.setContentTitle(getString(R.string.app_name))
.setContentText(message)
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
.setWhen(System.currentTimeMillis())
.setTicker(message)
.setAutoCancel(true)
.setChannelId(channel_id)
.setSmallIcon(R.drawable.notification_material_icon)
.setColor(Color.rgb(125, 125, 125));
}catch (Exception e){
Log.d("Unity", "sendNotification Oreo Error : " + e);
}
}else {
// バージョンOreo未満
Log.d("Unity", "sendNotification");
notificationBuilder
.setContentIntent(pendingIntent)
.setContentTitle(getString(R.string.app_name))
.setContentText(message)
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
.setWhen(System.currentTimeMillis())
.setTicker(message)
.setAutoCancel(true);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Lollipop以上なら設定するSmallIconを別のものにする
notificationBuilder
.setSmallIcon(R.drawable.notification_material_icon)
.setColor(Color.rgb(125, 125, 125));
} else {
notificationBuilder.setSmallIcon(R.drawable.notification_icon);
}
}
NotificationManager notificationManager;
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if( sound_uri != null ){
notificationBuilder
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE)
.setSound(sound_uri);
}
else {
notificationBuilder.setDefaults(Notification.DEFAULT_ALL);
}
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
Log.d("Unity", "sendNotification End");
}
/**
* チャンネル作成(Android8以降)
* **/
private void createSoundChannel(String sound_name){
// Oreo以降のsdkVersion()を使う場合はチャンネルを作らないと通知されない
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = getSystemService(NotificationManager.class);
// グループの登録
String groupId = UnityBridge.group_name; // 通知チャンネルグループのID
String name = "Sound"; // グループ名
NotificationChannelGroup group = new NotificationChannelGroup(groupId, name);
nm.createNotificationChannelGroup(group);
// チャンネルの作成
NotificationChannel channel = new NotificationChannel(sound_name, "更新情報", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("通知のチャンネル"+sound_name);
// サウンドを登録
Uri sound_uri = null;
final int id = getApplicationContext().getResources().getIdentifier(sound_name, "raw", getPackageName());
// ある
if (id != 0) {
sound_uri = Uri.parse("android.resource://" + getPackageName() + "/" + id);
Log.d(TAG, "sound uri: " + sound_uri);
}
channel.setSound(sound_uri, null);
// チャンネルにグループを設定
channel.setGroup(group.getId());
// マネージャにチャンネルを設定
nm.createNotificationChannel(channel);
}
}
}
Android8用のチャンネル登録や、通知設定できになる箇所があると思いますが
一つ一つ解説すると結構な量になるので、気になる関数は調べてみてください(必要あらば今後説明しますっ)
・「R.drawable」系でエラーが出ちゃったと思います。これらは画像リソースを入れることで解決できます。
「notification_icon.png」「notification_material_icon.png」画像を追加しちゃいましょう。
↑notification_icon.png
↑ notification_material_icon.png
追加する場所は「{パッケージ名}/{モジュール名}/res/drawable」の中です。
app/res/drawable を右クリックして 「Reveal in Finder」をクリックしたら該当のFinderが表示されるので、そこに入れちゃってください。
これでエラーが無くなったと思います。
【音声ファイルを追加する】
今回は音付きプッシュが目的なので、音声ファイルも追加しちゃいます。
追加する場所は「{パッケージ名}/{モジュール名}/res/raw」の中です...が、rawフォルダーが存在しないので追加するところから始めます。
・res を右クリックし New -> Android Resource Directory を選択
・Directory Name:raw
・Resource Type:raw
・Source Set:main
で作成。
・Reveal in Finder でフォルダを表示し
・好きな音声ファイルを追加
(私はmp3でしか確認してないので、他の形式が対応されてるかわからないです。)
ちなみに音源はこちらからいただきました。
効果音ラボ
nekuro_cooking.mp3:ネクロ「どう料理してやろうかのう」
kenshi_star.mp3:剣士「ほ、星が見えるぅ...」
【ネイティブのプロジェクトをビルドする】
これでネイティブの仕込みは終わったのでビルドしたいと思います。
・Build -> Select Build Variant をクリックしてください
・ウィンドウが開くので、作成した{モジュール名}のBuildVariantをreleaseに変更してください。
・Build -> Clean Build をクリックしてください。これでビルドが始まります。
・ビルド完了後、下記フォルダに{モジュール名}-release.aar というフォルダができたら終了です!
・見栄えが悪いので、SoundPushNative.aar といいう名前に変更しちゃいましょう。
(別に好きな名前で大丈夫です)
・SoundPushNative.aarを UnityProjectのAssets/Plugins/Android 以下に移動したら完了です!
Unityとネイティブを連携する
ここではUnityで作業を進めていきます。
【AndroidManifest.xmlの編集】
・Assets/Plugins/Android/AndroidManifest.xmlを開いてください。
こんな感じになってると思います。(すでにAndroidManifestを改造してる方は違うと思いますが。)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="${applicationId}"
android:versionCode="1"
android:versionName="1.0">
<application android:label="@string/app_name"
android:icon="@drawable/app_icon">
<!-- The MessagingUnityPlayerActivity is a class that extends
UnityPlayerActivity to work around a known issue when receiving
notification data payloads in the background. -->
<activity android:name="com.google.firebase.MessagingUnityPlayerActivity"
android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
<service android:name="com.google.firebase.messaging.MessageForwardingService"
android:exported="false"/>
</application>
</manifest>
先ほど作成したUnityBridgeをMainActivityとして利用したいので、下記のように変更してください。
<!-- 変更前 -->
<activity android:name="com.google.firebase.MessagingUnityPlayerActivity"
android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
<!-- 変更後 -->
<!-- (パッケージ名(com.push.liblary)とクラス名(UnityBridge)は環境に合わせてください) -->
<activity android:name="com.push.liblary.UnityBridge"
android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
MyFcmListenerServiceをきちんと動作させるために下記の追加も行います。
<service android:name="com.push.liblary.MyFcmListenerService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
↓完成イメージ
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="${applicationId}"
android:versionCode="1"
android:versionName="1.0">
<application android:label="@string/app_name"
android:icon="@drawable/app_icon">
<!-- The MessagingUnityPlayerActivity is a class that extends
UnityPlayerActivity to work around a known issue when receiving
notification data payloads in the background. -->
<activity android:name="com.push.liblary.UnityBridge"
android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
<service android:name="com.google.firebase.messaging.MessageForwardingService"
android:exported="false"/>
<service android:name="com.push.liblary.MyFcmListenerService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
これで完了です。
###【Unity側のトークン取得処理実装】
ふぅ、やっとc#のコードを書けます。
・FcmCallback.cs というファイルを作ってください。
・FcmCallback.csに何も考えず下記を突っ込みなさい(説明はソースコメントを読んでね)
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;
public class FcmCallback : MonoBehaviour {
// あとで空文字部分を説明しますね!
#if RELEASE
private static string s_SenderId = "";
private static string s_AppId = "";
private static string s_ApiKey = "";
private static string s_ServerKey = "";
#else
private static string s_SenderId = "";
private static string s_AppId = "";
private static string s_ApiKey = "";
private static string s_ServerKey = "";
#endif
private static string s_TopicName = "all";
private bool initialized = false;
private string tokenCache;
[SerializeField] private Text tokenView;
void Start (){
Debug.Log ("Unity Start !");
}
/// <summary>
/// Firebase初期化ボタン押下処理
/// </summary>
public void OnInitializeFirebaseButton(){
InitializeFirebase ();
}
/// <summary>
/// tokenRefreshボタン押下処理
/// </summary>
public void OnTokenRefreshButton(){
TokenRefresh ();
}
/// <summary>
/// トークンをクリップボードにコピーする
/// </summary>
public void OnCopyTokenButton(){
CopyTokenButton ();
}
#if UNITY_EDITOR
private void InitializeFirebase(){}
private void TokenRefresh(){}
private void CopyTokenButton(){}
#elif UNITY_ANDROID
/// <summary>
/// ファイアベースを初期化する
/// </summary>
private void InitializeFirebase(){
tokenView.text = "ファイアベース初期化するよ";
Debug.Log ("ファイアベース初期化するよ");
if (initialized) {return;}
Debug.Log ("initializeFirebase");
// ネイティブパッケージcom.push.liblaryのUnitybridgeクラスの関数initializeFirebaseを呼ぶ
using (AndroidJavaObject aAndroidObject = new AndroidJavaObject("com.push.liblary.UnityBridge"))
{
aAndroidObject.Call("initializeFirebase", s_AppId, s_ApiKey, s_SenderId);
}
initialized = true;
}
/// <summary>
/// トークンをリフレッシュ・取得する
/// </summary>
private void TokenRefresh(){
if (!initialized) {tokenView.text = "先に初期化をしてください"; return;}
Debug.Log ("tokenRefresh");
// ネイティブパッケージcom.push.liblaryのUnitybridgeクラスの関数receiveSenderIDを呼ぶ
using (AndroidJavaObject aAndroidObject = new AndroidJavaObject("com.push.liblary.UnityBridge"))
{
aAndroidObject.Call("receiveSenderID");
}
}
/// <summary>
/// ネイティブが取得したトークンを受け取る
/// </summary>
/// <param name="token">トークン.</param>
public void Fcm_CallBack(string token){
Debug.Log ("fcm callback:"+token);
if (token != null) {
var dic = new Dictionary<string,string> ();
dic.Add ("token", token);
/*必要があれば、トークンを自作サーバーに送る処理があってもいいかもしれませんね!*/
// ZisyaServerNiOkuru(token);
}
tokenCache = token;
tokenView.text = "token:" + tokenCache;
}
/// <summary>
/// トークンをクリップボードにコピーする
/// </summary>
private void CopyTokenButton(){
if(tokenCache.Equals("")){ Debug.Log ("can not copy"); return;}
Debug.Log("copy:"+tokenCache);
using (AndroidJavaObject aAndroidObject = new AndroidJavaObject("com.push.liblary.UnityBridge"))
{
aAndroidObject.Call("copyToClipboard",tokenCache);
}
}
#endif
}
ちなみに「Fcm_CallBack」というメソッド名はUnityBridge.java内のUnitySendMessage第二引数と同じであればなんでも大丈夫です。
public void tokenRefresh() {
//~略
// Unityへトークンを通知する
UnityPlayer.UnitySendMessage("FcmCallback", "Fcm_CallBack", refreshedToken);
// 第一引数:UnityのScene内にあるオブジェクト名
// 第二引数:オブジェクトに存在するメソッド名
// 第三引数:Unity側メソッドの第一引数
}
クラス作成を終えたので、今度はシーンをいじっていきます。
ここら辺はみなさん慣れたものだと思うので、簡単に説明していきますね。
・「FcmCallback」という名前の空のオブジェクトを作成し、FcmCallback.csをアタッチしてください。
・「Firebase初期化ボタン(Button)」「tokenRefreshボタン(Button)」「CopyTokenボタン(Button)」「Token表示枠(Text)」を作ってください。
こんな感じ
・「tokenView」をFcmCallbackのtokenViewに登録してください
・ボタン押下時に下記のようにそれぞれ対応する処理を呼び出すようにしてください。
InitFirebaseButton押下処理 -> FcmCallback.OnInitializeFirebaseButton
TokenRefreshButton押下処理 -> FcmCallback.OnTokenRefreshButton
CopyTokenButton押下処理 -> FcmCallback.OnCopyTokenButton
これで完了です!
Sceneを保存して一休みしておいてください。
###【FCMの情報を追加】
FcmCallback.csを追加してもらいましたが、説明してない部分がありましたね。
#if RELEASE
private static string s_SenderId = "";
private static string s_AppId = "";
private static string s_ApiKey = "";
private static string s_ServerKey = "";
#else
private static string s_SenderId = "";
private static string s_AppId = "";
private static string s_ApiKey = "";
private static string s_ServerKey = "";
#endif
これらに入力するべきものはこいつらです!
s_ServerKeyは、3枚目の画像の「サーバーキー」の部分になります。
これらを#else 〜 #endif のところにコピーしてきてください。
#if RELEASE
private static string s_SenderId = "";
private static string s_AppId = "";
private static string s_ApiKey = "";
private static string s_ServerKey = "";
#else
private static string s_SenderId = "7xxxxxxxxxx0";
private static string s_AppId = "1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4";
private static string s_ApiKey = "Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8";
private static string s_ServerKey = "AxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPSksO";
#endif
これで完了です!
###【アプリのビルド】
ビルドタイム!
この辺も皆さんは慣れたもんだと思うので割愛します。
ただ二点程
・PackageNameをFCMプロジェクトで登録したものと同じものにしてください
(私の場合com.push.sound.dev)
・BuildSettings の BuildSystem をGradle(new)にしておいてください。
ではビルドをし...
なんじゃこりゃ、自プロジェクトでやった時はこんなのでなかったぞ(汗汗汗
とりあえず、同じところで引っかかった人のために画像だけでなくテキストでもエラー内容を残しとこう...
com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.dex.DexException: Multiple dex files define Lcom/google/firebase/BuildConfig;
###【緊急・ビルドエラーの解決(firebaseライブラリ編)】
com.google.firebase.BuildConfigが複数宣言されてるっぽい?
Resolver実行でライブラリ取得したはずなのに何故こんなことが起きるの...
とりあえず下記ファイルはUnity実装想定のライブラリっぽいので削除します
(今回はAndroidのみの対応なので)
↓削除したファイル
・Assets/Plugins/Android/com.google.firebase.firebase-app-unity-5.6.0
・Assets/Plugins/Android/com.google.firebase.firebase-messaging-unity-5.6.0
・Assets/Plugins/Android/libmessaging_unity_player_activity.jar
そしてfirebase系ライブラリ群の中に、なぜかフォルダ形式の「com.google.firebase.firebase-common-16.1.0」
があったので、com.google.firebase.firebase-common-16.1.0.aarをダウンロードし、差し替えました。
ダウンロードはこちらから
ビルド実行...成功! やったね!
【実機にインストールする】
PCとAndroid端末をつなげ、ターミナルから下記を実行
$ adb install -r {apkパス}/{ビルドしたapk名}.apk
$ adb install -r apkpath/pushTest.apk
adbコマンドが使えない場合はこちらから
adbをMacのターミナルで使えるようにする
【いざ、実機確認!】
「Firebase初期化」を押し〜の...
「TokenRefresh」を押し〜...ん?? default??? いや、慌てなさんな。
深呼吸してもう一度「TokenRefresh」を押し〜の...
トークン取れた!!!
初期化には少し時間がかかるらしいですね。
(ちなみに圏外の状態だとトークンが取得できなかったりします)
(さらにちなみにトークンを別のものに変更したくなったら、一度アプリをアンインストールして再度インストールしたら変更されます。)
取得したトークンをPCに送信する
トークン表示時に「CopyToken」を押せばクリップボードにコピーされると思います。
各々すきなやり方でPCにトークンを送っちゃってください。
(AndroidStudioでログ取得してトークンゲットするのもアリです)
ターミナルでプッシュを打つ!
【登録確認】
トークンをPCに持って来ましたか?
そしたらまずは、FCMにトークンが登録されているのか確認してみましょう。
ターミナルで下記コマンドを叩いてください。
(1行で書いてくださいね)
curl -H "Authorization:key={サーバーkey}" "https://iid.googleapis.com/iid/info/{トークン}?details=true"
curl -H "Authorization:key=AAAAxxxxxxxxxx~~~~~~~abcd" "https://iid.googleapis.com/iid/info/xxxxxxxxxxxx~~~~~~~~xxxxxxx?details=true"
サーバーkey:FcmCallback.cs->s_ServerKey
トークン:取得したトークン
するとこんな結果が返ってくると思います。
返って来ていれば成功です。
{"applicationVersion":"1","connectDate":"2019-03-28","attestStatus":"UNKNOWN","application":"com.push.sound.dev","scope":"*","authorizedEntity":"xxxxxxxxxxx","connectionType":"WIFI","appSigner":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","platform":"ANDROID"}
ちなみに失敗パターンはこちら(サーバーKeyやトークンが間違ってる時)
叩いたコマンドや取得した情報を見直してみてください。
{"error":"InvalidToken"}
または
{"error":"Failed to decode authentication token"}
その他 {error} というレスポンスが返ってくる
【プッシュを打つ】
ここまで来たらプッシュが打てます。
ターミナルで下記コマンドを叩いてください。
(1行で書いてくださいね)
curl https://fcm.googleapis.com/fcm/send -H "Content-type: application/json" -H "Authorization:key={サーバーkey}" -X POST -d '{"to":"{トークン}","data": {"message" : "プッシュ通知を打つのだよ", "sound" : "{音声ファイル名}"},"priority":"high"}'
{音声ファイル名}は拡張子抜きで大丈夫です。
私なら「nekuro_cooking」や「kenshi_star」ですね。
そして❤️と⭐️アイコンに気づきましたね!
MyFcmListenerService.javaのここで設定しているのです!
private void sendNotification(Bundle data) {
//~略
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
.setSmallIcon(R.drawable.notification_material_icon)
//~略
}
画像を差し替えれば好きなアイコンを表示させることができますよ。
MyFcmListenerService.javaを弄ればプッシュ内容にアイコン情報を含めることも可能になります(多分)
トピックを指定してプッシュ!
トピックとはグループみたいなものです(ざっくり)
たくさんのユーザーにプッシュを送るときに1つ1つ送るのは大変なのでトピックにまとめ、トピックに対しプッシュを打っちゃおうって感じです。
ではtopicに追加しますか。
###【トピックへトークンを追加(ターミナルで追加編)】
ターミナルで下記コマンドを叩いてください。
(1行で書いてくださいね)
curl https://iid.googleapis.com/iid/v1/{トークン}/rel/topics/{トピック名} -H "Content-type: application/json" -H "Content-Length: 0" -H"Authorization:key={サーバーKey}" -X POST
{トークン}と{サーバーKey}はもうわかりますね。
トピック名は好きな名前を使っちゃっていいです。
私は全体プッシュを想定しているので「all」を使用しました。
ちなみに成功したときに帰ってくるレスポンスはこれだけです。
{}
エラーが出た場合は、何か設定が間違ってないか確認してみてくださいね。
トピックへの追加コマンドが成功したら、「登録確認コマンド」を叩いてみてください。
このような結果が返ってくるかと思います。
{"applicationVersion":"1","connectDate":"2019-03-28","attestStatus":"UNKNOWN","application":"com.push.sound.dev","scope":"*","authorizedEntity":"xxxxxxxxxxx","rel":{"topics":{"{トピック名}":{"addDate":"2019-03-28"}}},"connectionType":"WIFI","appSigner":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","platform":"ANDROID"}
"rel":{"topics":{"{トピック名}":{"addDate":"2019-03-28"}}}
が増えているかと思います。
これでトピックへの追加は完了です。
###【トピックに対しプッシュを打つ】
トピックへのプッシュコマンドは、トークンの時とさほど変わりません。
curl https://fcm.googleapis.com/fcm/send -H "Content-type: application/json" -H "Authorization:key={サーバーkey}" -X POST -d '{"to":"/topics/{トピック名}","data": {"message" : "プッシュ通知を打つのだよ", "sound" : "{音声ファイル名}"},"priority":"high"}'
これで問題なく届くと思います。
【アプリ側でトピックを追加しといてもらう】
まどろっこしいことをさせてすみません。
実はこうすればアプリ側からトピックにトークンを追加することができるんです。
public void tokenRefresh() {
//~略
// トピック{all}にトークンを追加する
FirebaseMessaging.getInstance().subscribeToTopic("all");
// Unityへトークンを通知する
UnityPlayer.UnitySendMessage("FcmCallback", "Fcm_CallBack", refreshedToken);
}
ですが、私の環境ではなぜかエラーが出て動いてくれなかったので実装から外してしまいました...
とはいえ、アプリ側から追加出た方が楽ですよねってことで、FcmCallback.csの方に手を加えて自動追加できるようにしました。
public class FcmCallback : MonoBehaviour {
// "all"はお好みの名前で
private static string s_TopicName = "all";
//~略
#if UNITY_EDITOR
private void InitializeFirebase(){}
private void TokenRefresh(){}
private void CopyTokenButton(){}
#elif UNITY_ANDROID
//~略
/// <summary>
/// ネイティブが取得したトークンを受け取る
/// </summary>
/// <param name="token">トークン.</param>
public void Fcm_CallBack(string token){
Debug.Log ("fcm callback:"+token);
if (token != null) {
//~略
/*追加!*/
StartCoroutine (AddTopic(token));
}
tokenCache = token;
tokenView.text = "token:" + tokenCache;
}
/*追加!*/
/// <summary>
/// トピックにトークンを追加する
/// </summary>
/// <param name="token">トークン.</param>
IEnumerator AddTopic(string token) {
string url = "https://iid.googleapis.com/iid/v1/"+token+"/rel/topics/"+s_TopicName;
WWWForm form = new WWWForm ();
form.AddField ("dummy", "dummy");
UnityWebRequest request = UnityWebRequest.Post(url,form);
// SetRequestHeaderを介してリクエストヘッダを設定する
request.SetRequestHeader("Content-type", "application/json");
request.SetRequestHeader("Authorization", "key=" + s_ServerKey);
// リクエスト送信
yield return request.Send();
// 通信エラーチェック
if (request.isHttpError || request.isNetworkError) {
Debug.LogError(request.error);
} else {
Debug.Log ("succsess send token");
}
}
}
処理を追加したらビルドしてください。
新しいトークンでないと正常にトピックに追加できたかわからないので、一旦アプリをアンインストールしてから、再度ビルドしたapkを実機にインストールしてください。
インストール後「RefreshToken」を押しトークンが表示されたら、PCで「登録確認コマンド」を叩いてください。
(新しいトークンを設定してから叩いてくださいね!)
"rel":{"topics":{"{トピック名}":{"addDate":"2019-03-28"}}}
が表示されていれば大成功です。
プッシュコマンドを打ってみてください、成功しますから。
開発と本番を同時に入れる
【本番用のFCMプロジェクトを作成する】
基本FCMプロジェクトの作成と同じなので、画像は省略しますね。
・Firebase Consoleにアクセス。
・プロジェクトを追加をクリック
・好みのプロジェクト名を入力し、同意チェックを押し、「プロジェクト作成」をクリック!
私はプロジェクト名を「SoundPushProduct」としました。
・FCMプロジェクト内の左上の歯車をクリックして「プロジェクトの設定」を選択。
・画面をスクロールすると、「プロジェクトにはアプリがありません」の枠が出るのでUnityマークを選択
・今回はAndroidのみ対応したいので「Register as Android app」にチェックを入れ
Unityプロジェクトの本番用パッケージ名を入力します。
私は「com.push.sound.pro」としました。
入力したら「アプリを登録」をクリック。
・アプリの登録が済んだら、一番上までスクロールし左側にある「×」を押して閉じましょう。
完了!
【本番のトークンやキーをアプリに追加する】
FcmCallback.csを開いて、
本番用FCMプロジェクトの情報を#RELEASE 〜 #else のところにコピーしてきてください。
//~略
#RELEASE
private static string s_SenderId = "xxxxxxxxxx";
private static string s_AppId = "xxxxxxxxxxxxxxxxxxxxxx";
private static string s_ApiKey = "xxxxxxxxxxxxxxxxxxxxxx";
private static string s_ServerKey = "xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx";
#else
//~略
【本番用のビルド設定を行う】
Unityの設定はみなさん大得意だと思うのでざっくりと。
・PlayerSettingsを開きます。
・ProductNameを「プッシュ本番」にします(本番と分かればなんでもok)
・OtherSettings -> PackageNameを「com.push.sound.pro」にします(本番用FCMプロジェクトに登録したパッケージ名と合わせるように)
・OtherSettings -> ScriptingDefineSymbols に「RELEASE」を入力してEnterを押す(Enterを押さないと反映されません。)
これでFcmCallback.cs内の「#RELEASE 〜 #else」の間が適応されるようになります。
気になる方はFcmCallback.csを開いてみてください。
・BuildSettingを開き、DevelopmentBuild のチェックを外す
・ビルド開始!
【いざインストール】
の前に、開発と本番の両方を同時に入れることが目的なので
実機に開発用のアプリがインストールされてることを確認してください。
確認できたらこのコマンドを叩いてインストール
$ adb install -r apkpath/pushProduct.apk
Success
...あれ? Success?
自プロジェクトでやった時はエラーが出たんだけどな〜〜〜
SuccessならSuccessで万々歳か...
【if もしここでエラーが出るとしたら】
私が時プロジェクトでインストールしようとした時はこんなエラーが出ました
(パッケージ名は今回のものに差し替えて説明してます)
$ adb install -r apkpath/pushProduct.apk
Failure [INSTALL_FAILED_DUPLICATE_PERMISSION perm=com.push.sound.dev.permission.C2D_MESSAGE pkg=com.push.sounde.dev]
訳:com.push.sound.dev.permission.C2D_MESSAGEという権限はすでにパッケージ[com.push.sounde.dev]で宣言されてるよ。
だからcom.push.sounde.proでは使えないんだ。
・原因:Resolver実行でFirebase系ライブラリを追加すると、パーミッションのパッケージ名部が追加した時のパッケージ名で固定されるらしい。
つまり、com.push.sounde.proでもcom.push.sound.devというパッケージ名で権限が登録されてることに。
UnityのGradleビルドでPlayServicesResolverとおさらばする手順
パッケージ名(バンドルID)の自動置換をResolver実行時にしか行ってくれない
・対応:こちらのサイトから対応したFirebase系ライブラリをダウンロード・追加し、Resolver実行でインポートしたFirebase系ライブラリとはおさらばする。
これでもう一度開発と本番をビルドすればいれられるようになります。
ターミナルでプッシュを打つ2!!
・開発、本番を同時に入れられたことで、それぞれにプッシュを打ってみましょう。
・別々で通知を受け取れることを確認する。
...やったー!
プッシュを打つ環境を作る
このままだとプッシュを打つたびにターミナルを起動することになるので
私はJenkinsを使ってプッシュするようにしました。
シェルスクリプトで対話式で打ったり等、やり方はいろいろあると思うので、各々試してくださいませ。
最後に
最初に「長ったらしい記事が一つくらいあってもいいと思う」とか言いましたけど
これ、書くのもですが読むのもしんどそうですね...反省。
次回からはなるだけ分割できるようにしたいと思います。
いずれiOSも対応する予定なので、そのうち記事書くかと思います。