■目次
注意点や前提などAndroidアプリの作成Firebaseの設定AmazonSNSの設定AndroidアプリにPush機能を実装PHPプログラムの作成動作確認iOSアプリの作成トラブルAmazonSNSを使わずにPHPでプッシュを送信(Android)AmazonSNSを使わずにPHPでプッシュを送信(iOS)トークン管理Firebaseから本番用と開発用のそれぞれにプッシュを送信Apple Developer Program から本番用と開発用のそれぞれにプッシュを送信プッシュ通知の受診時にダイアログを表示する(Android)プッシュ通知のタップ時に任意の画面を開くアプリからプッシュ通知の設定画面を開くAPNSの証明書を更新その他考察メモその他参考ページサイレントプッシュ
■注意点や前提など
■参考ページ Android から Amazon SNS を使ってみる - Qiita https://qiita.com/kusokamayarou/items/27e023ad06cade20c731 ※Android版のみだが、全体の流れとスクリーンショットは参考になる 以下に記載したKotlin・Swift・PHPプログラムなどは別途調査してくれた人のものをベースにしている 現状 Amazon Cognito は使っていない ■開発環境 ※サーバサイドはVagrantで構築している ここではホストPCからは http://192.168.33.10/ で、同一LAN内の他端末からは http://192.168.1.14:8080/ でアクセスできるものとする ■アプリのID ※公開すると変更できないので、慎重に決定したい ※iOSとAndroidの両方で作る、本番用と開発用がある、Pushも使用する などを考慮する ※現状の結論として、iOSもAndroidも net.refirio.pushtest1 で作って開発版書き出し時には .dev を付ける…が良さそう iOSとAndroidでIDを統一できるように、「pushtest1」部分にハイフンやアンダーバーは無い方が無難 詳細はこのテキストの「その他考察メモ」も参考に ■Androidのターゲットデバイス ※Android4は今はSSLへのリクエストに難がある できるだけAndroid5以降をターゲットデバイスとする方が無難そう Android4系端末のTLS1.1&1.2対応について - Qiita https://qiita.com/ntsk/items/9f31fc7b44c04ea45e0b android - javax.net.ssl.SSLException: Read error: ssl=0x9524b800: I/O error during system call, Connection reset by peer - Stack Overflow https://stackoverflow.com/questions/30538640/javax-net-ssl-sslexception-read-error-ssl-0x9524b800-i-... ■GCMの廃止 ※GCMは廃止が決定している 今新規に作るならFCMで作る 「Googleクラウドメッセージング(GCM)」が1年後に廃止、「Firebase Cloud Messaging(FCM)」への移行が必要に:Googleのアプリメッセージング基盤が完全に交代 - @IT http://www.atmarkit.co.jp/ait/articles/1804/13/news051.html おじさんのための2018年スマホPUSH通知事情 (+GCM終了のお知らせ) - Qiita https://qiita.com/keidroid/items/290af7b99952e889f4a7 ご注意ください!プッシュ配信GCM廃止について(〜2019年04月10日まで) - ニフクラ mobile backend(mBaaS)お役立ちブログ https://blog.mbaas.nifcloud.com/entry/2018/04/11/165730
■Androidアプリの作成
※いったんプッシュ機能の無い状態でアプリを作る 新規 Android Studio プロジェクトの開始 ↓ プロジェクトの選択 「空のアクティビティ」が選択されているので、そのまま「次へ」 ↓ プロジェクトの構成 名前: Push Test1 パッケージ名: net.refirio.pushtest1 (名前をもとに自動入力される) 保存ロケーション: C:\Users\refirio\AndroidStudioProjects\PushTest1 (アプリケーション名をもとに自動入力される) 言語: Kotlin 最小APIレベル: API 19: Android 4.4 (KitKat) 「完了」 エミュレータと実機で、アプリを起動できるかテストする
■Firebaseの設定
■Firebaseプロジェクトの作成 公式サイト https://firebase.google.com/ コンソール https://console.firebase.google.com/ Firebaseのコンソールにアクセスし、「プロジェクトを追加」をクリック プロジェクト名: Push Test プロジェクトID: push-test-54dbc(プロジェクト名から自動で入力される) 地域 / ロケーション: アナリティクスの地域: 日本 Cloud Firestore のロケーション: us-central(変更せず) それぞれ規約の同意にチェックを入れて「プロジェクトを作成」をクリック しばらく待つと「新しいプロジェクトの準備ができました」と表示されるので「次へ」をクリック 「アプリに Firebase を追加して利用を開始しましょう」という画面へ遷移する 「開始するにはアプリを追加してください」の文言の上にある、Androidのアイコンをクリック 「Android アプリに Firebase を追加」という画面へ遷移する ※作成済みのプロジェクトを使用する場合、そのプロジェクトの画面に移動して 「+アプリを追加 → (Androidアイコン)」 をクリックする ■Firebaseアプリの作成 「1 アプリの登録」という画面になる Android パッケージ名: net.refirio.pushtest1 アプリのニックネーム: Push Test1 Dev (開発版想定。本番では「Push Test1」とする) デバッグ用の署名証明書 SHA-1: (空欄) 上記を入力して「アプリを登録」をクリック 「2 設定ファイルのダウンロード」という画面になる 「google-services.json をダウンロード」をクリック 表示される解説どおり、「Androidプロジェクトの新規作成」で作成したアプリの「app」直下にファイルを配置する (Android Studio で、「Android」ではなく「プロジェクト」に表示を切り替えてから配置する) 配置できたら「次へ」をクリック 「3 Firebase SDK の追加」という画面になる プロジェクト直下の build.gradle と、「app」直下の build.gradle に指定のコードを追加する 追加したら、AndroidStudioの画面上部に「今すぐ同期」が表示されるのでクリックする ビルドが完了されるまで待つ。完了したら「次へ」をクリック (google-services.json が見つからない旨のエラーが表示されたら、配置場所を確認してプロジェクトのクリーンと再ビルドを試す) 「4 アプリを実行してインストールを確認」という画面になる 指示通りアプリを実行してしばらく待つと「Firebase がアプリに正常に追加されました。」と表示される (何十秒か待つ。エミュレータで実行しても大丈夫だった。実機で実行した場合、その実機がインターネットに繋がっている必要がある) 「コンソールに進む」が表示されるのでクリック ■サーバーキーの取得 コンソールに「Push Test1 Dev」と表示されているのでクリック 歯車(設定)のアイコンをクリック 設定画面に遷移するので、タブメニューから「クラウドメッセージング」をクリック 「サーバーキー」の値を確認する AAAAsNOhnZY:APA91bEp7obtws-R7EoKzv3NhywPdBoxaHW2iFKR4aCtgQtSppooVN-oAXATFUDeGCuPqb7NADe3XVycNWSBvsC8ip4ta_ejWVH-ABCDEFGHIJKLMN-XXXXXXXXXX
■AmazonSNSの設定
■AmazonSNSアプリケーションの作成 AWSのコンソールから「Simple Notification Service」を開く 左メニューから「アプリケーション」を開く 画面上部の「プラットフォームアプリケーションの作成」をクリック アプリケーション名: Push-Test1-GCM-Dev(開発版想定。本番なら「Push-Test1-GCM」などとする。iOS用にも作る可能性があるので、単に「Push-Test1」ではなく「GCM」の文字を含めておくのが無難そう) プッシュ通知プラットフォーム: Google Cloud Messaging (GCM) APIキー: (「サーバーキーの取得」で取得した「サーバーキー」) ※「プッシュ通知プラットフォーム」は「GCM」となっているが、将来的には「FCM」に表記が修正されると思われる 「プラットフォームアプリケーションの作成」をクリック アプリケーション一覧に追加されたことを確認する ■AmazonSNSトピックの作成 左メニューから「トピック」を開く 「新しいトピックの作成」ボタンを押すとトピックの画面が開くので トピック名: Push-Test1-GCM-Dev-Topic1 表示名: (SMS用なので空欄) 「トピックの作成」をクリック トピック一覧に追加されたことを確認する (表示されるARNの値は、後ほどPHPプログラムに設定する) トピックのARNを指定するだけで一斉送信ができるので、運用の際は「全端末配信用」のトピックを作っておくといい 後からトピックを追加すると、トピックに対して端末を登録する必要があるので注意 トピックを気軽に増減する設計は避けるほうがいいかも。要検証
■AndroidアプリにPush機能を実装
※プッシュ受信時のアイコン指定は必須みたい?その他、慣れないうちはうかつにコードを削除しないほうが良さそう ■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\build.gradle
implementation 'com.google.firebase:firebase-core:16.0.1' ↓ implementation 'com.google.firebase:firebase-core:16.0.1' implementation 'com.google.firebase:firebase-messaging:17.3.0'
「firebase-messaging」の最新バージョンは以下で確認できる https://firebase.google.com/docs/android/setup?hl=ja 追加したら「今すぐ同期」 ■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\AndroidManifest.xml
<activity android:name=".MainActivity"> 〜略〜 </activity>
の直後に以下を追加
<service android:name=".MyFirebaseMessagingService"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT"/> </intent-filter> </service>
■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\res\drawable-hdpi プッシュ用のアイコン画像を追加(アイコンの指定を省略することはできないみたい) ic_launcher.png ... 72px×72pxのPNG画像にしたが、サイズなど改めて調査したい ic_stat_notification.png ... 38px×38pxのPNG画像にしたが、サイズなど改めて調査したい ■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\res\layout\activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_log_token" android:layout_width="120dp" android:layout_height="42dp" android:text="Log Token" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.06" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.034" /> <Button android:id="@+id/button_post_token" android:layout_width="120dp" android:layout_height="42dp" android:layout_marginTop="8dp" android:text="Post Token" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.56" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.017" /> <Button android:id="@+id/button_notify" android:layout_width="95dp" android:layout_height="42dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp" android:text="NOTIFY" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="15dp" android:text="DeviceToken" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_log_token" /> <EditText android:id="@+id/text_result" android:layout_width="0dp" android:layout_height="209dp" android:layout_marginBottom="10dp" android:layout_marginTop="80dp" android:ems="10" android:gravity="top|left" android:inputType="text|textMultiLine" android:singleLine="false" android:textAlignment="viewStart" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" tools:textAlignment="inherit" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="134dp" android:text="Log" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/text_device_token" /> <EditText android:id="@+id/text_device_token" android:layout_width="357dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="92dp" android:ems="10" android:inputType="text" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.545" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>
■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\res\values\strings.xml
<resources> <string name="app_name">Push Test1</string> <string name="default_notification_channel_id">0</string> </resources>
■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\java\net\refirio\pushtest1\MainActivity.kt
package net.refirio.pushtest1 import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.support.v4.app.NotificationCompat import android.support.v7.app.AppCompatActivity import android.util.Log import android.widget.Button import android.widget.EditText import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.iid.FirebaseInstanceId const val TIMEOUT = 10 * 1000 class MainActivity : AppCompatActivity() { companion object { private const val TAG = "MainActivity" const val NOTIFICATION_ID = 1 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // トークン取得ボタン val buttonLogToken = findViewById<Button>(R.id.button_log_token) // トークン送信ボタン val buttonPostToken = findViewById<Button>(R.id.button_post_token) // 各テキストフィールド val textDeviceToken = findViewById<EditText>(R.id.text_device_token) val textResult = findViewById<EditText>(R.id.text_result) // トークン取得ボタンクリック時の処理 buttonLogToken.setOnClickListener { FirebaseInstanceId.getInstance().instanceId.addOnCompleteListener(OnCompleteListener { task -> if (!task.isSuccessful) { Log.d(TAG, "Result: getInstanceId failed", task.exception) return@OnCompleteListener } val token: String = task.result.token textDeviceToken.setText(token) textResult.append("Token is $token\n") }) } // トークンをPHPへ送信するボタンクリック時の処理 buttonPostToken.setOnClickListener { val token:String = textDeviceToken.text.toString() if (token.isNotEmpty()) { MyFirebaseMessagingService.sendRegistrationToServer(token) { responseBody, httpStatus -> // コールバック textResult.append("Result: $httpStatus, $responseBody\n") } textResult.append("Token sent\n") } else { textResult.append("Token is empty\n") } } // 通知の表示テスト(プッシュの受信に関係なく、任意のタイミングで通知を表示) val buttonNotify = findViewById<Button>(R.id.button_notify) buttonNotify.setOnClickListener { val packageContext : Context = this val intent = Intent(packageContext, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) val pendingIntent : PendingIntent = PendingIntent.getActivity(packageContext, 0 /* Request code */, intent, PendingIntent.FLAG_ONE_SHOT) val channelId : String= getString(R.string.default_notification_channel_id) val notificationBuilder : NotificationCompat.Builder = NotificationCompat.Builder(packageContext, channelId) .setSmallIcon(R.drawable.ic_stat_notification) .setContentTitle(getString(R.string.app_name)) .setContentText("これは通知のテストです。") .setAutoCancel(true) .setContentIntent(pendingIntent) val notificationManager : NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Since android Oreo notification channel is needed. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); } } }
■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\java\net\refirio\pushtest1\MyFirebaseMessagingService.kt
package net.refirio.pushtest1 import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.support.v4.app.NotificationCompat import android.util.Log import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import org.json.JSONObject class MyFirebaseMessagingService : FirebaseMessagingService() { /** * メッセージ受信時の処理 */ override fun onMessageReceived(remoteMessage: RemoteMessage?) { Log.d(TAG, "From: " + remoteMessage!!.from!!) // Check if message contains a data payload. if (remoteMessage.getData().isNotEmpty()) { Log.d(TAG, "Message data payload: " + remoteMessage.getData().get("default")) // 10秒以上処理にかかる場合は、Firebase Job Dispatcherを使用する sendNotification(this, remoteMessage.getData().get("default")) } // Check if message contains a notification payload. if (remoteMessage.notification != null) { Log.d(TAG, "Message Notification Body: " + remoteMessage.notification!!.body!!) sendNotification(this, remoteMessage.notification!!.body!!) } } /** * 新しいデバイストークンが発行された時に呼び出される処理 */ override fun onNewToken(token: String?) { Log.d(TAG, "Refreshed token: " + token!!) // デバイストークンをサーバに登録 sendRegistrationToServer(token) {responseBody, httpStatus -> } } /** * 通知を表示する */ private fun sendNotification(packageContext : Context, messageBody : String?) { val intent = Intent(packageContext, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) val pendingIntent : PendingIntent = PendingIntent.getActivity(packageContext, 0 /* Request code */, intent, PendingIntent.FLAG_ONE_SHOT) val channelId : String= getString(R.string.default_notification_channel_id) val notificationBuilder : NotificationCompat.Builder = NotificationCompat.Builder(packageContext, channelId) .setSmallIcon(R.drawable.ic_stat_notification) .setContentTitle(getString(R.string.app_name)) .setContentText(messageBody) .setAutoCancel(true) .setContentIntent(pendingIntent) val notificationManager : NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Since android Oreo notification channel is needed. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); } companion object { private const val TAG = "MyFirebaseMsgService" /** * PHP経由でデバイストークンをAWS SNSへ登録する */ fun sendRegistrationToServer(token: String?, callback: (responseBody: String, httpStatus:Int) -> Unit) { // AWS SNSのプラットフォームエンドポイントに登録する処理 var body : String val builder = Uri.Builder() builder.appendQueryParameter("api", "createPlatformEndpoint") builder.appendQueryParameter("deviceToken", token) body = builder.build().encodedQuery HttpTask { it, httpStatus -> if (it == null) { Log.d(TAG, "connection error") return@HttpTask } Log.d(TAG, "Result:$httpStatus, $it") callback(it, httpStatus) }.execute("POST", "http://192.168.1.14:8080/code/pushtest1-dev/api.php", body) } } }
■C:\Users\refirio\AndroidStudioProjects\PushTest1\app\src\main\java\net\refirio\pushtest1\HttpRequest.kt
package net.refirio.pushtest1 import android.os.AsyncTask import java.io.* import java.net.HttpURLConnection import java.net.URL class HttpTask(callback: (String?, Int) -> Unit) : AsyncTask<String, Unit, String>() { var callback = callback var httpStatus = 0 override fun doInBackground(vararg params: String): String? { val url = URL(params[1]) val httpClient = url.openConnection() as HttpURLConnection httpClient.readTimeout = TIMEOUT httpClient.connectTimeout = TIMEOUT httpClient.requestMethod = params[0] if (params[0] == "POST") { httpClient.instanceFollowRedirects = false httpClient.doOutput = true httpClient.doInput = true httpClient.useCaches = false } try { if (params[0] == "POST") { httpClient.connect() val os = httpClient.outputStream val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8")) writer.write(params[2]) writer.flush() writer.close() os.close() } httpStatus = httpClient.responseCode if (httpStatus == HttpURLConnection.HTTP_OK) { val stream = BufferedInputStream(httpClient.inputStream) val data: String = readStream(inputStream = stream) return data } else { println("ERROR ${httpClient.responseCode}") } } catch (e: Exception) { e.printStackTrace() } finally { httpClient.disconnect() } return null } private fun readStream(inputStream: BufferedInputStream): String { val bufferedReader = BufferedReader(InputStreamReader(inputStream)) val stringBuilder = StringBuilder() bufferedReader.forEachLine { stringBuilder.append(it) } return stringBuilder.toString() } override fun onPostExecute(result: String?) { super.onPostExecute(result) callback(result, httpStatus) } }
■PHPプログラムの作成
あらかじめ、以下からSDKを入手しておく 各プログラムの同階層に lib を作り、その中に入手したプログラムを格納するものとする https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html なおComposerを使う場合、以下でインストールできる $ composer require aws/aws-sdk-php フォルダ名は開発版想定。本番用を作るなら pushtest1 フォルダなどに別途作ると良さそう 実際にアプリからPHPプログラムを呼び出す場合、SSL経由にする必要があるので注意 ■pushtest1-dev\AwsSns.php
<?php use Aws\Sns\SnsClient; class AwsSns { protected $snsClient; /** * コンストラクタ * * @param string $accessKey * @param string $secretAccessKey * @return void */ function __construct($accessKey, $secretAccessKey) { $this->snsClient = new SnsClient([ 'version' => 'latest', 'credentials' => [ 'key' => $accessKey, 'secret' => $secretAccessKey, ], 'region' => 'ap-northeast-1', ]); } /** * アプリケーションに端末を追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createplatformendpoint * * @param string $applicationArn * @param string $deviceToken * @return SnsClient */ function createPlatformEndpoint($applicationArn, $deviceToken) { return $this->snsClient->createPlatformEndpoint([ 'PlatformApplicationArn' => $applicationArn, 'Token' => $deviceToken, ]); } /** * アプリケーションに対するエンドポイントの一覧を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listendpointsbyplatformapplic... * * @param string $nextToken * @param string $applicationArn * @return SnsClient */ function listEndpointsByPlatformApplication($nextToken, $applicationArn) { return $this->snsClient->listEndpointsByPlatformApplication([ 'NextToken' => $nextToken, 'PlatformApplicationArn' => $applicationArn, ]); } /** * アプリケーションに対するエンドポイントの状態を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#getendpointattributes * * @param string $endpointArn * @return SnsClient */ function getEndpointAttributes($endpointArn) { return $this->snsClient->getEndpointAttributes([ 'EndpointArn' => $endpointArn, ]); } /** * アプリケーションに対するエンドポイントの状態を変更 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#setendpointattributes * * @param string $enabled * @param string $endpointArn * @return SnsClient */ function setEndpointAttributes($enabled, $endpointArn) { return $this->snsClient->setEndpointAttributes([ 'Attributes' => [ 'Enabled' => $enabled ], 'EndpointArn' => $endpointArn, ]); } /** * トピックを追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createtopic * * @param string $name * @return SnsClient */ function createTopic($name) { return $this->snsClient->createTopic([ 'Name' => $name, ]); } /** * トピック一覧を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listtopics * * @param string $nextToken * @return SnsClient */ function listTopics($nextToken) { return $this->snsClient->listTopics([ 'NextToken' => $nextToken, ]); } /** * トピックにエンドポイントを追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#subscribe * * @param string $endpoint * @param string $topicArn * @return SnsClient */ function subscribe($endpoint, $topicArn) { return $this->snsClient->subscribe([ 'Endpoint' => $endpoint, 'Protocol' => 'application', 'ReturnSubscriptionArn' => false, 'TopicArn' => $topicArn, ]); } /** * 指定した端末もしくはトピックに対してプッシュを送信 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#publish * * @param string $message * @param string $targetArn * @param string $topicArn * @return SnsClient */ function publish($message, $targetArn, $topicArn) { $parameter = [ 'Message' => $message, ]; if (!empty($targetArn)){ $parameter['TargetArn'] = $targetArn; } elseif (!empty($topicArn)) { $parameter['TopicArn'] = $topicArn; } return $this->snsClient->publish($parameter); } }
■pushtest1-dev\config.php ※コード上はiOSへのプッシュを考慮しているが、この時点ではiOS用のARNは無いので未設定で可
<?php /********* 設定 ***************************************************************/ $CONFIG = []; // AWS: アクセスキー $CONFIG['AWS_ACCESS_KEY'] = 'XXXXX'; $CONFIG['AWS_SECRET_ACCESS_KEY'] = 'YYYYY'; // AWS: AmazonSNS: アプリケーションARN $CONFIG['AWS_SNS_APPLICATION_ARN_GCM'] = 'arn:aws:sns:ap-northeast-1:0123456789:app/GCM/TEST1-GCM-Dev'; $CONFIG['AWS_SNS_APPLICATION_ARN_APNS'] = 'arn:aws:sns:ap-northeast-1:0123456789:app/APNS_SANDBOX/TEST1-APNS_SANDBOX-Dev'; // AWS: AmazonSNS: トピックARN $CONFIG['AWS_SNS_TOPIC_ARN_GCM'] = 'arn:aws:sns:ap-northeast-1:0123456789:TEST1-GCM-Dev-All'; $CONFIG['AWS_SNS_TOPIC_ARN_APNS'] = 'arn:aws:sns:ap-northeast-1:0123456789:TEST1-APNS_SANDBOX-Dev-All'; // PHP: AWSオートローダー require_once '/var/www/vhosts/libs/aws/aws-autoloader.php';
■pushtest1-dev\api.php
<?php require_once 'config.php'; require_once 'AwsSns.php'; /* APIを実行 */ $sns = new AwsSns($CONFIG['AWS_ACCESS_KEY'], $CONFIG['AWS_SECRET_ACCESS_KEY']); $api = isset($_POST['api']) ? $_POST['api'] : ''; $result = null; $code = null; $data = null; try { switch ($api) { case 'createPlatformEndpoint': $platform = isset($_POST['platform']) ? $_POST['platform'] : 'GCM'; $applicationArn = ($platform == 'APNS') ? $CONFIG['AWS_SNS_APPLICATION_ARN_APNS'] : $CONFIG['AWS_SNS_APPLICATION_ARN_GCM']; $result = $sns->createPlatformEndpoint($applicationArn, $_POST['deviceToken']); $code = $result->get('@metadata')['statusCode']; $data = $result['EndpointArn']; break; case 'listEndpointsByPlatformApplication': $result = $sns->listEndpointsByPlatformApplication($_POST['nextToken'], $_POST['applicationArn']); $code = $result->get('@metadata')['statusCode']; $data = $result['Endpoints']; break; case 'getEndpointAttributes': $result = $sns->getEndpointAttributes($_POST['endpointArn']); $code = $result->get('@metadata')['statusCode']; $data = $result['Attributes']; break; case 'setEndpointAttributes': $result = $sns->setEndpointAttributes($_POST['enabled'], $_POST['endpointArn']); $code = $result->get('@metadata')['statusCode']; $data = []; break; case 'createTopic': $result = $sns->createTopic($_POST['name']); $code = $result->get('@metadata')['statusCode']; $data = $result['TopicArn']; break; case 'subscribe': $result = $sns->subscribe($_POST['endpoint'], $_POST['topicArn']); $code = $result->get('@metadata')['statusCode']; $data = $result['SubscriptionArn']; break; case 'listTopics': $result = $sns->listTopics($_POST['nextToken']); $code = $result->get('@metadata')['statusCode']; $data = $result['Topics']; break; case 'publish': $result = $sns->publish($_POST['message'], $_POST['targetArn'], $_POST['topicArn']); $code = $result->get('@metadata')['statusCode']; $data = $result['MessageId']; break; default: break; } } catch (Exception $e) { $result = $e->getMessage(); } /* 結果を表示 */ print_r(['code' => $code, 'data' => $data, 'result' => $result]);
■pushtest1-dev\index.php
<?php require_once 'config.php'; require_once 'AwsSns.php'; ?> <html> <head> <meta charset="utf-8"> <title>AwsSns</title> </head> <body> <h1>AwsSns</h1> <hr> <?php if ($_SERVER['REQUEST_METHOD'] === 'POST') : ?> <h2>Result</h2> <pre><?php require_once 'api.php' ?></pre> <hr> <?php endif ?> <h2>CreatePlatformEndpoint</h2> <p>アプリケーションに端末を追加。</p> <form action="" method="post"> <input type="hidden" name="api" value="createPlatformEndpoint"> <dl> <dt>applicationArn(必須)</dt> <dd><input type="text" size="60" name="applicationArn"></dd> <dt>deviceToken(必須)</dt> <dd><input type="text" size="60" name="deviceToken"></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>ListEndpointsByPlatformApplication</h2> <p>アプリケーションに対するエンドポイントの一覧を取得。(1回のリクエストで100件まで。1秒間に30トランザクションまで。)</p> <form action="" method="post"> <input type="hidden" name="api" value="listEndpointsByPlatformApplication"> <dl> <dt>nextToken</dt> <dd><input type="text" size="60" name="nextToken" value=""></dd> <dt>applicationArn(必須)</dt> <dd><input type="text" size="60" name="applicationArn" value=""></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>GetEndpointAttributes</h2> <p>アプリケーションに対するエンドポイントの状態を取得。</p> <form action="" method="post"> <input type="hidden" name="api" value="getEndpointAttributes"> <dl> <dt>endpointArn(必須)</dt> <dd><input type="text" size="60" name="endpointArn"></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>SetEndpointAttributes</h2> <p>アプリケーションに対するエンドポイントの状態を変更。</p> <form action="" method="post"> <input type="hidden" name="api" value="setEndpointAttributes"> <dl> <dt>enabled(必須)</dt> <dd> <select name="enabled"> <option value=""></option> <option value="true">true</option> <option value="false">false</option> </select> </dd> <dt>endpointArn(必須)</dt> <dd><input type="text" size="60" name="endpointArn"></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>CreateTopic</h2> <p>トピックを追加。</p> <form action="" method="post"> <input type="hidden" name="api" value="createTopic"> <dl> <dt>name(必須)</dt> <dd><input type="text" size="60" name="name"></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>Subscribe</h2> <p>トピックにエンドポイントを追加。</p> <form action="" method="post"> <input type="hidden" name="api" value="subscribe"> <dl> <dt>endpoint(必須)</dt> <dd><input type="text" size="60" name="endpoint" value=""></dd> <dt>topicArn(必須)</dt> <dd><input type="text" size="60" name="topicArn" value=""></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>ListTopics</h2> <p>トピック一覧を取得。(1回のリクエストで100件まで。1秒間に30トランザクションまで。)</p> <form action="" method="post"> <input type="hidden" name="api" value="listTopics"> <dl> <dt>nextToken</dt> <dd><input type="text" size="60" name="nextToken" value=""></dd> </dl> <p><button type="submit">実行</button></p> </form> <hr> <h2>Publish</h2> <p>指定した端末もしくはトピックに対してプッシュを送信。</p> <form action="" method="post"> <input type="hidden" name="api" value="publish"> <dl> <dt>message(必須)</dt> <dd><input type="text" size="60" name="message" value=""></dd> <dt>targetArn</dt> <dd><input type="text" size="60" name="targetArn" value=""></dd> <dt>topicArn</dt> <dd><input type="text" size="60" name="topicArn" value=""></dd> </dl> <p><button type="submit">実行</button></p> </form> </body> </html>
■補足 AwsSns.php の publish() を以下のように変更すると、単純なメッセージ以外も送信できるみたい ただしAndroidではプッシュの本文が表示されなくなるので、Androidアプリ側で受け取り処理の調整が必要になるかも(検証中)
function publish($message, $targetArn, $topicArn) { $gcm = json_encode([ 'data' => [ 'message' => $message, 'param1' => 'xxx', 'param2' => 'yyy' ], ]); $apns = json_encode([ 'aps' => [ //'alert' => $message, 'alert' => [ //'title' => 'タイトル', //'subtitle' => 'サブタイトル', 'body' => $message, ], 'badge' => 0, 'sound' => 'default' ], 'param1' => 'xxx', 'param2' => 'yyy' ]); $message = [ 'default' => $message, 'GCM' => $gcm, 'APNS' => $apns, 'APNS_SANDBOX' => $apns, ]; $parameter = [ 'Message' => json_encode($message), 'MessageStructure' => 'json', ]; if (!empty($targetArn)){ $parameter['TargetArn'] = $targetArn; } elseif (!empty($topicArn)) { $parameter['TopicArn'] = $topicArn; } return $this->snsClient->publish($parameter); /* $parameter = [ 'Message' => $message, ]; if (!empty($targetArn)){ $parameter['TargetArn'] = $targetArn; } elseif (!empty($topicArn)) { $parameter['TopicArn'] = $topicArn; } return $this->snsClient->publish($parameter); */ }
Androidアプリ側では MyFirebaseMessagingService.kt の以下の部分の調整で取得できるかも(検証中)
override fun onMessageReceived(remoteMessage: RemoteMessage?) { Log.d(TAG, "From: " + remoteMessage!!.from!!) // Check if message contains a data payload. if (remoteMessage.getData().isNotEmpty()) { Log.d(TAG, "Message data payload: " + remoteMessage.getData().get("default")) // 10秒以上処理にかかる場合は、Firebase Job Dispatcherを使用する sendNotification(this, remoteMessage.getData().get("default")) } // Check if message contains a notification payload. if (remoteMessage.notification != null) { Log.d(TAG, "Message Notification Body: " + remoteMessage.notification!!.body!!) sendNotification(this, remoteMessage.notification!!.body!!) } }
以下のようにdefaultをmessageに変更すると、メッセージを受け取ることができた プッシュ一覧でもテキストが表示された
if (remoteMessage.getData().isNotEmpty()) { Log.d(TAG, "Message data payload1: " + remoteMessage.getData().toString()) Log.d(TAG, "Message data payload2: " + remoteMessage.getData().get("message")) // 10秒以上処理にかかる場合は、Firebase Job Dispatcherを使用する sendNotification(this, remoteMessage.getData().get("message")) }
■動作確認
■端末ごとに送信 AmazonSNS → アプリケーション → 作成したアプリケーションのARNをクリック この画面で、エンドポイントがカラであることを確認する アプリを起動する。エミュレータでも実機でも可 アプリ内で「LOG TOKEN」をクリックすると、デバイストークンが画面に表示される さらに「POST TOKEN」をクリックすると、デバイストークンがサーバに送られる (初回起動時は自動で上記の処理を行っている) AmazonSNS → アプリケーション → 作成したアプリケーションのARNをクリック この画面で、エンドポイントが登録されていることを確認する http://192.168.33.10/code/pushtest1-dev/ 「Publish」でプッシュを送る targetArnはエンドポイントARNを入力する message: 任意のメッセージ(日本語可)を入力 targetArn: arn:aws:sns:ap-northeast-1:0123456789:endpoint/GCM/Push-Test1-Dev/aee4dc65-cf96-3a60-918e-a4707b7e9339 無効になったエンドポイントに送信した場合、以下のエラーが表示される これをもとに無効な送信先を削除したりはできそう これとは別に、アプリを起動するたびにデバイストークンの更新を行うと良さそう Error executing "Publish" on "https://sns.ap-northeast-1.amazonaws.com"; AWS HTTP error: Client error: `POST https://sns.ap-northeast-1.amazonaws.com` resulted in a `400 Bad Request` response: <ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"> <Error> <Type>Sender</Type> <Code>EndpointDis (truncated...) EndpointDisabled (client): Endpoint is disabled - <ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"> <Error> <Type>Sender</Type> <Code>EndpointDisabled</Code> <Message>Endpoint is disabled</Message> </Error> <RequestId>2175f167-ac6a-55fe-9458-a5405b37d703</RequestId> </ErrorResponse> ■トピックごとに送信 AmazonSNS → トピック → 作成したトピックのARNをクリック この画面で、エンドポイントがカラであることを確認する http://192.168.33.10/code/pushtest1-dev/ 「Subscribe」でトピックに端末を追加する endpointはエンドポイントARNを入力する topicArnはトピックARNを入力する endpoint: arn:aws:sns:ap-northeast-1:0123456789:endpoint/GCM/Push-Test1/aee4dc65-cf96-3a60-918e-a4707b7e9339 protocol: application topicArn: arn:aws:sns:ap-northeast-1:0123456789:Push-Test1-Topic1 http://192.168.33.10/code/pushtest1-dev/ 「Publish」でプッシュを送る topicArnはトピックARNを入力する message: 任意のメッセージ(日本語可)を入力 targetArn: arn:aws:sns:ap-northeast-1:0123456789:Push-Test1-Dev-Topic1 トピック内に無効なエンドポイントが含まれていても、特にエラーなどは返してくれないみたい? 要検証 ■AmazonSNSから送信(デバッグ用) AmazonSNS → アプリケーション → 作成したアプリケーションのARNをクリック 送信先にチェックを入れて「エンドポイントの発行」をクリック メッセージ形式: Row メッセージ: AmazonSNSから直接送信 「メッセージの発行」をクリックすると送信できる ■Firebaseから送信(デバッグ用) プロジェクトのページを開く 左メニュー「拡大 → Cloud Messaging → Send your first message(最初のメッセージを送信)」をクリック ※2回目以降に送信するときは「新しい通知」をクリック 通知テキスト: これはテストです。 ターゲット: ユーザーセグメント ターゲットとするユーザー: アプリ net.refirio.pushtest1 その他必要に応じて設定し、「確認」をクリックすると確認画面が表示されるので、「送信」をクリックすると送信できる
■iOSアプリの作成
■iOSアプリの作成 ※いったんプッシュ機能の無い状態でアプリを作る ※Organization Identifier を考慮して、いったん「pushtest1」のような Product Name で作成してIDを決めさせ、後から名前を変更する Xcodeでプロジェクトを作成する Create a new Xcode project ↓ iOS Application Single View App 「Next」 ↓ Product Name: pushtest1 Team: TERRAPORT CO.,LTD. (Enterprise) Organization Name: テラポート Organization Identifier: jp.terraport Language: Swift 「Next」 ↓ プロジェクトの作成場所を選択 「Create」 Bundle Identifier はプロジェクトのGeneralを確認すると「jp.terraport.pushtest1」になっていた エミュレータと実機で、アプリを起動できるかテストする ■Apple Developer Programへのログイン ブラウザで以下にログインできることを確認しておく Apple Developer Program https://developer.apple.com/jp/programs/ apple@terraport.jp 右上のアカウント名が表示されている部分をクリックし、「Enterprise - TERRAPORT CO.,LTD.」に切り替える ■Push通知の使用を設定 XcodeのプロジェクトのCapabilitiesで「Push Notifications」をONにする ブラウザから Apple Developer Program の Certificates, IDs & Profiles → Identifiers → App IDs → XC jp terraport pushtest1 (jp.terraport.pushtest1) で確認すると、「Push Notifications」が「Configurable」になっている なお、Xcodeから登録されたAppIDは、名前に「XC」のプレフィックスが付く(IDではなく名前なので、後から編集すればいいみたい) ■開発者証明書を作成 Macでキーチェーンアクセスを起動 メニューから キーチェーンアクセス → 証明書アシスタント → 認証局に証明書を要求... を実行 ユーザのメールアドレス: refirio@example.com 通称: refirio (日本語を含めると、AmazonSNSに登録できないので注意) CAのメールアドレス: (空欄) 要求の処理: 「ディスクに保存」「鍵ペア情報を指定」にチェックを入れる 「続ける」 ↓ 保存場所を指定する 「保存」 ↓ 鍵のサイズ: 2048ビット アルゴリズム: RSA デフォルトで上記設定のはずなので「続ける」 ↓ 証明書要求がディスク上に作成されました 「完了」 デフォルトでは CertificateSigningRequest.certSigningRequest というファイル名で作成される これでCSRの作成は完了。引き続きCSRをAppleに登録する ブラウザから Apple Developer Program の Certificates, IDs & Profiles → Identifiers → App IDs → XC jp terraport pushtest1 (jp.terraport.pushtest1) → Edit Push Notifications にある「Development SSL Certificate」の「Create Certificate」をクリック (いったん開発用だけでいい。本番用やアドホック用の場合は「Production SSL Certificate」の「Create Certificate」をクリック) ↓ About Creating a Certificate Signing Request (CSR) CSRの作成について説明が表示される 「Continue」 ↓ Generate your certificate. 作成したCSRファイルを選択する 「Continue」 ↓ Your certificate is ready. 「Download」をクリックして証明書をダウンロードする(aps_development.cer) ■p12ファイルを作成 Macで証明書 aps_development.cer をダブルクリックして、キーチェーンに登録する (登録すると、キーチェーンアクセスの一覧に表示される) ↓ 登録したファイルを選択して「ファイル → 書き出す」をクリック(「分類」を「自分の証明書」にした状態で探す。Finderで検索してから書き出そうとすると、p12を選択できないので注意) デフォルトで「証明書」というファイル名になるが、日本語ファイル名だとAWSへの登録に失敗するので変更する ここでは「Push-Test1-Dev」として保存する(開発版用。本番用なら「Push-Test1」などとする) 「書き出した項目を保護するために使用されるパスワード」の入力画面になるが、パスワードはカラのまま「OK」をクリック 「キーチェーンアクセスは、キーチェーンのキー○○を書き出そうとしています。」の入力画面になるが、Macのログインパスワードを入力して「許可」をクリック ■AmazonSNSの設定 AWSのコンソールから「Simple Notification Service」を開く 左メニューから「アプリケーション」を開く 画面上部の「プラットフォームアプリケーションの作成」をクリック アプリケーション名: Push-Test1-APNS-Dev(開発版想定。本番なら「Push-Test1-APNS」などとする。Android用にも作る可能性があるので、単に「Push-Test1」ではなく「APNS」の文字を含めておくのが無難そう) プッシュ通知プラットフォーム: Apple Development (開発用の場合。本番用なら「Apple Production」にする) プッシュ証明書タイプ: iOSプッシュ証明書 P12ファイルの選択: Push-Test1-Dev.p12(先の手順で作成したファイル) パスワードの入力: (p12ファイル作成時、パスワードをカラで作成したなら空欄) 「認証情報をファイルから読み込み」をクリック 証明書の情報が表示されたことを確認して「プラットフォームアプリケーションの作成」をクリック ※「認証情報をファイルから読み込み」をクリックしたとき 「Apple の認証情報をファイルから読み込んでいるときにエラーが発生しました」 というエラーになる場合、p12のファイル名に日本語を含めていないか、証明書作成時に通称に日本語を含めていないか、などを確認する その他、原則として半角英数字に統一しておくほうが無難 ※Apple Developer Program での作業は、MacのSafariで行うことが推奨されているみたい どうしても意図した操作ができなければ、MacのSafariで試す ※証明書が壊れていないかなどは、以下のコマンドで確認できる パスワード入力後、ファイルの情報が表示されることを確認する $ openssl pkcs12 -in Push-Test1-Dev.p12 -info -noout Enter Import Password: MAC Iteration 1 MAC verified OK PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048 Certificate bag PKCS7 Data Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048 OpenSSL https://sehermitage.web.fc2.com/crypto/openssl.html PKCS #12 個人情報交換ファイルフォーマットについて - Qiita https://qiita.com/kunichiko/items/3e2ec27928a95630a73a APNsで使うp12形式の証明書、秘密鍵からpem形式の証明書、公開鍵を作成する方法がかなりわかりづらいのでまとめてみました - lineocean.com https://lineocean.com/2017/10/31/499/ ■AmazonSNSトピックの作成 ※Androidと同じ 左メニューから「トピック」を開く 「新しいトピックの作成」ボタンを押すとトピックの画面が開くので トピック名: Push-Test1-APNS-Dev-Topic1 表示名: (SMS用なので空欄) 「トピックの作成」をクリック ■iOSアプリにPush機能を実装 Capabilities > Background Modes をONにし、Remote notification にチェックを入れる http://unity-yuji.xyz/youve-implemented-applicationdidreceiveremotenotification-remote-notification-... ストーリーボードにパーツを配置
Push ... Button Device Token ... Lavel [ ] ... Text Field(一行入力欄) Log ... Lavel [ ] ... Text View(複数行入力欄 / 初期状態は空欄にしておく)
ViewController.swift
// // ViewController.swift // Push Test1 // // Created by テラポート on 2018/10/03. // Copyright (C) 2018年 テラポート. All rights reserved. // import UIKit class ViewController: UIViewController { @IBOutlet weak var deviceTokenTextField: UITextField! // 一行入力欄とOutlet接続させる @IBOutlet weak var logTextView: UITextView! // 複数行行入力欄とOutlet接続させる var log: String = "" let ap = UIApplication.shared.delegate as! AppDelegate override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. ap.mainView = self } func outputLog(_ string: String) { print(string) DispatchQueue.main.async { self.logTextView.text.append(string) self.logTextView.text.append("\n") } } @IBAction func touchUpInside(_ sender: UIButton) { let deviceToken = ap.deviceToken if deviceToken == "" { self.outputLog("There is no Device Token") } else { ap.sendRegistrationToServer(deviceToken) } } }
AppDelegate.swift
// // AppDelegate.swift // Push Test1 // // Created by テラポート on 2018/10/03. // Copyright (C) 2018年 テラポート. All rights reserved. // import UIKit import UserNotifications // UserNotificationsを読み込み @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var mainView : ViewController! var deviceToken: String = "" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. registerForPushNotifications() return true } /* 〜中略〜 */ // プッシュ通知の受信の登録が成功したときに呼ばれる func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenParts = deviceToken.map { data -> String in return String(format: "%02.2hhx", data) } let token = tokenParts.joined() print("Device Token: \(token)") self.deviceToken = token self.sendRegistrationToServer(token) self.mainView.deviceTokenTextField.text = token } // プッシュ通知の受信の登録が失敗したときに呼ばれる(ユーザーが「いいえ」した場合など) func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Failed to register for remote notifications with error: \(error)") } // アプリ起動中に通知を受信する処理 func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("Received message") self.mainView.outputLog("Message Received") let key: Array! = Array(userInfo.keys) if key != nil { for i in 0..<key.count { let key0 = key[i] as! String let value0 = userInfo["\(key0)"] if let unwrapValue = value0 { self.mainView.outputLog("Key: \(key0)") if key0 == "aps" { let apsUnwrapValue = String(describing: unwrapValue) self.mainView.outputLog(apsUnwrapValue) } else { self.mainView.outputLog(unwrapValue as! String) } } } } } // プッシュ通知の許可を得る処理(didFinishLaunchingWithOptionsでcall) func registerForPushNotifications() { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound,.badge]) { (Bool, Error) in print("Permission granted: \(Bool)") //guard Bool else {return} DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } // PHP経由でデバイストークンをAWS SNSへ登録する func sendRegistrationToServer(_ deviceToken: String) { var components: URLComponents = URLComponents() components.queryItems = [] components.queryItems?.append(URLQueryItem(name: "api", value: "createPlatformEndpoint")) components.queryItems?.append(URLQueryItem(name: "deviceToken", value: deviceToken)) components.queryItems?.append(URLQueryItem(name: "platform", value: "APNS")) let bodyString = components.string ?? "" let bodyStringWithoutQuestion = bodyString[bodyString.index(bodyString.startIndex, offsetBy: 1)...] let url = URL(string: "http://192.168.1.14:8080/code/pushtest1-dev/api.php") var request:URLRequest = URLRequest(url: url!) request.httpMethod = "POST" request.httpBody = bodyStringWithoutQuestion.data(using: .utf8) self.mainView.outputLog("SendToken...") let session = URLSession.shared session.dataTask(with: request) { (data, response, error) in if error == nil, let data = data, let response = response as? HTTPURLResponse { print("statusCode: \(response.statusCode)") let resultString = String(data: data, encoding: .utf8) ?? "" self.mainView.outputLog(resultString) } }.resume() } }
複数行のテキスト入力欄はUITextView(Xcode上はTextViewかも?)で作成する https://teratail.com/questions/135574 ヒモ付を間違えたときのエラー。変数名を変えたときなどは紐付けなおしが必要みたい http://ios.steppers-hi.net/?eid=24 plistは不要だった CocoaPodsのインストールは不要だった ■PHPプログラムの作成 以下の部分に、AmazonSNSで取得したApple用のARNを設定する aws_sns.php
const AWS_SNS_PLATFORM_APPLICATION_ARN_APNS = ''; ↓ const AWS_SNS_PLATFORM_APPLICATION_ARN_APNS = 'arn:aws:sns:ap-northeast-1:0123456789:app/APNS_SANDBOX/Push-Test2-APNS-Dev';
■動作確認 アプリを実行する。エミュレータではプッシュを受け取れないので、実機で実行する 実行すると「"pushtest2"は通知を送信します。よろしいですか?」のダイアログが表示されるので許可する http://192.168.33.10/code/pushtest1-dev/ 「Publish」でプッシュを送る(Androidと同じ流れ) targetArnはエンドポイントARNを入力する トピックへの一斉配信もAndroidと同じ流れで行える アプリを開いていると、Pushは届いているが画面では確認できない(ログに表示するようにはしてくれている) あとはこれをダイアログに表示するか、などを考える ボタンを押したときの処理の書き方は、このままでいいかは考えたい。ボタンが増えたときにややこしくならないか と思ったけど、今は「IBAction func touchUpInside」をボタンと紐付けていないだけかも むしろ今が通常の書き方か。要確認 Push送信画面 http://192.168.1.14:8080/code/pushtest1-dev/ ■Pusherから送信(デバッグ用) MacにPusherをインストールすれば、そこからプッシュを送ることができる https://github.com/noodlewerk/NWPusher/releases インストールして起動すると証明書の選択があるが、キーチェーンアクセスに登録してあるPush通知の証明書が表示される 選択して「Reconnect」をクリックする。パスワードを求められたら、Macのログインパスワードを入力する その下に送信先を入力する ここには「エンドポイントARN」ではなく、AWSコンソールに並んで表示されている「トークン」を入力する 具体的には以下のような値 7a9346db02e6e2ee2af801369bde9624f5cc6072959b988ab435ab0123456789 一番下のテキストエリアに、送信したい内容を入力する 以下のようにJSON形式で入力できる {"aps":{"alert":"テスト1","badge":1,"sound":"default"}} 「Push」ボタンを押すとプッシュが送信される
■トラブル
■プッシュが届かない 以下を確認する ・アプリのパッケージ名を確認し、それに対応する設定が行われているか ・Firebase、Apple Developer Program、AmazonSNS でそれぞれ設定内容が正しいか Androidの場合は以下も確認する ・USBデバッグでインストールしたか、APKを書き出してインストールしたか iOSの場合は以下も確認する ・Distribution証明書とDevelopment証明書、適切な方を使用しているか ・アカウントの有効期限切れ、証明書の有効期限切れになっていないか ・「Development SSL Certificate」で設定をしたのか「Production SSL Certificate」で設定したのか
■AmazonSNSを使わずにPHPでプッシュを送信(Android)
■Firebase Admin SDKのインストール Add the Firebase Admin SDK to Your Server https://firebase.google.com/docs/admin/setup 公式サイトにはPHPはないので、以下を使用する(将来的に公式なSDKになると思われる) https://github.com/kreait/firebase-php/ ただし、PHP7以上のみ対応 公式ドキュメント https://firebase-php.readthedocs.io/en/latest/ SDKのインストール(依存関係が多いのでcomposerでインストールする) $ composer require kreait/firebase-php ■秘密鍵の入手 Firebaseのプロジェクトの設定ページで対象のアプリをクリックし、表示された歯車アイコンをクリック サービスアカウント → 新しい秘密鍵の生成 → キーを生成 push-test-54dbc-firebase-adminsdk-o0ylo-94efb9dda1.json のような名前のファイルがダウンロードされる このファイルとデバイストークンを使って、直接プッシュを送信できる ■プログラムの作成
<?php ini_set('display_errors', 1); require __DIR__ . '/vendor/autoload.php'; use Kreait\Firebase; use Kreait\Firebase\Messaging\CloudMessage; $serviceAccount = Firebase\ServiceAccount::fromJsonFile(__DIR__.'/push-test-54dbc-firebase-adminsdk-o0ylo-0123456789.json'); $firebase = (new Firebase\Factory) ->withServiceAccount($serviceAccount) ->create(); $messaging = $firebase->getMessaging(); $deviceToken = 'dNqYZhjr8j8:APA91bEsLF0IQlhWljxtiKGwLia8ANDeu1s8tFKQZC5Fr2nxskCKBfOtNeuN2q0wVCLCf5t6Ho3OZA9_OCXaxW-HPnEo_fkhtlwClJGYItI7-yFN6m_ABCDEFGHIJKLMN-YeCBX'; $message = CloudMessage::fromArray([ 'token' => $deviceToken, 'data' => ['message' => 'TEST'], 'notification' => [ 'title' => 'テスト', 'body' => 'これはテストです。', ], ]); $messaging->send($message); print('sent push');
■AmazonSNSを使わずにPHPでプッシュを送信(iOS)
[iPhone] APNs プッシュ通知、リモート通知の取得 https://i-app-tec.com/ios/apns-remote.html ■ApnsPHPのインストール ApnsPHP https://github.com/immobiliare/ApnsPHP 証明書類(pem形式)が2つ必要 ・PUSH証明書(と鍵)を含むpemファイル ・APNsのサーバを検証するためのルート証明書 ■サーバルート証明書の入手 Entrustのサーバルート証明書は以下からダウンロードする (なお、今回ダウンロードした証明書は、7/24/2029まで有効) Entrust.net Certificate Authority (2048) https://www.entrustdatacard.com/pages/root-certificates-download https://entrust.com/root-certificates/entrust_2048_ca.cer ■pemファイルの作成 opensslコマンドの使える環境で、作成した Push-Test2-Dev.p12 をpem形式に変換する パスワードの入力を求められるが、p12ファイルをパスワードなしで作成した場合、パスワードは空欄のままEnterでいい $ openssl pkcs12 -in Push-Test2-Dev.p12 -out server_certificates_bundle_sandbox.pem -nodes -clcerts Enter Import Password: MAC verified OK このファイルとデバイストークンを使って、直接プッシュを送信できる ■プログラムの作成
<?php /** * @file * sample_push.php * * Push demo * * LICENSE * * This source file is subject to the new BSD license that is bundled * with this package in the file LICENSE.txt. * It is also available through the world-wide-web at this URL: * http://code.google.com/p/apns-php/wiki/License * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to aldo.armiento@gmail.com so we can send you a copy immediately. * * @author (C) 2010 Aldo Armiento (aldo.armiento@gmail.com) * @version $Id$ */ // Adjust to your timezone date_default_timezone_set('Europe/Rome'); // Report all PHP errors error_reporting(-1); // Using Autoload all classes are loaded on-demand require_once 'ApnsPHP/Autoload.php'; // Instantiate a new ApnsPHP_Push object $push = new ApnsPHP_Push( ApnsPHP_Abstract::ENVIRONMENT_SANDBOX, 'server_certificates_bundle_sandbox.pem' ); // Set the Provider Certificate passphrase // $push->setProviderCertificatePassphrase('test'); // Set the Root Certificate Autority to verify the Apple remote peer //$push->setRootCertificationAuthority('entrust_root_certification_authority.pem'); $push->setRootCertificationAuthority('entrust_2048_ca.cer'); // Connect to the Apple Push Notification Service $push->connect(); // Instantiate a new Message with a single recipient $message = new ApnsPHP_Message('7a9346db02e6e2ee2af801369bde9624f5cc6072959b988ab435ab0123456789'); // Set a custom identifier. To get back this identifier use the getCustomIdentifier() method // over a ApnsPHP_Message object retrieved with the getErrors() message. $message->setCustomIdentifier("Message-Badge-3"); // Set badge icon to "3" $message->setBadge(3); // Set a simple welcome text $message->setText('プッシュ送信テスト!'); // Play the default sound $message->setSound(); // Set a custom property $message->setCustomProperty('acme2', array('bang', 'whiz')); // Set another custom property $message->setCustomProperty('acme3', array('bing', 'bong')); // Set the expiry value to 30 seconds $message->setExpiry(30); // Add the message to the message queue $push->add($message); // Send all messages in the message queue $push->send(); // Disconnect from the Apple Push Notification Service $push->disconnect(); // Examine the error message container $aErrorQueue = $push->getErrors(); if (!empty($aErrorQueue)) { var_dump($aErrorQueue); }
■補足 ※未検証 ApnsPHPでは、「旧式のバイナリインターフェイス」を利用している 【ドキュメント】バイナリProvider API https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Bin... Binary Provider API https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotifi... 証明書入手先 Entrust.net Certificate Authority (2048) https://www.entrustdatacard.com/pages/root-certificates-download →2018/10/16時点で拡張子が、cerだが、中身を確認したところテキスト形式であったため変換不要 接続先サーバ gateway.push.apple.com, port 2195; gateway.sandbox.push.apple.com, port 2195 将来的には、GeoTrust Global CAルート証明書を使用すべき 【ドキュメント】APNsの概要 > プロバイダ−APNs間の接続信頼 https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APN... APNs Overview > Provider-to-APNs Connection Trust https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotifi... 【ドキュメント】APNsとのやり取り https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Com... Communicating with APNs https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotifi... 証明書入手先 https://www.geotrust.com/resources/root-certificates/ https://www.geotrust.com/resources/root_certificates/certificates/GeoTrust_Global_CA.pem 接続先サーバ Development server: api.development.push.apple.com:443 Production server: api.push.apple.com:443 2197のポートも使用可能 ApnsPHPでは、Feedback.php,Push.phpの2ファイルで接続先が定義されている なお、証明書とURLを変更しただけでは、接続できなかった。
■トークン管理
プッシュ送信先となるトークンは、ときどき変更されることがあるので注意 最新の登録情報を維持し続けるために、以下のような手段がある Amazon SNS のモバイルトークン管理についてのベストプラクティス | Developers.IO https://dev.classmethod.jp/cloud/aws/sns-mobile-token/ [PHP]Amazon SNS を使い、iOS・AndroidへPUSH通知 - SNS設定編 - - Qiita https://qiita.com/kei_ohsaki/items/723595671767fcae1ec1 [PHP]Amazon SNS を使い、iOS・AndroidへPUSH通知 - デバイストークン登録・更新編 - - Qiita https://qiita.com/kei_ohsaki/items/257e4f42224dd89e15ce [PHP]Amazon SNS を使い、iOS・AndroidへPUSH通知 - トピック作成・配信編 - - Qiita https://qiita.com/kei_ohsaki/items/564e75b346e1495a33e7 Amazon SNS モバイルプッシュ API の使用 - Amazon Simple Notification Service https://docs.aws.amazon.com/ja_jp/sns/latest/dg/mobile-push-api.html ■APIの取得件数制限 「listEndpointsByPlatformApplication」の場合、以下の注意点がある
Lists the endpoints and endpoint attributes for devices in a supported push notification service, such as GCM and APNS. The results for ListEndpointsByPlatformApplication are paginated and return a limited list of endpoints, up to 100. If additional records are available after the first page results, then a NextToken string will be returned. To receive the next page, you call ListEndpointsByPlatformApplication again using the NextToken string received from the previous call. When there are no more records to return, NextToken will be null. For more information, see Using Amazon SNS Mobile Push Notifications. This action is throttled at 30 transactions per second (TPS).
1回のリクエストで100件まで 1秒間に30トランザクションまで 一度に3000件以上取る場合は工夫が必要かも トークン管理のために 「無効になったトークンをすべて取得して何らかの処理をする」 とするよりも、上のリンク先で紹介されているように 「アプリ起動時に毎回トークンの有効/無効を確認する。無効ならトークンを差し替える」 とする方がいいかも
■Firebaseから本番用と開発用のそれぞれにプッシュを送信
■前提 例えば先に本番用に作成し、その後単純にAndroidStudioで .dev にして実行すると 「No matching client found for package name」と表示されてインストールできない Firebaseをandroidアプリに追加したときにつまずいたポイント - noyのブログ http://noy.hatenablog.jp/entry/2018/02/15/121431 google-services.json の「package_name」に「.dev」を付けると .dev でインストールできる が、google-services.jsonは本番用なのでプッシュは届かないみたい もちろん「.dev」を削除して 「.dev」 なしでインストールするとプッシュは届く Firebaseプロジェクトを開発版用に用意する必要がある ■本番用と開発用に分ける手順 まずは本番&開発などを一緒に管理するためのプロジェクトを作成しておく 通常の手順で、Firebase本番想定のアプリを作成しておく Android パッケージ名: jp.terraport.zabbix.dev アプリのニックネーム: Zabbix Dev Firebaseで開発用に、プロジェクトではなくアプリを追加する Android パッケージ名: jp.terraport.zabbix.dev アプリのニックネーム: Zabbix Dev ダウンロードした google-services.json の内容を確認すると、本番用と開発用の記述両方があった Firebase SDK の追加作業は済んでいるので飛ばす その状態で developDebug 版に切り替えてみるとエラーは出なかった アプリを実行してインストールを確認すると「正常に追加されました」が表示された Firebaseのコンソールからサーバキーを確認すると本番と開発で共通になっていたので、この値を変更する必要はない アプリ内でのPHPの通信先調整(本番と開発)は必要 これで本番と開発版を端末に共存させたうえで、プッシュもそれぞれに送信できる 1つのソースコードでPHPへの送信先を分ける必要があるので、例えば以下のようにして送信先を振り分ける 実際はこの手の記述は、設定ファイルなどにまとめて記述すると良さそう
var target = "" if ("production".equals(BuildConfig.FLAVOR)) { target = "https://php7.terraport-dev.com/tool/zabbix/prod/api.php" } else { target = "https://php7.terraport-dev.com/tool/zabbix/dev/api.php" } Log.d("TARGET", target)
同じ手順で jp.terraport.zabbix jp.terraport.zabbix.dev jp.terraport.zabbix.dev.debu などを1端末に共存させて、それぞれでプッシュを受け取ることも可能なはず 以下の方法は採用していないが、参考までにメモ Firebaseプロジェクトを開発環境用と本番環境用にシンプルに分ける方法 - Qiita https://qiita.com/hinom77/items/9cd6818210a52f86a6a3 Flavor毎に異なったgoogle-services.jsonをつかう - Qiita https://qiita.com/gyamoto/items/39351917ee6755abf7bf
■Apple Developer Program から本番用と開発用のそれぞれにプッシュを送信
検証中 BundleIDを以下のように設定してみた Develop_Debug | jp.terraport.buildtest1.dev Develop_Release | jp.terraport.buildtest1.dev Production_Release | jp.terraport.buildtest1 アプリケーション名を以下のように設定してみた Develop_Debug | buildtest1 Dev Develop_Release | buildtest1 Dev Production_Release | buildtest1 開発時は jp.terraport.zabbix.dev が実機にインストールされるので、この「Development SSL Certificate」に対して登録してみる Enterpriseで社内向けにデバッグ版を配布するときに、改めて「Production SSL Certificate」に登録するか 証明書はいったん、本番用に作ったものを共用としてみる AmazonSNSで本番用の実機書き出し用として Zabbix-APNS-Dev を作ったが Zabbix-APNS-Development とした方が良かったかも その上で、開発用の実機書き出し用として Zabbix-APNS-Dev-Development を作るか それなら、Android版も同じルールにしておく方が無難か でもAndroidでは「Development SSL Certificate」と「Production SSL Certificate」のような区別は無いので、合わせると冗長か と思ったけど、AmazonSNSでは「Apple iOS Prod」と「Apple iOS Dev」が異なれば同じ名前を付けられる それなら 本番 ... Zabbix-APNS | Apple iOS Prod 本番 ... Zabbix-APNS_SANDBOX | Apple iOS Dev 開発 ... Zabbix-APNS-Dev | Apple iOS Prod 開発 ... Zabbix-APNS_SANDBOX-Dev | Apple iOS Dev 本番トピック ... Zabbix-APNS-All | Apple iOS Prod 本番トピック ... Zabbix-APNS_SANDBOX-All | Apple iOS Dev 開発トピック ... Zabbix-APNS-Dev-All | Apple iOS Prod 開発トピック ... Zabbix-APNS_SANDBOX-Dev-All | Apple iOS Dev とする方がいいか でもトピックではこのような区別がないので名前で区別するしかないか でもPHPプログラムのURLをどうするか また「Zabbix-APNS | Apple iOS Dev」などは使うことが無いか
■プッシュ通知の受診時にダイアログを表示する(Android)
Firebaseで送信する際に「Android通知ちゃんねる」「優先度」「通知音」などがある これらを変更して対応するのかも と思ったが、もともと「優先度」は「高」なので特に変化は無い
■プッシュ通知のタップ時に任意の画面を開く
プッシュ通知からアプリ内の特定のviewを開く(iOS) - Growthbeat FAQ https://faq.growthbeat.com/article/88-view-ios [iOS 10] 画面上部または通知センター上に表示された通知がタップされたときの処理を実装する | DevelopersIO https://dev.classmethod.jp/smartphone/iphone/wwdc-2016-user-notifications-7/
■アプリからプッシュ通知の設定画面を開く
プッシュ通知設定画面へ遷移させる為の実装 - Qiita https://qiita.com/yamataku29/items/5361d7c3146604dcca44
■APNSの証明書を更新
AWS SNSのiOS向けPush通知(APNs)証明書の更新手順 - Qiita https://qiita.com/b_a_a_d_o/items/e3bf9cd52b6cd9252088
■その他考察メモ
■アプリのID ※アプリは本番用と開発用でパッケージ名を変える方がいいかも。1端末に両方インストールできるように ※アプリのIDは、普通に作ると Android ... net.refirio.pushtest1dev iOS ... net.refirio.Push-Test1-Dev のようになる net.refirio.pushtest1 で作成して net.refirio.pushtest1 net.refirio.pushtest1.stg net.refirio.pushtest1.dev に切り替えられるように同プロジェクト内で最初に設定しておくといい (本番と開発でソースコードが別々になるのは、大変すぎるので原則としてNG) ※切り替える仕組みはXcodeならSchemeを、AndroidStudioならFlavorを使うといい 可能ならiOSとAndroidの両方で統一したパッケージ名を使えるように調整するといい 詳細は AndroidStudio.txt と Xcode.txt の「製品用、開発用などの切り分け」を参照 以下、専用の仕組みがないか調べたときのメモ iPhoneアプリ開発でデバッグ版とリリース版をきれいに同居させる - しめ鯖日記 http://www.cl9.info/entry/2015/07/29/010020 iOS開発で環境ごとにアイコンやアプリ名、コード等を切り分けるオレオレプラクティス - Qiita https://qiita.com/KazaKago/items/2835d76ced43f913c31d 以下は Bundle ID を変更している?そういうやり方がダメというわけでは無いみたい? iPhoneアプリ開発でBundle IDを書き分けてビルドする方法 | SONICMOOV LAB https://lab.sonicmoov.com/development/iphone-app-dev/build-change-bundle-id/ Android Studio でも変更できなくは無いみたいだが、ファイルが消えることがあるとか書かれているので非推奨な方法? 【AndroidStudio】パッケージ名を変更する方法 - Qiita https://qiita.com/n-yusa/items/413c8a131ebc451e80f8 方法はあるみたい?ややこしそう? Andoridアプリの開発と本番の取扱をGradleでどうにかする - aswww log http://aswww.hatenablog.com/entry/2016/10/20/000146 ※「Push-Test1」などの名前はすべて「Push-Test1-Dev」のようにしておく方がいいかも プッシュの証明書なども開発用と本番用が必要のため ※証明書の有効期限についても調査しておく。差し替え方法 ■処理の流れ 初回ログイン時 ... 端末からPHPに、デバイストークンを投げてくる PHPがSNSから、エンドポイントを取得する PHPがDBに、ログインしているユーザID、デバイストークン、エンドポイントを記録する PHPから端末に、エンドポイントを返して端末に保存させる 次回ログイン時 ... 端末からPHPに、デバイストークンとエンドポイントを投げてくる PHPがSNSから、エンドポイントを取得する エンドポイントと新エンドポイントに違いがあれば、PHPがDBに、ログインしているユーザID、デバイストークン、エンドポイントを記録する PHPから端末に、新エンドポイントを返す。保持しているエンドポイントと違いがあれば、新エンドポイントを端末に保存させる 別端末でログイン時 ... 上とまったく同じ流れでユーザID、デバイストークン、エンドポイントを記録する devices テーブルには user_id が同じ値のデータができる セッションタイムアウト時 ... プッシュは相変わらず届き続ける アプリを立ち上げると、その時点でデバイストークンの更新処理は走る。ログイン情報(ユーザ名とパスワード)はアプリ内に保存されているものを使う ログアウト時 ... 端末からPHPに、デバイストークンを投げてくる 該当するデバイストークンを、devices テーブルから削除する ログアウトして別ユーザでログイン時 ... 「ログアウト時」の手順でログアウトし、「初回ログイン時」の手順でログインする プッシュ送信時 ... PHPがSNSを使って、エンドポイントに対してプッシュを送信する 1ユーザが複数の端末を持っていれば、それぞれにプッシュが送信される ※iOSアップデートのタイミングでデバイストークンが変わる可能性がある? エンドポイントで端末の特定を行うべきか iOS 9からAPNsデバイストークンがアプリインストールの度に変わるようになったようです - Qiita https://qiita.com/mono0926/items/9ef83c8b0de0e84118ac ■端末情報の持ち方 最初にプッシュを有効にしなかったとき、あとから有効or無効にしたとき、端末ごとの固有情報を保持するとき、などを考慮する必要がある users にユーザのログイン情報、 devices にデバイストークンなどプッシュに必要なデバイス情報 のようにした場合、端末ごとの設定はどこに持つべきか devices の情報は永続的と考えないほうがいいかも ローカルストレージに持って、それをまとめたjsonをデータベースに参考として持っておくか もともとエンドポイントはローカルストレージに持つ必要がある ソーシャルゲームの場合を例にして再度考えるといいかも…と思ったが、そういうデータは端末固有ではなくユーザ固有なのでusersになる ■プッシュ設定時の挙動 プッシュ通知設定画面へ遷移させる為の実装 - Qiita https://qiita.com/yamataku29/items/5361d7c3146604dcca44 ■プッシュ受診時の挙動 Androidでプッシュ受信時にダイアログを表示できるか ■メインスレッドで処理を実行 [Swift] MainThreadで処理を実行する - Qiita https://qiita.com/valmet/items/6de0921ca6106414228c ■現状考えている、設計のベストプラクティス ※ログインしてもしなくても使えるアプリの設計メモ users にユーザのログイン情報などを記録しておく devices にデバイスごとの情報を記録する。devices には user_id を持ち、ログインしたらデバイスとユーザを紐付ける (複数アカウントへの同時ログインが必要なら、中間テーブルを使って多対多で紐付ける) 次回からは user_id を参照してオートログイン アプリ起動直後に最低限の情報(OS名、OSバージョン、アプリバージョン、ユーザーエージェントなど)を devices にインサートする 戻り値としてアプリに代理キーを返し、その代理キーのみをアプリ側に保存しておく(代理キーだと番号を変えてアクセスされる可能性があるので、別途ランダムな文字列を発行して記録しておく方がいいかも) 原則としてこの代理キーは変更削除しない その時点でのIPアドレスも記録しておくと、何かと役に立つかも 端末ごとの設定を持ちたければ devices のレコードに保存 ユーザ毎の設定を持ちたければ users のレコードに保存(もしくは profiles や configs など、一対一となる別テーブルも有効) ユーザごとの設定を持つためにはユーザ登録が必要、とする 上記情報の保存直後に、プッシュを許可するかのダイアログを表示(初回起動時) プッシュを許可したら端末IDを devices のレコードに上書き保存。AmazonSNSからエンドポイントを取得したらそのレコードに上書き保存。一斉配信用のトピックにも追加しておく それぞれ、アプリ起動時に毎回取得し devices のレコードに上書き。 プッシュを拒否したら何もしない アプリ内の設定でプッシュを無効にしたら、devices テーブルにプッシュ送信除外の旨を記録し、その値をもとにプッシュ送信時には除外する。一斉配信用のトピックからも除外する アプリ内の設定でプッシュを有効にしたら、devices テーブルから除外の旨を削除し、一斉配信用のトピックにも再度追加する OSの機能でアプリのプッシュを無効にした場合の挙動は「受け取るけど何もしない」となっているみたい。「無効→有効」を試して問題は無かった ログアウトしたら devices の user_id はNULLにする アプリ起動のたびに devices のアプリバージョンなどは上書き更新する (プッシュを使用しない場合でも更新する。プッシュを使用する場合はさらに端末IDやエンドポイントも更新する) 以下、要検討 アプリデータを初期化して起動した場合など、同じデバイストークンがデータベースに登録される可能性はある プッシュが重複して届かないように、初回起動の新規登録時は「同じデバイストークンのデータがあれば無効にする」としておくか もしくは、そもそもデバイストークンの列はUNIQUE指定をしておくか(同じデバイストークンのデータがあればトークンをカラにするか) ログインした状態でアプリの設定をリセットしたら、サーバサイドでは「ログイン中」の情報が残ってしまう 次回初回起動の新規登録時「同じデバイストークンのデータがあれば無効にする」とする際に、ひも付きも整理するか 既読数もしくは未読数をサーバサイドで管理する必要がある? でもそれだと、トピックへの一斉配信が使えない?それに、pushtestアプリでは何もしなくても未読件数が表示されているような
■その他参考ページ
Amazon SNS で、iOS・Androidにpush通知する方法 - Qiita https://qiita.com/papettoTV/items/f45f75ce00157f87e41a phpでAWSのSNSを使ってpush通知を送るときのパターン的なお話 ~ 適当な感じでプログラミングとか! http://watanabeyu.blogspot.com/2017/01/phpawssnspush.html 【iOS】Firebase の Notifications でプッシュ通知を送る - Qiita https://qiita.com/koogawa/items/ca8cce019b4ff7ce2576 大規模ネイティブアプリへのプッシュ通知機能導入にあたって考えたこと - Qiita https://qiita.com/gomi_ningen/items/ab31aa2b3d46bb6ffa5e OreoでNotificationを表示させる方法 - Qiita https://qiita.com/naoi/items/367fc23e55292c50d459 kotlin-AndroidでHTTPで取得したデータを表示する - 動かざることバグの如し http://thr3a.hatenablog.com/entry/20180326/1521995307 AWS SNSを使ってiOSへpush通知 - Qiita https://qiita.com/ijun/items/2cbff7664e49fb93bf39 iOSのPUSH通知(APNS)の特徴・ノウハウまとめ(iOS 9まで対応) - Qiita https://qiita.com/mono0926/items/df03c61adc56934e2e7a
■サイレントプッシュ
iOSのPUSH通知(APNS)の特徴・ノウハウまとめ(iOS 9まで対応) - Qiita https://qiita.com/mono0926/items/df03c61adc56934e2e7a AWS SNSを使ってiOSへpush通知 - Qiita https://qiita.com/ijun/items/2cbff7664e49fb93bf39 Xcode: Background Modes の Modes で「Remote notifications」に加えて「Background fetch」にもチェックを入れる PHP: iOSに送るプッシュデータに「'content-available' => 1」を追加する
$apns = json_encode([ 'aps' => [ 'alert' => [ 'title' => 'タイトル', 'body' => $message, ], 'badge' => 0, 'sound' => 'default', 'content-available' => 1 ], 'param1' => 'xxx', 'param2' => 'yyy' ]);
これでプッシュ受信時にiOSアプリ側の
// アプリ起動中に通知を受信する処理 func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { }
のメソッドが呼ばれるようになるが、これだけだとプッシュ受診時にXcodeのコンソールに 「but the completion handler was never called.」 と表示される。以下のページにあるように iOS アプリでメッセージを受信する | Firebase https://firebase.google.com/docs/cloud-messaging/ios/receive?hl=ja 上記 application メソッドの最後に以下の処理を追加すると表示されなくなった
completionHandler(UIBackgroundFetchResult.newData)
これで「プッシュを受信した際に、バックグラウンドで任意の処理を実行する」ができそう このうえで、iOSに送る際に
$apns = json_encode([ 'aps' => [ /* 'alert' => [ 'title' => 'タイトル', 'body' => $message, ], */ 'badge' => 1, 'sound' => 'default', 'content-available' => 1 ], 'param1' => 'xxx', 'param2' => 'yyy' ]);
このように alert のブロックをを丸ごとコメントアウトすると、プッシュは表示されないがプッシュの受信処理は行われる さらに「'sound' => 'default',」もコメントアウトすると、プッシュ受診時のサウンドやバイブレーションも再生されない この仕組を使えば「サーバからの指示によって、裏側でこっそり任意の処理を行わせる」ができそう ただし当然ながら、プッシュが許可されていなかったりオフラインになっていたりすると実行できないので注意