■目次
概要・前提・注意点などAndroid: アプリの作成Android: Firebaseの設定Android: アプリにPush機能を実装Android: アプリにPHPからPushを送信iOS: アプリの作成iOS: 証明書の作成iOS: アプリにPush機能を実装iOS: アプリにPHPからPushを送信iOS: アプリにPHPからPushを送信(HTTP/2版)AmazonSNS: プッシュ用のサーバーキーや各証明書の登録AmazonSNS: PHPプログラムの作成AmazonSNS: 動作確認iOS: 証明書の更新AmazonSNS: 証明書の更新アプリからHTTPリクエストする例トラブル考察: 本番公開用に作成する考察: 本番公開用に作成する(上にまとめる前のメモ)その他メモその他参考になりそうなページ
■概要・前提・注意点など
■概要 AndroidとiOSにプッシュを送信するためのメモ AndroidとiOSではプッシュの仕組みはまったく異なる また、大量のプッシュを安定して配信するのも難しい部分が多い。この部分はAmazonSNSに任せることとする iOSよりもAndroidの方が実装が比較的容易なので、 まずはAndroid単体でプッシュを実装し、 次にiOS単体でプッシュを実装し、 最後にAmazonSNSでAndroidとiOSの両方にプッシュを送信できるようにする ■参考ページ Android から Amazon SNS を使ってみる - Qiita https://qiita.com/kusokamayarou/items/27e023ad06cade20c731 ※Android版のみだが、全体の流れとスクリーンショットは参考になる ただし以下に記載したKotlin・Swift・PHPプログラムなどは、別途調査してくれた人のものをベースにしている 解説で触れられている Amazon Cognito は、以下に記載したメモでは使っていない ■開発環境 サーバサイドは通常のLAMP環境で大丈夫だが、PHPを使う場合は7以上が必要 Vagrantで構築することも可能だが、アプリからPHPにアクセス場合は同一LAN内の他端末からアクセスできるようにしておく必要がある ■アプリのID いったんアプリを公開すると変更できないので、慎重に決定したい 「iOSとAndroidの両方で作る」「本番用と検収用と開発用がある」「Pushも使用する」などを考慮する 現状の結論として、iOSもAndroidも net.refirio.pushtest1 のようなIDで作って開発版書き出し時には .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-... ■Android: GCMの廃止 Googleのプッシュ送信は、以前は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 ■iOS: レガシーバイナリプロトコルの廃止 ※未検証 ApnsPHPでは「旧式のバイナリインターフェイス」を利用しているが、これは2020年11月以降使えなくなる HTTP/2ベースに移行する必要がある ニュース - Apple Developer https://developer.apple.com/jp/news/ APNs Provider API(HTTP/2)をPHPで試してみる - Qiita https://qiita.com/itosho/items/2402df4de85b360d5bd9 APNsとHTTP/2通信でiOSのPush通知 - Qiita https://qiita.com/joooee0000/items/721e809954f7d1cc3c42 cURL(curl)でアプリにプッシュ通知を送信する - すいすいSwift https://swiswiswift.com/2019-11-06/ 環境の構築は以下を参考にできそう PHP curlでHTTP/2リクエストを実行するための設定 on CentOS 7 | 稲葉サーバーデザイン https://inaba-serverdesign.jp/blog/20171011/php_curl_http2_centos7.html 以下、昔調べてもらったときの資料 【ドキュメント】バイナリ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...
■Android: アプリの作成
※いったんプッシュ機能の無い状態でアプリを作る 新規 Android Studio プロジェクトの開始 ↓ プロジェクトの選択 「空のアクティビティ」が選択されているので、そのまま「次へ」 ↓ プロジェクトの構成 名前: Push Test1 パッケージ名: net.refirio.pushtest1 (名前をもとに自動入力される) 保存ロケーション: C:\Users\refirio\AndroidStudioProjects\PushTest1 (アプリケーション名をもとに自動入力される) 言語: Kotlin 最小APIレベル: API 19: Android 4.4 (KitKat) 「完了」 「最小APIレベル」は、案件のターゲット層に応じて調整する エミュレータと実機で、アプリを起動できるかテストする
■Android: 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」ではなく「プロジェクト」に表示を切り替えてから「app」フォルダアイコンに大してドラッグ&ドロップで配置する) 配置できたら「次へ」をクリック 「3 Firebase SDK の追加」という画面になる プロジェクト直下の build.gradle と、「app」直下の build.gradle に指定のコードを追加する 追加したら、AndroidStudioの画面上部に「今すぐ同期」が表示されるのでクリックする ビルドが完了されるまで待つ。完了したら「次へ」をクリック (google-services.json が見つからない旨のエラーが表示されたら、配置場所を確認してプロジェクトのクリーンと再ビルドを試す) 「4 アプリを実行してインストールを確認」という画面になる 指示通りアプリを実行してしばらく待つと「Firebase がアプリに正常に追加されました。」と表示される (何十秒か待つ。エミュレータで実行しても大丈夫だった。実機で実行した場合、その実機がインターネットに繋がっている必要がある) 「コンソールに進む」が表示されるのでクリック
■Android: アプリにPush機能を実装
■app\build.gradle
implementation 'com.google.firebase:firebase-analytics:17.2.0' ↓ implementation 'com.google.firebase:firebase-analytics:17.2.0' implementation 'com.google.firebase:firebase-core:17.0.0' implementation 'com.google.firebase:firebase-messaging:19.0.0'
「firebase-messaging」の最新バージョンは以下で確認できる バージョン情報以外にも、手順は参考にできる Android プロジェクトに Firebase を追加する | Firebase https://firebase.google.com/docs/android/setup?hl=ja 追加したら「今すぐ同期」 ■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>
■app\src\main\res\drawable-hdpi プッシュ用のアイコン画像を追加(アイコンの指定を省略すると、プッシュを受信できないようなので指定する) ic_launcher.png ... 72px×72pxのPNG画像にしたが、サイズなど改めて調査したい ic_stat_notification.png ... 38px×38pxのPNG画像にしたが、サイズなど改めて調査したい ★アイコン無しで動作するか確認したい ★アプリが起動中か否かで、プッシュにアイコンが反映されたりされなかったりする。対応したい ■app\src\main\res\layout\activity_main.xml ★Android版とiOS版で動きをできるだけ統一したい。両方ともタイトルをサーバ側から指定できるようにしたり。バイブレーションや音の設定をしたり
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.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"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="24dp" android:text="DeviceToken" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/text_device_token" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="12dp" android:layout_marginEnd="24dp" 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_toBottomOf="@+id/textView" /> </androidx.constraintlayout.widget.ConstraintLayout>
■app\src\main\res\values\strings.xml
<resources> <string name="app_name">Push Test1</string> <string name="default_notification_channel_id">0</string> </resources>
■app\src\main\java\net\refirio\pushtest1\MainActivity.kt
package net.refirio.pushtest1 import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.widget.EditText import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.iid.FirebaseInstanceId class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // テキストフィールド val textDeviceToken = findViewById<EditText>(R.id.text_device_token) // トークンを取得 FirebaseInstanceId.getInstance().instanceId.addOnCompleteListener(OnCompleteListener { task -> if (!task.isSuccessful) { Log.d("MainActivity", "instanceId failed", task.exception) return@OnCompleteListener } val token: String = task.result!!.token textDeviceToken.setText(token) }) } }
■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.os.Build import androidx.core.app.NotificationCompat import android.util.Log import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage class MyFirebaseMessagingService : FirebaseMessagingService() { /** * メッセージ受信時の処理 */ override fun onMessageReceived(remoteMessage: RemoteMessage?) { Log.d("MyFirebaseMsgService", "From: " + remoteMessage!!.from!!) // メッセージがdataを含むかどうかを確認 if (remoteMessage.getData().isNotEmpty()) { Log.d("MyFirebaseMsgService", "Message data payload: " + remoteMessage.getData().get("default")) // 10秒以上処理にかかる場合は、Firebase Job Dispatcherを使用する sendNotification(this, remoteMessage.getData().get("default")) } // メッセージがnotificationを含むかどうか確認 if (remoteMessage.notification != null) { Log.d("MyFirebaseMsgService", "Message Notification Body: " + remoteMessage.notification!!.body!!) // 通知を表示する sendNotification(this, remoteMessage.notification!!.body!!) } } /** * 通知を表示する */ 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()); } }
■動作確認 アプリを実行すると、画面内のテキストフィールドに文字列(デバイストークン)が表示される この文字列をもとに、サーバサイドプログラムからプッシュを送信する ■Firebaseから送信(デバッグ用) プロジェクトのページを開く 左メニュー 拡大 → Cloud Messaging → Send your first message(最初のメッセージを送信) ※「拡大」はメニューの下の方にある。初期表示では隠れている可能性があるので、下にスクロールする ※2回目以降に送信するときは「新しい通知」をクリック 通知のタイトル: テストのタイトル 通知テキスト: テストのテキスト ターゲット: ユーザーセグメント ターゲットとするユーザー: アプリ net.refirio.pushtest1 その他必要に応じて設定し、「確認」をクリックすると確認画面が表示されるので、「公開」をクリックすると送信できる
■Android: アプリにPHPからPushを送信
■Firebase Admin SDKのインストール Add the Firebase Admin SDK to Your Server https://firebase.google.com/docs/admin/setup 公式サイトにはPHPはないので、以下を使用する。PHP7以上のみ対応 https://github.com/kreait/firebase-php/ ドキュメント https://firebase-php.readthedocs.io/en/latest/ 以下のように、ComposerでSDKをインストールできる $ cd /path/to/directory $ curl -sS https://getcomposer.org/installer | php $ php composer.phar require kreait/firebase-php ■秘密鍵の入手 Firebaseのプロジェクトの設定ページで対象のアプリをクリックし、表示された歯車アイコンをクリック サービスアカウント → 新しいサービスアカウント作成 → 新しい秘密鍵の生成 → キーを生成 「Admin SDK 構成スニペット」に「PHP」が無いので「Node.js」のままで秘密鍵の生成を行ってみる push-test-54dbc-firebase-adminsdk-8cdmf-17691c7f2a.json のような名前のファイルがダウンロードされる このファイルとデバイストークンを使って、直接プッシュを送信できる ■プログラムの作成 ※文字コードは UTF-8N にする
<?php require __DIR__ . '/vendor/autoload.php'; use Kreait\Firebase; use Kreait\Firebase\Messaging\CloudMessage; // 秘密鍵 $serviceAccount = Firebase\ServiceAccount::fromJsonFile(__DIR__ . '/push-test-54dbc-firebase-adminsdk-8cdmf-17691c7f2a.json'); // Firebaseに接続 $firebase = (new Firebase\Factory) ->withServiceAccount($serviceAccount) ->create(); // プッシュ送信先のデバイストークン $deviceToken = 'cniQkqpYVmU:APA91bHnz0c_5Wfnd6kJV-gItvjDMcnFo8Yh4XG2BTfs5weZpLkdduOEIr2tEryacHwEKUppYEee-XHjAYgG2pSVM15cYM_EB55z2OGUtdRoL_2TGVTix5Vx9r2lnYovC6y3xUubf_ij'; // プッシュの送信メッセージ $message = CloudMessage::fromArray([ 'token' => $deviceToken, 'data' => ['message' => 'TEST'], 'notification' => [ 'title' => 'テスト', 'body' => 'これはプッシュ送信のテストです。', ], ]); // メッセージを送信 $messaging = $firebase->getMessaging(); $messaging->send($message); print('OK');
■動作確認 作成したPHPプログラムにアクセスして、アプリにプッシュが届くことを確認する
■iOS: アプリの作成
■iOSアプリの作成 ※いったんプッシュ機能の無い状態でアプリを作る ※Organization Identifier を考慮して、いったん「pushtest1」のような Product Name で作成してIDを決めさせ、後から名前を変更する Xcodeでプロジェクトを作成する Create a new Xcode project ↓ iOS ↓ Single View App 「Next」 ↓ Product Name: pushtest1 Team: (案件に応じて適切に選択する) Organization Name: refirio Organization Identifier: net.refirio Language: Swift User Interface: Storyboard 「Next」 ↓ プロジェクトの作成場所を選択 「Create」 Bundle Identifier は、上記内容でアプリを作成すると「net.refirio.pushtest1」になる エミュレータと実機で、アプリを起動できるかテストする
■iOS: 証明書の作成
■Apple Developer Programへのログイン ブラウザで以下にログインできることを確認しておく Apple Developer Program https://developer.apple.com/jp/programs/ 右上のアカウント名が案件に応じたものになっていることを確認する ■Push通知の使用を設定 Xcodeのプロジェクトの「Settings & Capabilities」で「+Capabilities」をクリックし、 一覧に表示される「Push Notifications」をダブルクリックで選択する Apple Developer Program で Certificates, IDs & Profiles → Identifiers にアクセスして確認すると、一覧に 「XC net refirio pushtest1 (net.refirio.pushtest1)」 が追加されている。クリックして確認すると、「Push Notifications」が「Configurable」になっている ※Xcodeから登録されたAppIDは、名前に「XC」のプレフィックスが付く(IDではなく名前なので、後から編集すればいいみたい) ※プッシュなど特別な機能を使わない場合は基本的にワイルドカード扱いになり、Identifiersの一覧には表示されない ■CSRを作成 Macでキーチェーンアクセスを起動 メニューから キーチェーンアクセス → 証明書アシスタント → 認証局に証明書を要求... を実行 ユーザのメールアドレス: refirio@example.com 通称: refirio (日本語を含めると、AmazonSNSに登録できないので注意) CAのメールアドレス: (空欄) 要求の処理: 「ディスクに保存」「鍵ペア情報を指定」にチェックを入れる 「続ける」 ↓ 保存場所を指定する 「保存」 ↓ 鍵のサイズ: 2048ビット アルゴリズム: RSA デフォルトで上記設定のはずなので「続ける」 ↓ 証明書要求がディスク上に作成されました 「完了」 デフォルトでは CertificateSigningRequest.certSigningRequest というファイル名で作成される これでCSRの作成は完了。引き続きCSRをAppleに登録する ■プッシュ通知用の証明書を作成 ブラウザから Apple Developer Program の Certificates, IDs & Profiles → Identifiers → XC net refirio pushtest1 (net.refirio.pushtest1) をクリック Push Notifications にある「Configure」をクリック さらに「Development SSL Certificate」の「Create Certificate」をクリック (いったん開発用だけでいい。本番用やアドホック用の場合は「Production SSL Certificate」の「Create Certificate」をクリック) ↓ Create a New Certificate 作成したCSRファイルを選択する 「Continue」 ↓ Download Your Certificate 「Download」をクリックして証明書をダウンロードする(aps_development.cer) ■プッシュ通知用のp12ファイルを作成 Macで証明書 aps_development.cer をダブルクリックして、キーチェーンに登録する (登録すると、キーチェーンアクセスの一覧に表示される) ↓ 登録したファイルを選択して「ファイル → 書き出す」をクリック(「分類」を「自分の証明書」にした状態で探す。Finderで検索してから書き出そうとすると、p12を選択できないので注意) デフォルトで「証明書」というファイル名になるが、日本語ファイル名だとAWSへの登録に失敗するので変更する ここでは「Push-Test1-Dev」として保存する(開発版用。本番用なら「Push-Test1」などとする) 「書き出した項目を保護するために使用されるパスワード」の入力画面になるが、パスワードはカラのまま「OK」をクリック 「キーチェーンアクセスは、キーチェーンのキー○○を書き出そうとしています。」の入力画面になるが、Macのログインパスワードを入力して「許可」をクリック
■iOS: アプリにPush機能を実装
Xcodeのプロジェクトの「Settings & Capabilities」で「+Capabilities」をクリックし、一覧に表示される「Background Modes」をダブルクリックで選択する その後、画面に表示される「Remote notification」にチェックを入れる 以下の記事に参考画面があるが、当時からUIは変更されている You've implemented -[ application: didReceiveRemoteNotification: fetchCompletionHandler:], but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.の解決法 - ゆーじのUnity開発日記 http://unity-yuji.xyz/youve-implemented-applicationdidreceiveremotenotification-remote-notification-... ストーリーボードにパーツを配置(オートレイアウトなどは、必要に応じて設定する)
Device Token ... Lavel [ ] ... Text Field(一行入力欄)
ViewController.swift
// // ViewController.swift // pushtest1 // // Created by refirio on 2019/12/13. // Copyright (C) 2019 refirio. All rights reserved. // import UIKit class ViewController: UIViewController { @IBOutlet weak var deviceTokenTextField: UITextField! let ap = UIApplication.shared.delegate as! AppDelegate override func viewDidLoad() { super.viewDidLoad() ap.mainView = self // アプリを起動したらバッジの数字を消す UIApplication.shared.applicationIconBadgeNumber = 0 } }
AppDelegate.swift
// // AppDelegate.swift // pushtest1 // // Created by refirio on 2019/12/13. // Copyright (C) 2019 refirio. All rights reserved. // import UIKit import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var mainView : ViewController! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. registerForPushNotifications() return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } // プッシュ通知の許可を得られたとき 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.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") } */ // アプリがフォアグラウンドの際に通知を受け取ったとき func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { // 通知センターのバナーを表示する completionHandler([.alert, .sound]) // 通知バナー表示、通知音の再生を指定 } // プッシュ通知の許可を得る func registerForPushNotifications() { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound,.badge]) { (Bool, Error) in print("Permission granted: \(Bool)") DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } }
※plistの編集は不要だった ※CocoaPodsのインストールは不要だった ■動作確認 アプリを実行すると、画面内のテキストフィールドに文字列(デバイストークン)が表示される この文字列をもとに、サーバサイドプログラムからプッシュを送信する ※エミュレータではプッシュを受け取れないので、実機で実行する ※実行すると「"pushtest1"は通知を送信します。よろしいですか?」のダイアログが表示されるので許可する ■MacアプリケーションのPusherから送信(デバッグ用) MacにPusherをインストールすれば、そこからプッシュを送ることができる https://github.com/noodlewerk/NWPusher/releases インストールして起動する 証明書の選択欄があるが、ここにはキーチェーンアクセスに登録してあるPush通知の証明書が表示される 選択して「Reconnect」をクリックする。パスワードを求められたら、Macのログインパスワードを入力する その下に送信先を入力する ここにはデバイストークンを入力する 一番下のテキストエリアに、送信したい内容を入力する 以下のようにJSON形式で入力できる
{"aps":{"alert":"テスト1","badge":1,"sound":"default"}}
「Push」ボタンを押すとプッシュが送信される ■WindowsアプリケーションのPush Notificationsから送信(デバッグ用) WindowsにPush Notificationsをインストールすれば、そこからプッシュを送ることができる https://github.com/onmyway133/PushNotifications/releases Push.Notifications-1.7.3-windows.msi をダウンロード ダウンロードしてインストールし、起動する p12ファイルでプッシュを送信する場合、 iOS → Authentication → CERTIFICATE で「SELECT P12」をクリックし、p12ファイルを選択する p8ファイルでプッシュを送信する場合、 iOS → Authentication → TOKEN で「SELECT P8」をクリックし、p8ファイルを選択する 「Enter key id」と「Enter team id」にも必要情報を入力する iOS → Body で「Enter bundle id」にバンドルIDを入力する で「Enter device token」にデバイストークンを入力する iOS → Environment は「Development SSL Certificate」としてプッシュ通知用の証明書を作成した場合は「Sandbox」のまま 「Production SSL Certificate」としてプッシュ通知用の証明書を作成した場合は「Production」に変更する 「Send」ボタンを押すとプッシュが送信される
■iOS: アプリにPHPからPushを送信
※ApnsPHPでは「旧式のバイナリインターフェイス」を利用しているが、これは2020年11月以降使えなくなる 今後はHTTP/2ベースの仕組みに移行する必要がある 詳細は、このファイル内の「iOS: アプリにPHPからPushを送信(HTTP/2版)」を参照 ■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-Test1-Dev.p12 をpem形式に変換する パスワードの入力を求められるが、p12ファイルをパスワードなしで作成した場合、パスワードは空欄のままEnterでいい $ openssl pkcs12 -in Push-Test1-Dev.p12 -out server_certificates_bundle_sandbox.pem -nodes -clcerts Enter Import Password: MAC verified OK このファイルとデバイストークンを使って、直接プッシュを送信できる ■プログラムの作成 ※文字コードは UTF-8N にする
<?php require_once 'ApnsPHP/Autoload.php'; // pemファイル $push = new ApnsPHP_Push( ApnsPHP_Abstract::ENVIRONMENT_SANDBOX, 'server_certificates_bundle_sandbox.pem' ); // サーバルート証明書 $push->setRootCertificationAuthority('entrust_2048_ca.cer'); // Apple Push Notification Service に接続 $push->connect(); // プッシュ送信先のデバイストークン $message = new ApnsPHP_Message('f7b8b8ade018f18e8b0dd9e34cfd1764575941243e21d9b47a6d3a3ad62d277b'); // バッジに表示する数字 $message->setBadge(2); // プッシュの送信メッセージ $message->setText('これはプッシュ送信のテストです。'); // サウンドを再生 $message->setSound(); // カスタムプロパティ $message->setCustomProperty('test1', array('aaa', 'bbb')); $message->setCustomProperty('test2', array('ccc', 'ddd')); // Set the expiry value to 30 seconds $message->setExpiry(30); // メッセージを送信 $push->add($message); $push->send(); // Apple Push Notification Service との接続を終了 $push->disconnect(); // エラーメッセージを確認 $errorQueue = $push->getErrors(); if (empty($errorQueue)) { echo 'OK'; } else { var_dump($errorQueue); }
■動作確認 作成したPHPプログラムにアクセスして、アプリにプッシュが届くことを確認する
■iOS: アプリにPHPからPushを送信(HTTP/2版)
※「旧式のバイナリインターフェイス」が使える間は、この手順は省略できる が、早い段階で検証しておくことを推奨 ■pemファイルの作成 「iOS: アプリにPHPからPushを送信」と同じ手順で作成する ■環境の構築 PHP curlでHTTP/2リクエストを実行するための設定 on CentOS 7 | 稲葉サーバーデザイン https://inaba-serverdesign.jp/blog/20171011/php_curl_http2_centos7.html PHP+curlでHTTP/2リクエストを送信できる環境が必要 上記ページが参考になりそうだが、Amazon Linux 2 なら特別な更新作業なしにPHP+curlでHTTP/2リクエストを送信できた 以下は2020年1月7日時点に構築した Amazon Linux 2。PHPはExtrasリポジトリからインストールした # cat /etc/system-release Amazon Linux release 2 (Karoo) # openssl version OpenSSL 1.0.2k-fips 26 Jan 2017 # php --version PHP 7.3.11 (cli) (built: Oct 31 2019 19:16:47) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies # curl --version curl 7.61.1 (x86_64-koji-linux-gnu) libcurl/7.61.1 OpenSSL/1.0.2k zlib/1.2.7 libidn2/2.3.0 libssh2/1.4.3 nghttp2/1.39.2 Release-Date: 2018-09-05 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-proxy Metalink # php -r 'phpinfo();' | grep SSL SSL => Yes MULTI_SSL => No SSL Version => OpenSSL/1.0.2k core SSL => supported extended SSL => supported OpenSSL support => enabled OpenSSL Library Version => OpenSSL 1.0.2k-fips 26 Jan 2017 OpenSSL Header Version => OpenSSL 1.0.2k 26 Jan 2017 Native OpenSSL support => enabled # curl -vso /dev/null --http2 https://www.google.co.jp/ * Trying 216.58.197.131... * TCP_NODELAY set * Connected to www.google.co.jp (216.58.197.131) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * TLSv1.2 (OUT), TLS header, Certificate Status (22): } [5 bytes data] * TLSv1.2 (OUT), TLS handshake, Client hello (1): } [512 bytes data] * TLSv1.2 (IN), TLS handshake, Server hello (2): { [96 bytes data] * TLSv1.2 (IN), TLS handshake, Certificate (11): { [3491 bytes data] * TLSv1.2 (IN), TLS handshake, Server key exchange (12): { [148 bytes data] * TLSv1.2 (IN), TLS handshake, Server finished (14): { [4 bytes data] * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): } [70 bytes data] * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): } [1 bytes data] * TLSv1.2 (OUT), TLS handshake, Finished (20): } [16 bytes data] * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): { [1 bytes data] * TLSv1.2 (IN), TLS handshake, Finished (20): { [16 bytes data] * SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256 * ALPN, server accepted to use h2 * Server certificate: * subject: C=US; ST=California; L=Mountain View; O=Google LLC; CN=*.google.com * start date: Dec 3 14:48:49 2019 GMT * expire date: Feb 25 14:48:49 2020 GMT * subjectAltName: host "www.google.co.jp" matched cert's "*.google.co.jp" * issuer: C=US; O=Google Trust Services; CN=GTS CA 1O1 * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 } [5 bytes data] * Using Stream ID: 1 (easy handle 0x6fdb70) } [5 bytes data] > GET / HTTP/2 > Host: www.google.co.jp > User-Agent: curl/7.61.1 > Accept: */* > { [5 bytes data] * Connection state changed (MAX_CONCURRENT_STREAMS == 100)! } [5 bytes data] < HTTP/2 200 < date: Tue, 07 Jan 2020 02:42:04 GMT < expires: -1 < cache-control: private, max-age=0 < content-type: text/html; charset=Shift_JIS < p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info." < server: gws < x-xss-protection: 0 < x-frame-options: SAMEORIGIN < set-cookie: 1P_JAR=2020-01-07-02; expires=Thu, 06-Feb-2020 02:42:04 GMT; path=/; domain=.google.co.jp; Secure < set-cookie: NID=195=J90uYLvbutUBMyCjx5B-cfvCaSQbmbTbItStMl_8Md-t_k6qRCq18qweObs-AYSOHrUVkw0u3xS-4y6DC_1kC4eOgd5JC94rgpdM9qm-62BiEZecOeYCMXqqZbdvZgdIEUpvAMBKM_gWQK2X_p5YFwtyCoHVl0M9B-wegtle3hc; expires=Wed, 08-Jul-2020 02:42:04 GMT; path=/; domain=.google.co.jp; HttpOnly < alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000 < accept-ranges: none < vary: Accept-Encoding < { [5 bytes data] * Connection #0 to host www.google.co.jp left intact ■テスト用PHPプログラム ※文字コードは UTF-8N にする $ vi curl_test.php
<?php if (!defined('CURL_HTTP_VERSION_2_0')) { define('CURL_HTTP_VERSION_2_0', CURL_HTTP_VERSION_1_1 + 1); } $url = 'https://www.google.co.jp/'; $opts = [ CURLOPT_VERBOSE => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYPEER => false ]; $ch = curl_init($url); curl_setopt_array($ch, $opts); curl_exec($ch); curl_close($ch);
以下で実行できる Googleのページデータを取得できれば成功 # php curl_test.php * Trying 172.217.25.227... * TCP_NODELAY set * Connected to www.google.co.jp (172.217.25.227) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256 * ALPN, server accepted to use h2 * Server certificate: * subject: C=US; ST=California; L=Mountain View; O=Google LLC; CN=*.google.com * start date: Dec 10 08:42:43 2019 GMT * expire date: Mar 3 08:42:43 2020 GMT * issuer: C=US; O=Google Trust Services; CN=GTS CA 1O1 * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x55866798eba0) > GET / HTTP/2 Host: www.google.co.jp Accept: */* * Connection state changed (MAX_CONCURRENT_STREAMS == 100)! < HTTP/2 200 < date: Tue, 07 Jan 2020 02:54:02 GMT < expires: -1 < cache-control: private, max-age=0 < content-type: text/html; charset=Shift_JIS < p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info." < server: gws < x-xss-protection: 0 < x-frame-options: SAMEORIGIN < set-cookie: 1P_JAR=2020-01-07-02; expires=Thu, 06-Feb-2020 02:54:02 GMT; path=/; domain=.google.co.jp; Secure < set-cookie: NID=195=F6rGPJWX6DSZJFKvW036m6NKEqLkasi-SLWmkjF6rwGRm9Ks9S2Ix3xVj1Kplj1_C1Bg0gc8LOsjuFJcR64Orm-tR7h0pIBuncfPxFEZsqP-pysrYrU_ah2C24Tty8ng2cyDMIBiPbOBBlluAPryLSYiJbOTMX8DoCgjwpRMUzU; expires=Wed, 08-Jul-2020 02:54:02 GMT; path=/; domain=.google.co.jp; HttpOnly < alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000 < accept-ranges: none < vary: Accept-Encoding < <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head> 〜〜中略 </body></html>* Connection #0 to host www.google.co.jp left intact ■curlでプッシュを送信 以下のコマンドでプッシュを送信できる(curlで送信するならPHPは関係ない) $ curl -v -d '{"aps":{"alert":"[送信メッセージ]"}}' -H "[アプリのID]" --http2 --cert [pemファイル] https://api.development.push.apple.com/3/device/[デバイストークン] 具体的には以下のようになる (server_certificates_bundle_sandbox.pem は同じ階層に配置してあるものとする p12 ファイルではなく pem ファイルが必要なので注意) $ curl -v -d '{"aps":{"alert":"Hello!"}}' -H "net.refirio.pushtest1" --http2 --cert server_certificates_bundle_sandbox.pem https://api.development.push.apple.com/3/device/f7b8b8ade018f18e8b0dd9e34cfd1764575941243e21d9b47a6d... * Trying 17.188.165.219... * TCP_NODELAY set * Connected to api.development.push.apple.com (17.188.165.219) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * TLSv1.2 (OUT), TLS header, Certificate Status (22): * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Request CERT (13): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Certificate (11): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS handshake, CERT verify (15): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 * ALPN, server accepted to use h2 * Server certificate: * subject: CN=api.development.push.apple.com; OU=management:idms.group.533599; O=Apple Inc.; ST=California; C=US * start date: Apr 17 17:37:51 2019 GMT * expire date: May 16 17:37:51 2021 GMT * subjectAltName: host "api.development.push.apple.com" matched cert's "api.development.push.apple.com" * issuer: CN=Apple IST CA 2 - G1; OU=Certification Authority; O=Apple Inc.; C=US * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x2046b70) > POST /3/device/f7b8b8ade018f18e8b0dd9e34cfd1764575941243e21d9b47a6d3a3ad62d277b HTTP/2 > Host: api.development.push.apple.com > User-Agent: curl/7.61.1 > Accept: */* > Content-Length: 26 > Content-Type: application/x-www-form-urlencoded > * Connection state changed (MAX_CONCURRENT_STREAMS == 1000)! * We are completely uploaded and fine < HTTP/2 200 < apns-id: DE5A62A4-C4FE-E576-8245-03404F50A790 < * Connection #0 to host api.development.push.apple.com left intact ■PHPでプッシュを送信 ※文字コードは UTF-8N にする apns_test.php
<?php if (defined('CURL_HTTP_VERSION_2_0')) { $ch = curl_init('https://api.development.push.apple.com/3/device/f7b8b8ade018f18e8b0dd9e34cfd1764575941243e21d9b47a6d3a3ad62d277b'); curl_setopt($ch, CURLOPT_POSTFIELDS, '{"aps":{"alert":"Hello!"}}'); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); curl_setopt($ch, CURLOPT_HTTPHEADER, ['apns-topic: net.refirio.pushtest1']); curl_setopt($ch, CURLOPT_SSLCERT, 'server_certificates_bundle_sandbox.pem'); //curl_setopt($ch, CURLOPT_SSLCERTPASSWD, 'your pem secret'); // pemファイルにパスワードを設定している場合は必要 $response = curl_exec($ch); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); var_dump($response); var_dump($httpcode); }
以下で実行できる (server_certificates_bundle_sandbox.pem は同じ階層に配置してあるものとする) $ php apns_test.php bool(true) int(200) 送信内容に問題があると、JSON形式でエラー内容を返してくれる 以下はデバイストークンを間違えた場合の例 $ php apns_test.php {"reason":"BadDeviceToken"}bool(true) int(400)
■AmazonSNS: プッシュ用のサーバーキーや各証明書の登録
■概要 AndroidとiOSにプッシュを送信できるようになったら、AmazonSNSから一括して送信できるようにする プッシュを送信するためのキーや証明書を、AmazonSNSに登録することで可能になる ■Firebaseのサーバーキーの取得(Android) Firebaseのコンソールに追加されている「Push Test1 Dev」にある歯車(設定)のアイコンをクリック 設定画面に遷移するので、タブメニューから「クラウドメッセージング」をクリック 「サーバーキー」の値を確認する AAAAsNOhnZY:APA91bEp7obtws-R7EoKzv3NhywPdBoxaHW2iFKR4aCtgQtSppooVN-oAXATFUDeGCuPqb7NADe3XVycNWSBvsC8ip4ta_ejWVH-ABCDEFGHIJKLMN-XXXXXXXXXX ■AmazonSNSアプリケーションの作成(Android) AWSのコンソールから「Simple Notification Service」を開く 左メニューから「プッシュ通知」を開く 画面内、一覧上部の「プラットフォームアプリケーションの作成」をクリック アプリケーション名: Push-Test1-FCM-Dev(開発版想定。本番なら「Push-Test1-FCM」などとする。iOS用にも作る可能性があるので、単に「Push-Test1」ではなく「FCM」の文字を含めておくのが無難そう) プッシュ通知プラットフォーム: Firebase Cloud Messaging (FCM) APIキー: (「サーバーキーの取得」で取得した「サーバーキー」) 「プラットフォームアプリケーションの作成」をクリック アプリケーション一覧に追加されたことを確認する ■AmazonSNSアプリケーションの作成(iOS) AWSのコンソールから「Simple Notification Service」を開く 左メニューから「プッシュ通知」を開く 画面内、一覧上部の「プラットフォームアプリケーションの作成」をクリック アプリケーション名: Push-Test1-APNS-Dev(開発版想定。本番なら「Push-Test1-APNS」などとする。Android用にも作る可能性があるので、単に「Push-Test1」ではなく「APNS」の文字を含めておくのが無難そう) プッシュ通知プラットフォーム: Apple iOS/VoIP/Mac サンドボックスでの開発に使用されます: 開発用(Development SSL Certificate)の場合、チェックを入れておく プッシュ証明書タイプ: iOSプッシュ証明書 証明書: 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-FCM-Dev-All 表示名: (SMS用の項目なので空欄) 「トピックの作成」をクリック トピック一覧に追加されたことを確認する (表示されるARNの値は、後ほどPHPプログラムに設定する) トピックのARNを指定するだけで一斉送信ができるので、運用の際は「全端末配信用」のトピックを作っておくといい 後からトピックを追加すると、トピックに対して端末を登録する必要があるので注意 トピックを気軽に増減する設計は避けるほうがいいかも。要検証 ■AmazonSNSトピックの作成(iOSへ一斉送信するトピックを作成する場合) Androidと同じ要領で、以下のトピックを作成する 名前: Push-Test1-APNS-Dev-All 表示名: (SMS用の項目なので空欄) ■AmazonSNSトピックの作成(すべてのデバイスへ一斉送信するトピックを作成する場合) Androidと同じ要領で、以下のトピックを作成する 名前: Push-Test1-Dev-All 表示名: (SMS用の項目なので空欄)
■AmazonSNS: PHPプログラムの作成
AWSのSDKを使ってプッシュを送信できる Composerを使う場合、以下でインストールできる kreait/firebase-php をインストール済みなら、同じ場所で追加インストールすればいい $ php composer.phar require aws/aws-sdk-php 環境によっては以下のコマンドになる $ composer require aws/aws-sdk-php 以下からSDKを入手して、手動で配置することもできる https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html 以降のフォルダ名は pushtest1-dev としているが、これは開発版想定 証明書などが異なるので、本番用を作るなら pushtest1 フォルダなどに、検収版を作るなら pushtest1-stg フォルダなどに、別途作ると良さそう アプリからPHPプログラムを呼び出す場合、SSL経由にする必要があるので注意 (この例では一方的に送信するだけなので問題ないが、本番用のアプリなら端末からPHPにアクセスしてデバイストークンを渡したり…が必要) ■pushtest1-dev\index.php ※文字コードは UTF-8N にする
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AmazonSNS</title> </head> <body> <h1>AmazonSNS</h1> <h2>CreatePlatformEndpoint</h2> <p>アプリケーションに端末を追加。</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> <h2>ListEndpointsByPlatformApplication</h2> <p>アプリケーションに対するエンドポイントの一覧を取得。(1回のリクエストで100件まで。1秒間に30トランザクションまで。)</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> <h2>GetEndpointAttributes</h2> <p>アプリケーションに対するエンドポイントの状態を取得。</p> <form action="exec.php" method="post"> <input type="hidden" name="api" value="getEndpointAttributes"> <dl> <dt>endpointArn(必須)</dt> <dd><input type="text" size="60" name="endpointArn"></dd> </dl> <p><input type="submit" value="実行"></p> </form> <h2>SetEndpointAttributes</h2> <p>アプリケーションに対するエンドポイントの状態を変更。</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> <h2>CreateTopic</h2> <p>トピックを追加。</p> <form action="exec.php" method="post"> <input type="hidden" name="api" value="createTopic"> <dl> <dt>name(必須)</dt> <dd><input type="text" size="60" name="name"></dd> </dl> <p><input type="submit" value="実行"></p> </form> <h2>Subscribe</h2> <p>トピックにエンドポイントを追加。</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> <h2>ListTopics</h2> <p>トピック一覧を取得。(1回のリクエストで100件まで。1秒間に30トランザクションまで。)</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> <h2>Publish</h2> <p>指定した端末もしくはトピックに対してプッシュを送信。</p> <form action="exec.php" 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><input type="submit" value="実行"></p> </form> </body> </html>
■pushtest1-dev\exec.php ※文字コードは UTF-8N にする
<?php require __DIR__ . '/vendor/autoload.php'; use Aws\Sns\SnsClient; /* アクセスキー */ $aws_access_key = 'XXXXX'; $aws_secret_access_key = 'YYYYY'; /* AmazonSNS */ $snsClient = new SnsClient([ 'version' => 'latest', 'credentials' => [ 'key' => $aws_access_key, 'secret' => $aws_secret_access_key, ], 'region' => 'ap-northeast-1', ]); $result = null; $code = null; $data = null; try { switch ($_POST['api']) { case 'createPlatformEndpoint': /** * アプリケーションに端末を追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createplatformendpoint */ $result = $snsClient->createPlatformEndpoint([ 'PlatformApplicationArn' => $_POST['applicationArn'], 'Token' => $_POST['deviceToken'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['EndpointArn']; break; case 'listEndpointsByPlatformApplication': /** * アプリケーションに対するエンドポイントの一覧を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listendpointsbyplatformapplic... */ $result = $snsClient->listEndpointsByPlatformApplication([ 'NextToken' => $_POST['nextToken'], 'PlatformApplicationArn' => $_POST['applicationArn'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['Endpoints']; break; case 'getEndpointAttributes': /** * アプリケーションに対するエンドポイントの状態を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#getendpointattributes */ $result = $snsClient->getEndpointAttributes([ 'EndpointArn' => $_POST['endpointArn'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['Attributes']; break; case 'setEndpointAttributes': /** * アプリケーションに対するエンドポイントの状態を変更 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#setendpointattributes */ $result = $snsClient->setEndpointAttributes([ 'Attributes' => [ 'Enabled' => $_POST['enabled'] ], 'EndpointArn' => $_POST['endpointArn'], ]); $code = $result->get('@metadata')['statusCode']; $data = []; break; case 'createTopic': /** * トピックを追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createtopic */ $result = $snsClient->createTopic([ 'Name' => $_POST['name'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['TopicArn']; break; case 'subscribe': /** * トピックにエンドポイントを追加 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#subscribe */ $result = $snsClient->subscribe([ 'Endpoint' => $_POST['endpoint'], 'Protocol' => 'application', 'ReturnSubscriptionArn' => false, 'TopicArn' => $_POST['topicArn'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['SubscriptionArn']; break; case 'listTopics': /** * トピック一覧を取得 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listtopics */ $result = $snsClient->listTopics([ 'NextToken' => $_POST['nextToken'], ]); $code = $result->get('@metadata')['statusCode']; $data = $result['Topics']; break; case 'publish': /** * 指定した端末もしくはトピックに対してプッシュを送信 * https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#publish */ $parameter = [ 'Message' => $_POST['message'], ]; if ($_POST['targetArn'] != '') { $parameter['TargetArn'] = $_POST['targetArn']; } elseif ($_POST['topicArn'] != '') { $parameter['TopicArn'] = $_POST['topicArn']; } $result = $snsClient->publish($parameter); $code = $result->get('@metadata')['statusCode']; $data = $result['MessageId']; break; default: break; } } catch (Exception $e) { $result = $e->getMessage(); } /* 実行結果を表示 */ ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AmazonSNS</title> </head> <body> <h1>AmazonSNS</h1> <pre><?php print_r(['code' => $code, 'data' => $data, 'result' => $result]) ?></pre> </body> </html>
■補足 プッシュ送信部分の $parameter を組み立てる部分を以下のように変更すると、単純なメッセージ以外も送信できる ただしAndroidではプッシュの本文が表示されなくなるので、Androidアプリ側で受け取り処理の調整が必要になるみたい(検証中)
$fcm = json_encode([ 'data' => [ 'message' => $_POST['message'], 'param1' => 'xxx', 'param2' => 'yyy' ], ]); $apns = json_encode([ 'aps' => [ //'alert' => $_POST['message'], 'alert' => [ //'title' => 'タイトル', //'subtitle' => 'サブタイトル', 'body' => $_POST['message'], ], 'badge' => 0, 'sound' => 'default' ], 'param1' => 'xxx', 'param2' => 'yyy' ]); $message = [ 'default' => $_POST['message'], 'FCM' => $fcm, 'APNS' => $apns, 'APNS_SANDBOX' => $apns, ]; $parameter = [ 'Message' => json_encode($message), 'MessageStructure' => 'json', ]; /* $parameter = [ 'Message' => $_POST['message'], ]; */
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: 動作確認
■端末ごとに送信(Android) Simple Notification Service → プッシュ通知 → 作成したアプリケーションの名前をクリック この画面で、「このアプリケーションにはエンドポイントがありません。」と表示されていることを確認する 作成したPHPプログラムの index.php にアクセスし、 「CreatePlatformEndpoint」で以下を入力して実行する applicationArn: arn:aws:sns:ap-northeast-1:0123456789:app/GCM/Push-Test4-FCM-Dev deviceToken: (プッシュを送信したいAndroid端末のデバイストークン) 先ほどの画面に、エンドポイントが追加されていることを確認する トークンに対応したARNが作成され、AmazonSNSでプッシュを送るときはこのARNを使用する 作成したPHPプログラムの index.php にアクセスし、 「Publish」で以下を入力して実行する message: 任意のメッセージ(日本語可)を入力 targetArn: (プッシュを送信したいAndroid端末のARN) なお、無効になったエンドポイントに送信した場合、以下のエラーが表示される これをもとに無効な送信先を削除したりはできそう これとは別に、アプリを起動するたびにデバイストークンの更新を行うと良さそう
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>
■端末ごとに送信(iOS) 「CreatePlatformEndpoint」で以下を入力して実行する applicationArn: arn:aws:sns:ap-northeast-1:949004901725:app/APNS_SANDBOX/Push-Test4-APNS-Dev deviceToken: (プッシュを送信したいiOSのデバイストークン) 「Publish」で以下を入力して実行する message: 任意のメッセージ(日本語可)を入力 targetArn: (プッシュを送信したいiOSのARN) ■トピックごとに送信 Simple Notification Service → トピック → 作成したトピックの名前をクリック この画面で、エンドポイントがカラであることを確認する 作成したPHPプログラムの index.php にアクセスし、 「Subscribe」でトピックのARNと追加したい端末のARNを入力して実行する 「Publish」で以下を入力して実行する message: 任意のメッセージ(日本語可)を入力 topicArn: (プッシュを送信したいAndroid端末のARN) ※トピック内に無効なエンドポイントが含まれていても、特にエラーなどは返してくれないみたい? 要検証 ■AmazonSNSから送信(デバッグ用) Simple Notification Service → プッシュ通知 → 作成したアプリケーションの名前をクリック 送信先にチェックを入れて「メッセージの発行」をクリック メッセージ構造: すべての配信プロトコルに同一のペイロード メッセージ: AmazonSNSから直接送信 「メッセージの発行」をクリックすると送信できる また、「メッセージ構造」を「配信プロトコルごとにカスタムペイロード」にすると、 メッセージに以下のようなテキストが表示される 「Sample message for iOS development endpoints」の部分を任意のメッセージに変更して送信でき、単純なメッセージ以外も送信できるようになる
{ "APNS_SANDBOX": "{\"aps\":{\"alert\":\"Sample message for iOS development endpoints\"}}" }
Simple Notification Service → トピック 送信先にチェックを入れて「メッセージの発行」をクリック とすれば、トピックに対しても送信できる
■iOS: 証明書の更新
iOSへプッシュを送信するための証明書は、一年に一回更新する必要がある 「iOS: 各証明書の作成」と「iOS: アプリにPHPからPushを送信」と行えば大丈夫のはず 本番公開前に、一度証明書の更新を行って手順をメモしておきたいところ AWS SNSのiOS向けPush通知(APNs)証明書の更新手順 - Qiita https://qiita.com/b_a_a_d_o/items/e3bf9cd52b6cd9252088
■AmazonSNS: 証明書の更新
更新したiOSの証明書をAmazonSNSに登録したときのメモ Simple Notification Service → プッシュ通知 → 対象のアプリケーションを選択 「Apple の証明書の有効期限」は「2020-02-15T09:53:15Z」となっている 「編集」をクリック 「プッシュ証明書タイプ」を「iOSプッシュ証明書」にする 「ファイルの選択」からp12ファイルをアップロードする アップロードできたら「認証情報をファイルから読み込み」をクリックし、証明書とプライベートキーが表示されることを確認する 「変更の保存」をクリック 「Apple の証明書の有効期限」が「2021-03-04T05:44:28Z」になった
■アプリからHTTPリクエストする例
上記の例ではデバイストークンをPHPに渡すために手動での作業が必要となる 実際のアプリでは、アプリ内からアプリからHTTPリクエストでPHPに渡すことになる 以下、アプリからHTTPリクエストする例 ■PHP test.php
<?php header('Content-Type: application/json; charset=utf-8'); echo json_encode(array( 'status' => 'OK', 'datetime' => date('Y/m/d H:i:s'), )); exit;
■Android(Kotlin) Kotlin: HTTP GET/POST サンプルコード(AsyncTask) | UBUNIFU INCORPORATED https://jp.ubunifu.co/development/kotlin-http-get-post-sample-code [Android] 非同期処理 AsyncTaskの使い方 https://akira-watson.com/android/asynctask.html [android開発] AndroidManifest.xmlの設定一覧(ネットワーク系) - 行け!偏差値40プログラマー http://hensa40.cutegirl.jp/archives/6501 まずは汎用HTTPリクエストクラスを作成する MainActivity.kt と同じ階層に HttpTask.kt を作成して以下を記述 パッケージ名はプロジェクトに合わせて調整する
package net.refirio.pushtest1 import android.os.AsyncTask import android.util.Log import java.io.* import java.net.HttpURLConnection import java.net.URL class HttpTask(callback: (String?) -> Unit) : AsyncTask<String, Unit, String>() { var callback = callback override fun doInBackground(vararg params: String): String? { val url = URL(params[1]) val httpClient = url.openConnection() as HttpURLConnection httpClient.setReadTimeout(10 * 1000) httpClient.setConnectTimeout(10 * 1000) httpClient.requestMethod = params[0] if (params[0] == "POST") { httpClient.instanceFollowRedirects = false httpClient.doOutput = true httpClient.doInput = true httpClient.useCaches = false httpClient.setRequestProperty("Content-Type", "application/json; charset=utf-8") } try { if (params[0] == "POST") { httpClient.connect() val os = httpClient.getOutputStream() val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8")) writer.write(params[2]) writer.flush() writer.close() os.close() } if (httpClient.responseCode == HttpURLConnection.HTTP_OK) { val stream = BufferedInputStream(httpClient.inputStream) val data: String = readStream(inputStream = stream) return data } else { Log.d("HttpTask", "ERROR: ${httpClient.responseCode}") } } catch (e: Exception) { e.printStackTrace() } finally { httpClient.disconnect() } return null } 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) } }
MainActivity.kt の onCreate 内に以下を記述
// HTTPリクエストのテスト HttpTask({ if (it == null) { Log.d("MainActivity", "Data is empty.") return@HttpTask } Log.d("MainActivity", "OK: ${it}") val json = JSONObject(it) val status = json.getString("status") val datetime = json.getString("datetime") Log.d("MainActivity", "status: ${status}") Log.d("MainActivity", "datetime: ${datetime}") }).execute("GET", "https://example.com/test.php")
インターネットにアクセスするため、AndroidManifest.xml に以下を追加 「<manifest>」の直下に追加すればいい
<uses-permission android:name="android.permission.INTERNET" />
■iOS(Kotlin) ViewController.swift の viewDidLoad 内に以下を記述
// HTTPリクエストのテスト if let url = URL(string: "https://example.com/test.php") { let request = URLRequest(url: url) let task = URLSession.shared.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data else { print("Data is empty.") return } do { let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) if let status = (json as AnyObject).object(forKey: "status") { print(status) } if let datetime = (json as AnyObject).object(forKey: "datetime") { print(datetime) } } catch { print("JSON parse error.") } print("Complete.") }) task.resume() }
■トラブル
■プッシュが届かない 以下などを確認する ・アプリのパッケージ名を確認し、それに対応する設定が行われているか ・Firebase、Apple Developer Program、AmazonSNS でそれぞれ設定内容が正しいか Androidの場合は以下も確認する ・USBデバッグでインストールしたか、APKを書き出してインストールしたか iOSの場合は以下も確認する ・Distribution証明書とDevelopment証明書、適切な方を使用しているか ・アカウントの有効期限切れ、証明書の有効期限切れになっていないか ・「Development SSL Certificate」で設定をしたのか「Production SSL Certificate」で設定したのか 後述の「送信サーバによってはプッシュが届かない」も参照 ■送信サーバによってはプッシュが届かない 開発環境から突然プッシュ通知を送れなくなった ただし、別サーバで同じプログラムを実行すると送れる Amazon SNS からのエラーメッセージを確認すると、以下のようになっていた
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 `403 Forbidden` response: <ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"> <Error> <Type>Sender</Type> <Code>SignatureDo (truncated...) SignatureDoesNotMatch (client): Signature expired: 20200129T081327Z is now earlier than 20200129T081427Z (20200129T082927Z - 15 min.) - <ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"> <Error> <Type>Sender</Type> <Code>SignatureDoesNotMatch</Code> <Message>Signature expired: 20200129T081327Z is now earlier than 20200129T081427Z (20200129T082927Z - 15 min.)</Message> </Error> <RequestId>b8fd243d-c596-5897-9820-72968131164b</RequestId> </ErrorResponse>
「20200129T081327Z is now earlier than 20200129T081427Z」となっている サーバの時間がおかしいみたい # date 2020年 1月 29日 水曜日 17:18:30 JST サーバの時間を確認すると、15分程度遅れていた # timedatectl set-timezone Asia/Tokyo # date 2020年 1月 29日 水曜日 17:19:20 JST タイムゾーンを再設定しても変化なし # chronyc -a makestep 200 OK chronyc で強制的に時間を調整してみる (chronyc がインストールされていなければインストールする) # date 2020年 1月 29日 水曜日 17:36:18 JST 時間がぴったりになった この状態ならAmazonSNSでプッシュが送れるようになった Centos7の時間がずれた - Qiita https://qiita.com/SwuBHj8aKGqBKHet/items/2f6a2003851420b460ba ■トークン管理 プッシュ送信先となるトークンは、ときどき変更されることがあるので注意 最新の登録情報を維持し続けるために、以下のような手段がある 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件以上取る場合は工夫が必要かも トークン管理のために 「無効になったトークンをすべて取得して何らかの処理をする」 とするよりも、上のリンク先で紹介されているように 「アプリ起動時に毎回トークンの有効/無効を確認する。無効ならトークンを差し替える」 とする方がいいかも
■考察: 本番公開用に作成する
■アプリのIDについて考察 ※アプリは本番用と開発用でパッケージ名を変える方が無難。1端末に両方インストールできるように ※アプリのIDは、アプリ名を初期として自動作成される。例えば「Push-Test1-Dev」というアプリ名で普通に作ると Android ... net.refirio.pushtest1dev iOS ... net.refirio.Push-Test1-Dev のようになる いったん手動で「pushtest1」というアプリ名を付け、 net.refirio.pushtest1 のパッケージ名で作成してから net.refirio.pushtest1 net.refirio.pushtest1.stg net.refirio.pushtest1.dev に切り替えられるように同プロジェクト内で最初に設定しておくといい (本番と開発でソースコードが別々になるのは、大変すぎるので避ける) ※切り替える仕組みはXcodeならSchemeを、AndroidStudioならFlavorを使うといい 可能ならiOSとAndroidの両方で統一したパッケージ名を使えるように調整するといい が、例えば net.refirio.android.pushtest1 と net.refirio.ios.pushtest1 などのように付けるのも一つの手段 詳細は AndroidStudio.txt と Xcode.txt の「製品用、開発用などの切り分け」を参照 以下、専用の仕組みがないか調べたときのメモ iPhoneアプリ開発でデバッグ版とリリース版をきれいに同居させる - しめ鯖日記 http://www.cl9.info/entry/2015/07/29/010020 iOS開発で環境ごとにアイコンやアプリ名、コード等を切り分けるオレオレプラクティス - Qiita https://qiita.com/KazaKago/items/2835d76ced43f913c31d 以下は Bundle ID を変更しているが、記事が古いのでSchemeでの変更ができなかったときの話かも 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 ※「Push-Test1」などの名前はすべて「Push-Test1-Dev」のようにしておく方がいいかも プッシュの証明書なども開発用と本番用が必要のため ■処理の流れについて考察 ※デバイストークンは非同期で取得されるので、 「アプリ起動時に端末情報を同期」と「アプリ起動時にデバイストークンを同期」の計2つのAPIが必要になりそう 以下、それに合わせてリライトしたい… ※ログインして使うアプリの設計メモ ※ユーザ情報のテーブルとデバイス情報のテーブルがある想定 ※「ログインしなくても基本機能は使える」「プッシュの送信を許可しなかった」も考慮する アプリ初回起動時 ... プッシュが許可されるとデバイストークンを取得できるようになる アプリからPHPに、デバイストークンを送る PHPがAmazonSNSから、エンドポイントを取得する PHPがDBに、デバイストークンとエンドポイントを記録する。さらに全端末でユニークな識別コードを作成し、合わせてDBに保存する PHPからアプリに、識別コードを返す。アプリはこの値を保存しておく アプリ次回起動時 ... アプリからPHPに、デバイストークンと識別コードを送る PHPがAmazonSNSから、エンドポイントを取得する デバイストークンやエンドポイントが変更されていれば、PHPがDBに、デバイストークンとエンドポイントを記録する(対象データは、識別コードをもとに判断する) PHPからアプリに、識別コードを返す(正常終了の判定などに使う) ログイン時 ... アプリ内でユーザ名とパスワードを入力し、その値をPHPに送る 認証情報が正しければ、データベーステーブルのデバイス情報をユーザ情報に紐付ける デバイス情報にはログイン中か否かのステータスも持たせておき、そのステータスをログイン中にする (厳密なログイン判定が不要なら、デバイス側に単純なログインフラグを持たせておくか。厳密なログイン判定が必要なら、ユーザ情報にも識別コードを持たせてそれをアプリ内に記録させ、アプリ起動時に毎回サーバ側でチェックするか) 以降のログインは、アプリ内に保存されている識別コードをもとに行う 別端末でログイン時 ... 上とまったく同じ流れでデバイストークンやエンドポイントを記録する 端末情報テーブルには、同じユーザIDのデータが別途作成される ログアウト時 ... アプリからPHPに、デバイストークンを投げてくる ログイン中か否かのステータスをログアウトにする ログアウトして別ユーザでログイン時 ... 「ログアウト時」の手順でログアウトし、「ログイン時」の手順でログインする プッシュ送信時 ... PHPがAmazonSNSを使って、エンドポイントに対してプッシュを送信する 1ユーザが複数の端末を持っていれば、それぞれにプッシュが送信される プッシュ拒否時 ... アプリ初回起動時にプッシュの送信を拒否した場合、デバイストークンの取得ができない この場合、アプリからPHPに「デバイストークンはカラ」という状態でデータを送る PHPからAmazonSNSにはアクセスしない PHPで全端末でユニークな識別コードを作成し、DBに保存する。デバイストークンとエンドポイントはNULLにしておく PHPからアプリに、識別コードを返す。アプリはこの値を保存しておく セッションタイムアウト時 ... デバイストークンはDBにあるので、それをもとにプッシュは届き続ける アプリを立ち上げると、その時点でデバイストークンの更新処理が走る ※iOSアップデートのタイミングでデバイストークンが変わる可能性があるらしい デバイストークンが変わるタイミングは不定らしいので、デバイストークンがいつ変わっても大丈夫な仕組みにする必要がある iOS 9からAPNsデバイストークンがアプリインストールの度に変わるようになったようです - Qiita https://qiita.com/mono0926/items/9ef83c8b0de0e84118ac ■現状考えている、設計のベストプラクティス ※デバイストークンは非同期で取得されるので、 「アプリ起動時に端末情報を同期」と「アプリ起動時にデバイストークンを同期」の計2つのAPIが必要になりそう 以下、それに合わせてリライトしたい… ※ログインしてもしなくても使えるアプリの設計メモ データベースの users テーブルに、ユーザのログイン情報(ユーザ名やパスワードなど)を記録する devices テーブルにデバイスごとの情報(デバイストークンやエンドポイントなど)を記録する ログインしたら、デバイスとユーザを紐付ける。具体的には devices には user_id を持っておき、users の id と紐付ける (複数アカウントへの同時ログインが必要なら、中間テーブルを使って多対多で紐付ける) 次回からは devices テーブル内の user_id を参照すればオートログインもできる アプリ初回起動直後に、最低限の情報(OS名、OSバージョン、アプリバージョン、ユーザエージェント、アプリ起動時点のIPアドレスなど)を devices にインサートする また、すべてのデバイスでユニークとなる識別コードも発行し、合わせて devices に記録しておく 戻り値としてアプリに識別コードを返し、その識別コードのみをアプリ側に保存しておく 原則としてこの識別コードは変更削除しない この時点ではデバイストークンは取得していないので保存しない 端末ごとの設定を持ちたければ devices のレコードに保存 ユーザ毎の設定を持ちたければ users のレコードに保存(もしくは profiles や configs など、users と対になる別テーブルも有効) ユーザごとの設定を持つためにはユーザ登録が必要、とする 設定はアプリ起動時にサーバから毎回取得し、端末内にキャッシュとして保存しておくか (インターネットに繋がっていなくても設定内容を参照できるように) アプリ初回起動直後の、上記情報の保存直後にプッシュを許可するかのダイアログを表示 プッシュを許可したら、デバイストークンを devices のレコードに上書き保存。デバイストークンの列にはUNIQUEの制約を付けておく PHP経由でAmazonSNSにデバイストークンを送り、エンドポイントを取得したらその devices のレコードに上書き保存 取得したエンドポイントは一斉配信用のトピックにも追加しておく アプリ起動時にはデバイストークンを毎回取得し devices のレコードに上書き エンドポイントもAmazonSNSから毎回取得し、変更があれば更新(トピックに追加したエンドポイントも更新) プッシュを拒否したら何もしない アプリ内の設定でプッシュを無効にしたら、devices テーブルにプッシュ送信除外の旨を記録し、その値をもとにプッシュ送信時には除外する。一斉配信用のトピックからも除外する アプリ内の設定でプッシュを有効にしたら、devices テーブルから除外の旨を削除し、一斉配信用のトピックにも再度追加する (OSの機能でアプリのプッシュを無効にした場合の挙動は「受け取るけど何もしない」となっているみたい。「無効→有効」を試して問題は無かった) ログアウトしたら devices の user_id はNULLにする アプリ起動のたびに devices のアプリバージョンなどは上書き更新する (プッシュを使用しない場合でも更新する。プッシュを使用する場合はさらにデバイストークンやエンドポイントも更新する) ■現状考えている、設計のベストプラクティス(テーブル設計を検討中)
CREATE TABLE IF NOT EXISTS users( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー', created DATETIME NOT NULL COMMENT '作成日時', modified DATETIME NOT NULL COMMENT '更新日時', deleted DATETIME COMMENT '削除日時', username VARCHAR(80) NOT NULL UNIQUE COMMENT 'ユーザ名', password VARCHAR(80) COMMENT 'パスワード', password_salt VARCHAR(80) COMMENT 'パスワードのソルト', email VARCHAR(255) NOT NULL UNIQUE COMMENT 'メールアドレス', loggedin DATETIME COMMENT '最終ログイン日時', failed INT UNSIGNED COMMENT 'ログイン失敗回数', failed_last DATETIME COMMENT '最終ログイン失敗日時', option1 VARCHAR(80) NOT NULL COMMENT 'ユーザごとの設定例1', option2 VARCHAR(80) NOT NULL COMMENT 'ユーザごとの設定例2', option3 VARCHAR(80) NOT NULL COMMENT 'ユーザごとの設定例3', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT 'ユーザ'; CREATE TABLE IF NOT EXISTS devices( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー', created DATETIME NOT NULL COMMENT '作成日時', modified DATETIME NOT NULL COMMENT '更新日時', deleted DATETIME COMMENT '削除日時', code INT UNSIGNED NOT NULL UNIQUE COMMENT 'コード(識別コード。代理キーを非可逆暗号化すれば、推測も重複も避けられそう)', useragent VARCHAR(255) NOT NULL COMMENT 'ユーザエージェント(あまり意味がないかもしれないが、一応記録しておくか)', platform VARCHAR(20) NOT NULL COMMENT 'プラットフォーム(例: ios / android)', os_version VARCHAR(80) NOT NULL COMMENT 'OSバージョン(例: 13.2.3)', app_version VARCHAR(80) NOT NULL COMMENT 'アプリバージョン(例: 1.0.2)', ip VARCHAR(80) NOT NULL COMMENT '最終IPアドレス', token VARCHAR(255) UNIQUE COMMENT 'デバイストークン', endpoint VARCHAR(255) COMMENT 'エンドポイント', option1 VARCHAR(80) NOT NULL COMMENT 'デバイスごとの設定例1', option2 VARCHAR(80) NOT NULL COMMENT 'デバイスごとの設定例2', option3 VARCHAR(80) NOT NULL COMMENT 'デバイスごとの設定例3', user_id INT UNSIGNED NOT NULL COMMENT '外部キー ユーザ', loggedin DATETIME COMMENT '最終ログイン日時', loggedout DATETIME COMMENT '最終ログアウト日時', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT 'デバイス';
■現状考えている、設計のベストプラクティス(以下は要検討) アプリ起動時にインターネットに繋がっていなければどうするか アプリの起動自体ができない…などは避けたい サーバ上の設定を取得する必要があるなら、その内容はアプリ内に保持しておいて起動時に毎回更新するか アプリ内の設定でプッシュの有効/無効を切り替えたとき、実装内容によっては一斉配信用のトピックからも追加/除外する必要がある 何かしらの通信エラーでデータベース内の値とトピックの値が異なると問題なので、データベースのロールバックなどを活用する 「データベースから削除 → トピックから削除 → 正常終了ならコミット/そうでなければロールバック」 で対応できるか アプリデータを初期化して起動した場合など、同じデバイストークンがすでにデータベースに登録されている可能性はある プッシュが重複して何度も届かないように、初回起動の新規登録時は「すでに同じデバイストークンのデータがあれば、そのデバイストークンはNULLにする。user_idもNULLにする」としておく 仮に「すでに同じデバイストークンのデータがあれば、そのデータを自身のデータとして紐付ける」をすると、「アプリを初期化したのに何故かデータが復元される」となるので避ける ログインした状態でアプリの設定をリセットしたら、サーバサイドでは「ログイン中」の情報が残ってしまうが、 次回初回起動の新規登録時、古いデータのデバイストークンとuser_idにはNULLが登録されるので大丈夫のはず お知らせの未読数をアプリのアイコンに表示する場合、既読数もしくは未読数をサーバサイドで管理する必要がある (プッシュを送信する際に、「アイコンの数字には何を表示するか」を指定する必要があるため) が、それだとトピックへの一斉配信が使えない(同一の内容を送信するため、ユーザごとの既読数などは送れない) プッシュを受け取ってからアプリ側で数字をインクリメントさせる必要があるが、通常のプッシュだとバックグラウンド時に受信したときの処理を指定できない サイレントプッシュを使えば一応は可能だが、バッジのためだけに常にサイレントプッシュを使うのもイマイチか 「何かしらのお知らせがあれば1と表示する」のように簡易な実装にする方が無難そう iOSのサイレントプッシュを試してみる - しめ鯖日記 http://www.cl9.info/entry/2017/10/14/145342 Swiftでサイレントプッシュを送る - ニフクラ mobile backend(mBaaS)お役立ちブログ https://blog.mbaas.nifcloud.com/entry/2018/11/06/144238 iOS 13以上でサイレントプッシュのdidReceiveRemoteNotificationが呼ばれない問題 - Qiita https://qiita.com/knagauchi/items/64dd9d57f123a24a4e14
■考察: 本番公開用に作成する(上にまとめる前のメモ)
■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 パッケージ名: net.refirio.zabbix.dev アプリのニックネーム: Zabbix Dev Firebaseで開発用に、プロジェクトではなくアプリを追加する Android パッケージ名: net.refirio.zabbix.dev アプリのニックネーム: Zabbix Dev ダウンロードした google-services.json の内容を確認すると、本番用と開発用の記述両方があった Firebase SDK の追加作業は済んでいるので飛ばす その状態で developDebug 版に切り替えてみるとエラーは出なかった アプリを実行してインストールを確認すると「正常に追加されました」が表示された Firebaseのコンソールからサーバキーを確認すると本番と開発で共通になっていたので、この値を変更する必要はない アプリ内でのPHPの通信先調整(本番と開発)は必要 これで本番と開発版を端末に共存させたうえで、プッシュもそれぞれに送信できる 1つのソースコードでPHPへの送信先を分ける必要があるので、例えば以下のようにして送信先を振り分ける 実際はこの手の記述は、設定ファイルなどにまとめて記述すると良さそう
var target = "" if ("production".equals(BuildConfig.FLAVOR)) { target = "https://example.com/tool/zabbix/prod/api.php" } else { target = "https://example.com/tool/zabbix/dev/api.php" } Log.d("TARGET", target)
同じ手順で net.refirio.zabbix net.refirio.zabbix.dev net.refirio.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 | net.refirio.buildtest1.dev Develop_Release | net.refirio.buildtest1.dev Production_Release | net.refirio.buildtest1 アプリケーション名を以下のように設定してみた Develop_Debug | buildtest1 Dev Develop_Release | buildtest1 Dev Production_Release | buildtest1 開発時は net.refirio.zabbix.dev が実機にインストールされるので、この「Development 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」などは使うことが無いか
■その他メモ
■プッシュ設定時の挙動 プッシュ通知設定画面へ遷移させる為の実装 - Qiita https://qiita.com/yamataku29/items/5361d7c3146604dcca44 ■プッシュ受診時の挙動 Androidでプッシュ受信時にダイアログを表示できるか ■メインスレッドで処理を実行 [Swift] MainThreadで処理を実行する - Qiita https://qiita.com/valmet/items/6de0921ca6106414228c ■プッシュ通知の受診時にダイアログを表示する(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 ■サイレントプッシュ 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',」もコメントアウトすると、プッシュ受診時のサウンドやバイブレーションも再生されない この仕組を使えば「サーバからの指示によって、裏側でこっそり任意の処理を行わせる」ができそう ただし当然ながら、プッシュが許可されていなかったりオフラインになっていたりすると実行できないので注意 ■iOS用プッシュの証明書 ※未検証 証明書入手先 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 で、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 大規模ネイティブアプリへのプッシュ通知機能導入にあたって考えたこと - Qiita https://qiita.com/gomi_ningen/items/ab31aa2b3d46bb6ffa5e [AWS][iOS] Amazon SNS で APNs に大量 Publish してみた http://dev.classmethod.jp/cloud/aws/sns-apns-push/