Memo

メモ > 技術 > サービス: AmazonSNS

概要・前提・注意点など
■概要 AndroidとiOSにプッシュ通知を送信するためのメモ。 AndroidとiOSではプッシュ通知のためのプログラムは変わるものの、基本的な流れは同じ。 端末の「デバイストークン」とアプリの「バンドルID」をもとに、送信対象の「端末」と端末にインストールされている「アプリ」を特定する。 プッシュ通知の配信は、Androidでは「FCM(Firebase Cloud Messaging)」を、iOSでは「APNs(Apple Push Notification Service)」を使用し、 GoogleやAppleが提供しているAPIをプログラムから叩くことで実現する。 大量のプッシュ通知を安定して配信するのは難しい部分が多い。 よって配信は、最終的にAmazonSNSに任せるといい。 今回は、 1. Android単体でプッシュ通知を実装する。 2. iOS単体でプッシュ通知を実装する。 3. AmazonSNSから、AndroidとiOSの両方にプッシュ通知を送信する。 という流れで実装する。 ■主な参考ページ AWS SNSからAndroidにプッシュ通知するためにやったこと、ハマったこと - Qiita https://qiita.com/nashitake/items/4724527c5c6fef4427d2 Android から Amazon SNS を使ってみる - Qiita https://qiita.com/kusokamayarou/items/27e023ad06cade20c731 AndroidのPush通知の導入方法 - Qiita https://qiita.com/YusukeYamazaki/items/9394234089b776bcb320 [2019年度版]AWS SNSでiOSのプッシュ通知 設定手順まとめ | カフーブログ https://kahoo.blog/howto-aws-sns-ios-push-notification/ AWS SDK for PHP を用いた Amazon SNS の操作 - Qiita https://qiita.com/gomi_ningen/items/1002d256285c6d72c6ac Amazon SNSでプッシュ通知を送るための基礎知識 | UNITRUST https://www.unitrust.co.jp/6182 以下は後発の記事なので、そのうち参考にしたい。 AmazonSNSを使ったpush通知の実装について #AWS - Qiita https://qiita.com/Dai_Kentaro/items/5b17272207c66adcdbe4 ■アプリのID ※iOSアプリではAppIDに、net.refirio.* のようなワイルドカードを利用することができ、複数のバンドルIDを作成する必要がなくなる。 ただしこの場合、バンドルIDをもとにアプリを特定する必要がある機能を利用できない。 プッシュ通知やアプリ内課金がこれにあたるので、今回はワイルドカードのまま使わない。 いったんアプリを公開すると変更できないので、慎重に決定したい。 「iOSとAndroidの両方で作る」「本番用と検収用と開発用がある」「Pushも使用する」などを考慮する。 現状の結論として、以下のようにするのが良さそう。 ・iOSもAndroidも net.refirio.pushtest1 のようなIDにする。 ・例えば開発版書き出し時には .dev を付ける。 つまり、具体的には以下のようなIDになる。 net.refirio.pushtest1 … 本番 net.refirio.pushtest1.stg … 検収 net.refirio.pushtest1.dev … 開発(ローカル環境など) iOSとAndroidでIDを統一できるように、「pushtest1」部分にハイフンやアンダーバーは無い方が無難か。 (と思ったが、使える文字も異なるので無理にIDを統一する必要は無いかもしれない。 ただしURLや各種サービスのIDは、普段からできるだけ統一しておきたいので悩ましいところ。) 詳細はこのテキストの「考察: 本番公開用に作成する」も参考に。 ■iOSのp8証明書 iOSではプッシュ通知の送信に証明書が必要になる。 p8証明書を使えば、証明書の定期的な更新が不要になる。 詳細は後述の「iOS: p8証明書」を参照。
環境の確認
サーバサイドの環境は、通常のLAMPでいい。 AmazonSNSのためにAWSのSDKを使用するので、PHPはその時点での最新版を使うことが推奨される。 DockerやVagrantで環境を構築することも可能。 ただしアプリからPHPにアクセスさせたい場合、当然ながら同一LAN内の他端末からアクセスできるようにしておく必要がある。 curlコマンドやPHPで通信を行うため、まずは環境を確認する。 PHP curlでHTTP/2リクエストを実行するための設定 on CentOS 7 | 稲葉サーバーデザイン https://inaba-serverdesign.jp/blog/20171011/php_curl_http2_centos7.html Amazon Linux 2 なら特別な更新作業なしにPHP+curlでHTTP/2リクエストを送信できた。 以下は2021年9月に構築した 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.4.21 (cli) (built: Jul 7 2021 17:35:08) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies $ curl --version curl 7.76.1 (x86_64-koji-linux-gnu) libcurl/7.76.1 OpenSSL/1.0.2k-fips zlib/1.2.7 libidn2/2.3.0 libssh2/1.4.3 nghttp2/1.41.0 Release-Date: 2021-04-14 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS GSS-API HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz Metalink NTLM NTLM_WB SPNEGO SSL UnixSockets $ php -r 'phpinfo();' | grep SSL SSL => Yes MULTI_SSL => No SSL Version => OpenSSL/1.0.2k-fips 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 OpenSSL => Stig Venaas, Wez Furlong, Sascha Kettler, Scott MacVicar $ curl -vso /dev/null --http2 https://www.google.co.jp/ * Trying 142.250.207.3:443... * Connected to www.google.co.jp (142.250.207.3) 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): { [4009 bytes data] * TLSv1.2 (IN), TLS handshake, Server key exchange (12): { [149 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: CN=*.google.co.jp * start date: Aug 14 08:23:49 2023 GMT * expire date: Nov 6 08:23:48 2023 GMT * subjectAltName: host "www.google.co.jp" matched cert's "*.google.co.jp" * issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1C3 * 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 0xc26600) } [5 bytes data] > GET / HTTP/2 > Host: www.google.co.jp > user-agent: curl/7.76.1 > accept: */* > { [5 bytes data] < HTTP/2 200 < date: Fri, 15 Sep 2023 02:02:38 GMT < expires: -1 < cache-control: private, max-age=0 < content-type: text/html; charset=Shift_JIS < content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-DzUUpbjjm6zwKLvJsasAFA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp < 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=2023-09-15-02; expires=Sun, 15-Oct-2023 02:02:38 GMT; path=/; domain=.google.co.jp; Secure < set-cookie: AEC=Ad49MVHajKAOpVrRPnIwK-msTAiUwUdGNpMKT8SiU444GjxwuDuApwL2r1s; expires=Wed, 13-Mar-2024 02:02:38 GMT; path=/; domain=.google.co.jp; Secure; HttpOnly; SameSite=lax < set-cookie: NID=511=Mo9ACzb5BLQ6s--enshWZGJ_aJR9z59-J1ozzZNJWiSssDdTS7aihWx6tURtjsq3eGVjb_dk9RjUyAIEg_9R2R5wU5pxOJ8LNElZliyyHn8WNYgt7gk0Tc53ytT3dTvVGm6mrFv9GLgQoqP8bl9NX90MEIQKDpIh8QW4fSGWmEI; expires=Sat, 16-Mar-2024 02:02:38 GMT; path=/; domain=.google.co.jp; HttpOnly < alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 < accept-ranges: none < vary: Accept-Encoding < { [5 bytes data] * Connection #0 to host www.google.co.jp left intact
PHPプログラムからもcurlコマンドを実行できることを確認しておく。 (ファイルの文字コードは UTF-8N にする。)
$ cat 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 142.250.207.3:443... * Connected to www.google.co.jp (142.250.207.3) 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: CN=*.google.co.jp * start date: Aug 14 08:23:49 2023 GMT * expire date: Nov 6 08:23:48 2023 GMT * issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1C3 * 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 0x5647b92d6360) > GET / HTTP/2 Host: www.google.co.jp accept: */* < HTTP/2 200 < date: Fri, 15 Sep 2023 02:05:51 GMT < expires: -1 < cache-control: private, max-age=0 < content-type: text/html; charset=Shift_JIS < content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-24lYDgFlIXzIPGj39wOGtg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp < 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=2023-09-15-02; expires=Sun, 15-Oct-2023 02:05:51 GMT; path=/; domain=.google.co.jp; Secure < set-cookie: AEC=Ad49MVF7GxHzcpVDA9KugVDkq5bfulVFD1MIxyXAX1eiBjjLbYVtTJQSbbg; expires=Wed, 13-Mar-2024 02:05:51 GMT; path=/; domain=.google.co.jp; Secure; HttpOnly; SameSite=lax < set-cookie: NID=511=FIn9mttV5kF9-t52WJGMQ0HJ-Xv6yfwXkFsqSvpBpk0Zg5dSu352hgAPCiCUQwjogG8WOrHCjB7Z3R3dp7fBl8CSYlqJjMWYzPOR0tfzluNlpB_8fNyLBH-6WBN1x3W97kormoZ0U8BGfcwLR9Yc_vQsAxPrktYJOYbWpQHC2Ew; expires=Sat, 16-Mar-2024 02:05:51 GMT; path=/; domain=.google.co.jp; HttpOnly < alt-svc: h3=":443"; ma=2592000,h3-29=":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
Android: アプリの作成
※いったんプッシュ通知機能の無い状態でアプリを作る。 Android Studioで「New Project」をクリック。 ↓ 「Empty Activity」が選択されているので、そのまま「Next」。 ↓ Name: PushTest1 Package name: net.refirio.pushtest1 (名前をもとに自動入力される。) Save location: C:\Users\refirio\AndroidStudioProjects\PushTest1 (アプリケーション名をもとに自動入力される。) Mininum SDK: API 26: Android 8.0 (Oreo)(案件のターゲット層に応じて調整する。) 入力したら「Finish」。 エミュレータと実機で、アプリを起動できることを確認する。
Android: Firebaseの設定
■Firebaseプロジェクトの作成 公式サイト https://firebase.google.com/ コンソール https://console.firebase.google.com/ Firebaseのコンソールにアクセスし、「プロジェクトを追加」をクリック。 プロジェクト名: PushTest1 プロジェクトID: pushtest1-aeb26(プロジェクト名から自動で入力されるが、後から判りやすいように編集し直してもいい。) 「続行」をクリック。 「このプロジェクトで Google アナリティクスを有効にする」にチェックが入っていることを確認する。 「続行」をクリック。 アナリティクス用に既存のアカウントを選択するか、新しいアカウントを作成する。 (今回は開発用である「refirio」を選択した。「このアカウントに新しいプロパティを自動的に作成します」と表示された。) 「プロジェクトを作成」をクリック。 しばらく待つと「新しいプロジェクトの準備ができました」と表示される。 「続行」をクリック。 「アプリに Firebase を追加して利用を開始しましょう」という画面へ遷移する。 「開始するにはアプリを追加してください」の文言の上にある、Androidのアイコンをクリック。 「Android アプリに Firebase を追加」という画面へ遷移する。 ※後から確認すると、アナリティクス側で「refirio」内に「pushtest1-aeb26」というプロパティが作成されていた。 またアプリ側で何かプログラムを書いたわけでも無いのに、「ユーザー」「イベント数」に反応がある。 最低限(アクティビティ単位でのアクセス数?)のことは自動で計測されているのか。 ■Firebaseアプリの作成 「1 アプリの登録」という画面になる。 Android パッケージ名: net.refirio.pushtest1 アプリのニックネーム: PushTest1 デバッグ用の署名証明書 SHA-1: (空欄) ※まずはニックネーム「PushTest1 Dev」で開発版用に作るか…と思ったが、重複してAndroidパッケージ名を登録することができない。 つまり「net.refirio.pushtest1」用に作るのは「PushTest1」であり、これは本番環境用に使うべきものとなる。 iOSでは最初に開発版用にプッシュ通知証明書を作成しているが、これは「パッケージ名は本番用と同じだが、開発用に『Development SSL Certificate』を登録できる」なので、Androidとは少し性格が違うものだと思われる。 ※後述の「考察: アプリのIDについて」も参照。 基本的には、プロジェクト自体を分ける方が良さそうではある。 ※プロジェクト名には「android」という文字を含めておく方がいいか。 つまり「pushtest1-android」「pushtest1-android-stg」「pushtest1-android-dev」のような名前にするか。 ただし今回のように「iOSではFirebaseを使わない」「最終的にはAmazonSESから送信する」という場合、「android」という文字を含めなくても支障は無い。 FirebaseはGoogleのサービスなので、「もしiOS用に作ることがあれば『-ios』という接尾語を付ける」でいいかもしれない。 ※某会社アプリの場合、「xxxxx」プロジェクトの中に「xxxxx-android」「xxxxx-android-stg」「xxxxx-android-dev」がある。 ただし、その後に作った別アプリは 「yyyyy」プロジェクトの中に「yyyyy」「yyyyy-dev」があり、 「yyyyy-staging」プロジェクトの中に「yyyyy」「yyyyy-dev」があり、 となっている。 上記を入力して「アプリを登録」をクリック。 「2 構成ファイルをダウンロードして追加する」という画面になる。 「google-services.json をダウンロード」をクリック。 表示される解説どおり、「Androidプロジェクトの新規作成」で作成したアプリの「app」直下にファイルを配置する。 (Android Studio で「Android」ではなく「Project」に表示を切り替え、「app」フォルダアイコンに対してドラッグ&ドロップで配置する。その後、また「Android」に戻しておく。) 配置できたら「次へ」をクリック。 「3 Firebase SDK の追加」という画面になる。 「Groovy(build.gradle)」を選択する。 プロジェクト直下の build.gradle と、「app」直下の build.gradle に指定のコードを追加する。 追加したら、AndroidStudioの画面上部に「sync now」が表示されるのでクリックする。 ビルドが完了されるまで待つ。完了したら「次へ」をクリック。 以上で完了。 「コンソールに進む」をクリック。 ※同じプロジェクト内に同じアプリで再作成する場合、 「プロジェクトの設定 → PushTest1 → SDKの手順を確認する」から作業できる。 ■参考 プロジェクト直下の build.gradle と、「app」直下の build.gradle に追加したコードは以下のとおり。 プロジェクト直下の build.gradle:
plugins { 〜略〜 id("com.google.gms.google-services") version "4.4.2" apply false … 追加 }
「app」直下の build.gradle:
plugins { 〜略〜 id("com.android.application") … 追加 id("com.google.gms.google-services") … 追加 } dependencies { 〜略〜 implementation(platform("com.google.firebase:firebase-bom:33.5.0")) … 追加 implementation("com.google.firebase:firebase-analytics") … 追加
「id("com.android.application")」というのも記載されているが、これは「alias(libs.plugins.android.application)」があるので追加不要みたい。 (追加すると「すでに読み込まれている」のエラーになる。) なお、ダウンロードした google-services.json には以下のような内容が記載されている。
{ "project_info": { "project_number": "884881935685", "project_id": "pushtest1-aeb26", "storage_bucket": "pushtest1-aeb26.appspot.com" }, "client": [ { "client_info": { "mobilesdk_app_id": "1:884881935685:android:cc453484c4825b2153022c", "android_client_info": { "package_name": "net.refirio.pushtest1" } }, "oauth_client": [], "api_key": [ { "current_key": "AIzaSyDYzY_QZPAGTpm20Pa69ExMWrNbCQukz1o" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } } ], "configuration_version": "1" }
Android: アプリにPush通知受信機能を実装
■app/build.gradle
dependencies { 〜略〜 implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-messaging") … 追加
「firebase-messaging」の最新バージョンは以下で確認できるが、今は常に「com.google.firebase:firebase-messaging-ktx」で問題無さそう。(バージョン情報以外にも、参考にできそうな情報がある。) ただし2024年10月時点で確認すると、デフォルトで「com.google.firebase:firebase-analytics-ktx」ではなく「com.google.firebase:firebase-analytics」が使われるようになっている。 これに合わせて、今回は「com.google.firebase:firebase-messaging-ktx」ではなく「com.google.firebase:firebase-messaging」にしている。 Android プロジェクトに Firebase を追加する | Firebase for Android https://firebase.google.com/docs/android/setup?hl=ja 追加したら、AndroidStudioの画面上部に「sync now」が表示されるのでクリックする。 念のため、この時点でアプリを起動できることを確認しておく。 ■res/drawable/ic_notification.webp プッシュ通知用のアイコンを作成する。 今回は res/mipmap-mdpi/ic_launcher.webp を複製して名前を変更した。 ★アイコンの形式やサイズについて、何が適切かは確認しておきたい。 また、アプリが起動中でもそうでなくても、プッシュ通知にアイコンが反映されるか確認する。 ■res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> 〜略〜 <color name="notification">#AAAAAA</color> … 追加 </resources>
※この色指定は必須のものか。 テキスト色を指定していないので、「ダークモードで表示されない」などは無いか。 省略してデフォルトの色を使ってくれるなら、それが良さそうだが。 …と思ったが、このアイコンと色の指定は、上記のとおりデフォルト値として指定しておくことが推奨されるらしい。 詳細は以下の記事を参照。 Android アプリでメッセージを受信する | Firebase Cloud Messaging https://firebase.google.com/docs/cloud-messaging/android/receive?hl=ja > 通知のデザインをカスタマイズするためのデフォルト値を設定することもおすすめします > 通知ペイロード内にアイコンやカラーの値が設定されていない場合に適用される、カスタム デフォルト アイコンとカスタム デフォルト カラーを指定できます。 > > Android では、以下に対してカスタム デフォルト アイコンが表示されます。 > ・Notifications Composer から送信されたすべての通知メッセージ。 > ・通知ペイロード内にアイコンが明示的に設定されていない通知メッセージ。 > > Android では、以下に対してカスタム デフォルト カラーが使用されます。 > ・Notifications Composer から送信されたすべての通知メッセージ。 > ・通知ペイロード内にカラーが明示的に設定されていない通知メッセージ。 ■app\src\main\AndroidManifest.xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> … Android13用に追加 <application android:allowBackup="true" 〜略〜 tools:targetApi="31"> <meta-data … ブロックを追加 android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification" /> <service … ブロックを追加 android:name=".MyFirebaseMessagingService" android:exported="true"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> <activity android:name=".MainActivity"
■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.util.Log import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) Log.d("MyFirebaseMsgService", "showNotification Data Default: " + remoteMessage.data["default"]) // 通知の内容を取得して表示する showNotification("通知タイトル", "通知メッセージ") //showNotification("PushTest1", remoteMessage.data["default"]) } private fun showNotification(title: String?, body: String?) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channelId = "notification" val channel = NotificationChannel( channelId, "通知", NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(channel) val intent = Intent(this, MainActivity::class.java) .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } val pendingIntent = PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE ) val notificationBuilder = NotificationCompat.Builder(this, channelId) .setContentTitle(title) .setContentText(body) .setSmallIcon(R.drawable.ic_notification) // 通知アイコン .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) // 通知をタップしたときの表示先 .setAutoCancel(true) // 通知をタップしたら消去 Log.d("MyFirebaseMsgService", "showNotification Title: $title") Log.d("MyFirebaseMsgService", "showNotification Body: $body") notificationManager.notify(0, notificationBuilder.build()) } }
■app\src\main\java\net\refirio\pushtest1\MainActivity.kt
package net.refirio.pushtest1 import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat import com.google.firebase.messaging.FirebaseMessaging import net.refirio.pushtest1.ui.theme.PushTest1Theme class MainActivity : ComponentActivity() { private var tokenState: MutableState<String?>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PushTest1Theme { val localTokenState = remember { mutableStateOf<String?>(null) } tokenState = localTokenState Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MainScreen(modifier = Modifier.padding(innerPadding), localTokenState) } } } // Firebaseのトークンを取得 FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w("MainActivity", "Fetching FCM registration token failed", task.exception) return@addOnCompleteListener } // トークンを取得 val token = task.result Log.d("MainActivity", "FCM Token: $token") tokenState?.value = token } } } fun checkAndRequestPermissions( context: Context, permissions: Array<String>, launcher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>> ) { if ( permissions.all { ContextCompat.checkSelfPermission( context, it ) == PackageManager.PERMISSION_GRANTED } ) { // パーミッションが与えられている Log.d("MainActivity", "Already granted") } else { // パーミッションを要求 launcher.launch(permissions) } } @Composable fun MainScreen(modifier: Modifier = Modifier, tokenState: MutableState<String?>) { val context = LocalContext.current val permissions = arrayOf( Manifest.permission.POST_NOTIFICATIONS ) val launcherMultiplePermissions = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissionsMap -> val areGranted = permissionsMap.values.reduce { acc, next -> acc && next } if (areGranted) { // パーミッションが与えられている Log.d("MainActivity", "Already granted") } else { // パーミッションを要求 Log.d("MainActivity", "Show dialog") } } Column(modifier = modifier) { Text("FCM Token: ${tokenState.value ?: "Loading..."}") Button( onClick = { checkAndRequestPermissions( context, permissions, launcherMultiplePermissions ) } ) { Text("通知を許可(Android13用)") } } } @Preview(showBackground = true) @Composable fun MainScreenPreview() { var tokenState: MutableState<String?>? = null val localTokenState = remember { mutableStateOf<String?>(null) } tokenState = localTokenState MainScreen(modifier = Modifier, localTokenState) }
※実際の実装では、トークンを画面に表示することなく「デバイス内に保存してサーバにも投げる」としておけば良さそう。 後述の「アプリ起動時にデバイストークンをサーバ側に記録」も参照。 以下を参考にした。 2 Ways to Request Permissions in Jetpack Compose | by Igor Stevanovic | Better Programming https://betterprogramming.pub/jetpack-compose-request-permissions-in-two-ways-fd81c4a702c 以下も参考にできるかもしれない。 AWS SNSからAndroidにプッシュ通知するためにやったこと、ハマったこと - Qiita https://qiita.com/nashitake/items/4724527c5c6fef4427d2 Android 13で導入されるNotification runtime permissionについて調べてみた - Money Forward Developers Blog https://moneyforward-dev.jp/entry/2022/04/11/android13-notification-runtime-permission/ 【Androidで通知を出すために】通知の権限回りを整える https://zenn.dev/tbsten/articles/droid-notification-permission 画面を表示したら通知の許可を求める…は、以下を参考にできそう。(未検証。) Composableが表示されたタイミングでコルーチンを起動する(LaunchedEffect) | mokelab tech sheets https://tech.mokelab.com/android/compose/effect/LaunchedEffect.html ■動作確認 アプリを実行すると、画面にデバイストークンが表示される。 また、ログにもデバイストークンが表示されるようにしている。 以下は実際に表示された値。
2024-10-24 18:54:27.967 13887-13887 MainActivity net.refirio.pushtest1 D FCM Token: envzgXQ9Tm0000000000-K:APA91bFp66znOyBVMEkFCBRXhh-ssW2iQeDnXXKDGy-Ey-cN-T9FlaXFk0eKHlvfeYtoOYQjhqFN3WayxCtFwhZnOm48Mx99LIiXD2x7YQwZ8wwYw-X756tTgRAB1QvGDbdZBcp7_x69
この文字列をもとに、サーバサイドプログラムからプッシュ通知を送信する。 具体的な送信方法は引き続き後述する。 ※ログに表示されない場合、Logcatの左上で対象のデバイスが選択されているか確認する。
Android: アプリにFirebaseからPushを送信
Firebaseでプロジェクトのページを開く。 左メニュー → 実行 → Messaging → 新しいキャンペーンを送信 → 通知 通知のタイトル: テストのタイトル 通知テキスト: テストのテキスト ターゲット: ユーザーセグメント ターゲットとするユーザー: アプリ net.refirio.pushtest1 スケジュール: 現在 その他必要に応じて設定し、「確認」をクリックすると確認画面が表示されるので、「公開」をクリックすると送信できる。 数分間待っていると、以下のログが表示された。
2023-09-15 19:18:09.706 5883-8376 MyFirebaseMsgService net.refirio.pushtest1 D showNotification Title: プッシュ通知テスト 2023-09-15 19:18:09.706 5883-8376 MyFirebaseMsgService net.refirio.pushtest1 D showNotification Body: null
なお、この時点ではタイトルが「通知タイトル」、本文が「通知メッセージ」という内容で通知が表示される。 (ソースコード内で文言を固定しているため。) ※届くまで5分ほどかかるみたい?AmazonSNS経由なら比較的早く届くみたい? AmazonSNSが色々良い感じに処理してくれているのかもしれない。 最終的にはAmazonSNSから送信するので、ここでは「時間はかかるが届くようだ」くらいまでの確認でいいか。 Jetpack Compose で Permission を要求する方法 https://zenn.dev/kaleidot725/articles/2021-11-13-jc-permission
Android: Firebaseの鍵ファイルを取得
■鍵ファイルとサーバーキーについて Firebaseで対象のプロジェクトで、「プロジェクトの概要」の隣にある歯車アイコンから「プロジェクトの設定」をクリック。 設定画面に遷移するので、タブメニューから「Cloud Messaging」をクリック。 ここで「サーバーキー」の値を確認する…だったが、「Cloud Messaging API(レガシー)」と表示されて「無効」になっている。 これは、AWSから「[No Action Required] Reminder to register SMS Sender ID for Singapore」という件名で告知があったもの。 > このメッセージを受信しているのは、過去90日間にAmazon SNSを使用してアカウントから1つ以上のモバイルプッシュ通知を送信したためです。 > 2023年6月21日、Google は、レガシー Firebase Cloud Messaging (FCM) API を使用しているアプリケーションは、2024年6月20日以降、モバイルプッシュ通知を送信できなくなると発表しました。 > 弊社では、このお知らせを承知しており、Amazon SNS経由での通知送信機能が中断されないよう、代替手段の提供に取り組んでおります。 > 新しいFCM APIを使用できるようになりましたら、ご連絡いたします。 2024年6月1日以降は、サーバキーでのプッシュ通知送信ができなくなる。 サーバキーではなく鍵ファイルを発行して、それをもとにGoogle_clientを使って送信する…という方式に変更する必要がある。 ただし送信プログラムも複雑になるため、可能ならAmazonSNSを使っての送信にすると良さそう。 [アップデート] Amazon SNS の FCM を使ったプッシュ通知がトークンベースの HTTP v1 API をサポートしたので、レガシー FCM API から移行してみた | DevelopersIO https://dev.classmethod.jp/articles/sns-fcm-http-v1-api-mobile-notifications/ ■鍵ファイルの発行 [アップデート] Amazon SNS の FCM を使ったプッシュ通知がトークンベースの HTTP v1 API をサポートしたので、レガシー FCM API から移行してみた | DevelopersIO https://dev.classmethod.jp/articles/sns-fcm-http-v1-api-mobile-notifications/ Firebaseで対象プロジェクト → プロジェクトの設定 → Cloud Messaging 「Cloud Messaging API」にサーバーキーが表示されていることを確認できる。 「Firebase Cloud Messaging API」には「サービス アカウントの管理」というリンクだけが表示されているのでクリック。 「プロジェクト「PushTest1」のサービス アカウント」という画面に遷移する。 一覧で「Firebase Admin SDK Service Agent」に対して「キーがありません」と表示されている列があるので、「… → 鍵を管理」をクリック。 鍵の作成画面に遷移するので、「鍵を追加 → 新しい鍵を作成」をクリック。 キーのタイプとして「JSON」と「P12」を選択できるが、今回は「JSON」を洗濯して「作成」をクリック。 pushtest1-aeb26-c00a0b10addc.json というファイルがダウンロードされた。 内容は以下のとおり。
{ "type": "service_account", "project_id": "pushtest1-aeb26", "private_key_id": "c00a0b10ad00000000000000000000baebe01a4f", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnoXNCLluQ4ikO\nY7mxImNS9ft4LFWCXDUqkPajWgMYwJzyLd〜中略〜hx0RWLhN9\n7mZwLUZfLvTKeCalOeDx61eCn4cnHRhpIukGzV4NdF1KwD+v3Jpo2Ot74/pGwSWl\nKUYqj6EYTMsXropj5TFXGg0=\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-ik32h@pushtest1-aeb26.iam.gserviceaccount.com", "client_id": "101260000000000064216", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-ik32h%40pushtest1-aeb26.iam.gserviceaccount.com", "universe_domain": "googleapis.com" }
なお鍵を作成した後も、先の「Firebase Cloud Messaging API」画面では「サービス アカウントの管理」というリンクだけが表示されている状態だった。 ■専用ツールから通知の送信 Firebase Cloud Messaging API(V1)でのPushテストの方法 #Android - Qiita https://qiita.com/ko2ic/items/d4001a3d246b7146e838 上記ページを参考に試す。 まずは以下にアクセスする。 OAuth 2.0 Playground https://developers.google.com/oauthplayground/ 「OAuth 2.0 Playground」が表示される。 左の一覧から「Firebase Cloud Messaging API v1」をクリックし、 さらに表示される「https://www.googleapis.com/auth/cloud-platform」をクリック。 チェックが付いたら、画面下にある「Authorize APIs」ボタンをクリック。 アカウントの選択を求められるので、開発に使用するアカウントを選択し、「Google OAuth 2.0 Playground」からのリクエストを許可する。 画面の右に「Request / Response」として以下が表示された。
HTTP/1.1 302 Found Location: https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foaut... GET /oauthplayground/?code=4/0AVG7fiSpC9M-LlpH2000000000000000000000000000000000cuotBvRbqzx5RSyZWMlw&scope=https://www.googleapis.com/auth/cloud-platform HTTP/1.1 Host: developers.google.com
次に画面の左で「Exchange authorization code for tokens」ボタンをクリック。 画面の右に「Request / Response」として以下が表示された。
HTTP/1.1 200 OK Content-length: 481 X-xss-protection: 0 X-content-type-options: nosniff Transfer-encoding: chunked Expires: Mon, 01 Jan 1990 00:00:00 GMT Vary: Origin, X-Origin, Referer Server: scaffolding on HTTPServer2 -content-encoding: gzip Pragma: no-cache Cache-control: no-cache, no-store, max-age=0, must-revalidate Date: Fri, 25 Oct 2024 03:25:23 GMT X-frame-options: SAMEORIGIN Alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Content-type: application/json; charset=utf-8 { "access_token": "ya29.a0AeD0000000000VXi3KrmXMexiZPq67qwtcpmLbCzZHCvHT0DHtGzS4DNFyBZpH9NQIO5pN0EV_FztYD_JinxSChoUiwXfPO-aIpKTj5FTQFvAEH8jMDdMDmW-YyutWgvmu4AM4-YLi0HJx_yqQ_WKLV3Wv-W3Zof7yMNRq_kaCgYKARkSARESFQHGX2MikQE7hU03dobDnoDfwS7o9g0175", "scope": "https://www.googleapis.com/auth/cloud-platform", "token_type": "Bearer", "expires_in": 3599, "refresh_token": "1//04Me3Hh0000000000ARAAGAQSNwF-L9Ir_q5RPTr_SXCv-a3yVX1bTyYch64HuSahgu4iGX2KD1_qQCZmpmFiqytTd0FJagdgFr0" }
次に画面の左で「HTTP Method」を「POST」にする。 さらに「Request URI」に以下を入力する。 https://fcm.googleapis.com/v1/projects/プロジェクトID/messages:send 今回の場合は以下のようになる。 https://fcm.googleapis.com/v1/projects/pushtest1-aeb26/messages:send 「Enter Request Body」をクリックすると、リクエスト内容の入力欄が表示される。 以下を入力する。(tokenの値には、送信先の端末のデバイストークンを設定する。以下はAndroid8に送信する場合。)
{ "message": { "token": "envzgXQ9Tm0000000000-K:APA91bFp66znOyBVMEkFCBRXhh-ssW2iQeDnXXKDGy-Ey-cN-T9FlaXFk0eKHlvfeYtoOYQjhqFN3WayxCtFwhZnOm48Mx99LIiXD2x7YQwZ8wwYw-X756tTgRAB1QvGDbdZBcp7_x69", "data": { "body": "You Got A Notification", "title": "You Got A Notification", } } }
画面下の「close」で入力欄を閉じる。 画面左にある「Send the request」ボタンを押すと、プッシュ通知が届いた。 画面右に以下が表示された。
POST /v1/projects/pushtest1-aeb26/messages:send HTTP/1.1 Host: fcm.googleapis.com Content-length: 331 Content-type: application/json Authorization: Bearer ya29.a0AeD0000000000VXi3KrmXMexiZPq67qwtcpmLbCzZHCvHT0DHtGzS4DNFyBZpH9NQIO5pN0EV_FztYD_JinxSChoUiwXfPO-aIpKTj5FTQFvAEH8jMDdMDmW-YyutWgvmu4AM4-YLi0HJx_yqQ_WKLV3Wv-W3Zof7yMNRq_kaCgYKARkSARESFQHGX2MikQE7hU03dobDnoDfwS7o9g0175 { "message": { "token": "envzgXQ9Tm0000000000-K:APA91bFp66znOyBVMEkFCBRXhh-ssW2iQeDnXXKDGy-Ey-cN-T9FlaXFk0eKHlvfeYtoOYQjhqFN3WayxCtFwhZnOm48Mx99LIiXD2x7YQwZ8wwYw-X756tTgRAB1QvGDbdZBcp7_x69", "data": { "body": "You Got A Notification", "title": "You Got A Notification", } } } HTTP/1.1 200 OK Content-length: 86 X-xss-protection: 0 X-content-type-options: nosniff Transfer-encoding: chunked Vary: Origin, X-Origin, Referer Server: scaffolding on HTTPServer2 -content-encoding: gzip Cache-control: private Date: Fri, 25 Oct 2024 03:31:12 GMT X-frame-options: SAMEORIGIN Alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Content-type: application/json; charset=UTF-8 { "name": "projects/pushtest1-aeb26/messages/0:1729827072841922%d7f69f40f9fd7ecd" }
■curlから通知の送信 「専用ツールから通知の送信」の手順中に取得したアクセストークンを使えば、curlコマンドでも送信することができる。
$ curl -X POST \ --header "Authorization: Bearer [アクセストークン]" \ --header "Content-Type: application/json" \ https://fcm.googleapis.com/v1/projects/[プロジェクトID]/messages:send -d " \ { \"message\": {\"token\":\"[FCM_TOKEN]\", \"data\":{\"body\": \"You Got A Notification\", \"title\": \"You Got A Notification\"}}}"
具体的には以下のようになる。
$ curl -X POST \ --header "Authorization: Bearer ya29.a0AeDClZAHyP3eyVXi3KrmXMexiZPq67qwtcpmLbCzZHCvHT0DHtGzS4DNFyBZpH9NQIO5pN0EV_FztYD_JinxSChoUiwXfPO-aIpKTj5FTQFvAEH8jMDdMDmW-YyutWgvmu4AM4-YLi0HJx_yqQ_WKLV3Wv-W3Zof7yMNRq_kaCgYKARkSARESFQHGX2MikQE7hU03dobDnoDfwS7o9g0175" \ --header "Content-Type: application/json" \ https://fcm.googleapis.com/v1/projects/pushtest1-aeb26/messages:send -d " \ { \"message\": {\"token\":\"envzgXQ9Tm0000000000-K:APA91bFp66znOyBVMEkFCBRXhh-ssW2iQeDnXXKDGy-Ey-cN-T9FlaXFk0eKHlvfeYtoOYQjhqFN3WayxCtFwhZnOm48Mx99LIiXD2x7YQwZ8wwYw-X756tTgRAB1QvGDbdZBcp7_x69\", \"data\":{\"body\": \"You Got A Notification\", \"title\": \"You Got A Notification\"}}}"
実行すると以下の結果が返され、プッシュ通知が届いた。
{ "name": "projects/pushtest1-aeb26/messages/0:1707468192965283%d7f69f40f9fd7ecd" }
同じアクセストークンでさらにプッシュ通知を送信できるが、この値は一定期間で失効してしまう。 しばらくしてから試すと、以下のエラーが返されるようになった。
{ "error": { "code": 401, "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", "status": "UNAUTHENTICATED" } }
この場合、上に記載の手順で再度アクセストークンを取得すればいい。
Android: アプリにPHPからPushを送信
主に以下の記事を参考に進める。 PHP/LaravelでFCMpush通知を実装した件 #PHP - Qiita https://qiita.com/takapon21/items/47a72472541650fe3b1a 以下のとおり、Composerを使えるようにする。(PHP7.4以上が必要。)
$ cd /path/to/firebase $ composer Composer version 2.4.4 2022-10-27 14:39:29
以下のとおり、必要ライブラリをインストールする。(バージョン指定は無しでいいかもしれない。)
$ composer require google/apiclient:^2.15.0 23 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files 6 packages you are using are looking for funding. Use the `composer fund` command to find out more! No security vulnerability advisories found
続いて、Pushを送信するためのPHPプログラムを作成する。 pushtest1-aeb26-c00a0b10addc.json の情報をもとにして、以下のプログラムを作成する。(ファイルの文字コードは UTF-8N にする。)
<?php require_once 'vendor/autoload.php'; $googleClient = new Google_client; $googleClient->useApplicationDefaultCredentials(); // 認証情報 $googleClient->setAuthConfig([ 'type' => 'service_account', 'project_id' => 'pushtest1-aeb26', 'private_key_id' => 'c00a0b10ad00000000000000000000baebe01a4f', 'private_key' => "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnoXNCLluQ4ikO\nY7mxImNS9ft4LFWCXDUqkPajWgMYwJzyLd〜中略〜hx0RWLhN9\n7mZwLUZfLvTKeCalOeDx61eCn4cnHRhpIukGzV4NdF1KwD+v3Jpo2Ot74/pGwSWl\nKUYqj6EYTMsXropj5TFXGg0=\n-----END PRIVATE KEY-----\n", 'client_email' => 'firebase-adminsdk-ik32h@pushtest1-aeb26.iam.gserviceaccount.com', 'client_id' => '101260000000000064216', 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', 'token_uri' => 'https://oauth2.googleapis.com/token', 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-ik32h%40pushtest1-aeb26.iam.gserviceaccount.com', ]); // FCMにアクセスするためのスコープ $googleClient->addScope('https://www.googleapis.com/auth/firebase.messaging'); // 認証されたHTTPクライアントを取得 $httpClient = $googleClient->authorize(); // 送信対象と送信内容を作成 $project = 'https://fcm.googleapis.com/v1/projects/pushtest1-aeb26/messages:send'; $tokens = [ 'envzgXQ9Tm0000000000-K:APA91bFp66znOyBVMEkFCBRXhh-ssW2iQeDnXXKDGy-Ey-cN-T9FlaXFk0eKHlvfeYtoOYQjhqFN3WayxCtFwhZnOm48Mx99LIiXD2x7YQwZ8wwYw-X756tTgRAB1QvGDbdZBcp7_x69', ]; $title = 'プッシュ通知のテスト'; $body = 'これはプッシュ通知のテストです。'; foreach ($tokens as $token) { try { // FCMにPOSTリクエストを送信 $response = $httpClient->post($project, [ 'json' => [ 'message' => [ 'token' => $token, 'notification' => [ 'title' => $title, 'body' => $body, ], 'apns' => [ 'payload' => [ 'aps' => [ 'alert' => [ 'title' => $title, 'body' => $body ] ] ] ], "android" => [ "priority" => "high", "notification" => [ "sound" => "default", ] ], ] ] ]); // 応答の確認 if ($response->getStatusCode() == 200) { echo "送信成功: Token = {$token}\n"; } else { echo "送信失敗: Token = {$token}, HTTP ステータスコード = " . $response->getStatusCode() . "\n"; } } catch (\GuzzleHttp\Exception\RequestException $e) { // 通信エラーなどリクエスト送信時のエラー if ($e->hasResponse()) { $errorMessage = $e->getResponse()->getBody()->getContents(); echo "送信失敗: Token = {$token}, エラーメッセージ = {$errorMessage}\n"; } else { echo "送信失敗: Token = {$token}, ネットワークエラーまたはその他のエラー\n"; } } catch (\Exception $e) { // その他のエラー echo "送信失敗: Token = {$token}, エラーメッセージ = " . $e->getMessage() . "\n"; } } echo "Complete\n";
以下のとおり実行して、本番アプリにプッシュ通知を送信できることを確認できた。
$ php firebase.php
これなら、数日経ってもアクセストークンが失効されることは無かった。 一度に複数端末へ送信したい場合、上記のように送信先を配列で定義して、ループで順に送信するといい。 もしくは、FCMのトピックを使って一斉送信するといい。(未検証。) なおPHP7環境だと、以下のエラーになるので注意。
Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.1.0". You are running 7.3.19. in /path/to/firebase/vendor/composer/platform_check.php on line 24
この場合、PHPを8にアップデートするか、別途PHP8.1をインストールして実行するといい。
$ /path/to/php81/php firebase.php
■トラブル事例1 Composer実行時に以下のエラーになった場合、
Problem 1 - Root composer.json requires google/apiclient 2.15.0 -> satisfiable by google/apiclient[v2.15.0]. - google/apiclient v2.15.0 requires php ^7.4|^8.0 -> your php version (7.3.19) does not satisfy that requirement.
一例だがWindows環境では、以下のようにPHP8.1を参照できるようにするといい。 C:\localhost\home\test\public_html\firebase\composer.bat
@ECHO OFF C:\php81\php.exe "%~dp0composer.phar" %*
またPHP7.3環境では、「composer require google/apiclient」とバージョン指定なしならインストールできることを確認できた。 プッシュ通知の送信もできた。 ■トラブル事例2 Composer実行時に以下のエラーになった場合、
The openssl extension is required for SSL/TLS protection but is not available. If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the 'disable-tls' option to true.
以下のページを参考にphp.iniを調整する。 composerのSSLのエラーを解決する https://digirakuda.org/blog/2018/06/04/post-251/ ■トラブル事例3 プッシュ通知送信時に以下のエラーになった場合、
PHP Warning: openssl_sign(): Supplied key param cannot be coerced into a private key in /path/to/firebase/vendor/firebase/php-jwt/src/JWT.php on line 254 Warning: openssl_sign(): Supplied key param cannot be coerced into a private key in /path/to/firebase/vendor/firebase/php-jwt/src/JWT.php on line 254 PHP Fatal error: Uncaught DomainException: OpenSSL unable to sign data in /path/to/firebase/vendor/firebase/php-jwt/src/JWT.php:256
鍵の指定が正しくできているか確認する。 鍵データには「\n」が含まれるので、正しく改行と解釈させるために、ダブルクォートでくくって指定する。 【Laravel/PHP】openssl_sign(): supplied key param cannot be coerced into a private key の 対応方法 https://engineer-jose-blog.com/%E3%80%90php%E3%80%91openssl_sign-supplied-key-param-cannot-be-coerce... ■AmazonSNSを使う準備 通知の動作を確認できたら、MyFirebaseMessagingService.kt にある以下の処理を修正しておく。(AmazonSNSからのメッセージを受け取る準備。) ★この修正を行なった後でも、問題無く通知を受け取れるみたい?
showNotification("通知タイトル", "通知メッセージ") ↓ showNotification("PushTest1", remoteMessage.data["default"])
※ひととおり試してから、上記の調整を行ったうえでプッシュ通知を再送信すると、普通にメッセージが表示された?気のせい? Firebase側で良い感じに調整されたのか、別のアプリに届いたなど何かの勘違いか。 何度か試していると、「通知タイトル」「通知メッセージ」になったり、また「テストのタイトル」「テストのテキスト」に戻ったりするような? ※そもそも「FirebaseからPushを送信した場合、このキーで送られてくる」はあるはずなので、改めて調査したい。 後述の「AmazonSNS: PHPプログラムの作成」にある「補足」も参照。
iOS: アプリの作成
※いったんプッシュ通知機能の無い状態でアプリを作る。 ※Organization Identifier を考慮して、いったん「pushtest1」のような Product Name で作成してIDを決めさせ、後からアプリの名前を変更するといい。 IDについては、「概要・前提・注意点など」の「アプリのID」も参照。 Xcodeでプロジェクトを作成する。 Application: App Product Name: pushtest1 Team: (案件に応じて適切に選択する) Organization Identifier: net.refirio Interface: SwiftUI Language: Swift Bundle Identifier は、上記内容でアプリを作成すると「net.refirio.pushtest1」になる。 エミュレータと実機で、アプリを起動できるかテストする。 ※以降の「証明書」は、断りが無ければ原則「p12証明書」を使った証明書のこと。 後発の「p8証明書」を使った手順は、後述の「iOS: p8証明書」を参照。
iOS: 証明書の作成
■Apple Developer Programへのログイン ブラウザで以下にログインする。 Apple Developer Program https://developer.apple.com/jp/programs/ 右上のアカウント名が案件に応じたものになっていることを確認する。 ■Push通知の使用を設定 Xcodeのプロジェクトの「Signing & Capabilities」で「+Capabilities」をクリックし、 一覧に表示される「Push Notifications」をダブルクリックで選択する。 (はじめて証明書を作成するときのみ。) Apple Developer Program で Certificates, Identifiers & Profiles → Identifiers にアクセスして確認すると、一覧に 「XC jp 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, Identifiers & Profiles → Identifiers → XC jp 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 をダブルクリックして、キーチェーンに登録する。 (登録すると、キーチェーンアクセスの一覧に表示される。) ↓ 登録された証明書を確認する。 キーチェーンアクセスの上部にある「分類」を「自分の証明書」にした状態で探す。 (証明書のダウンロードページにある「Expiration Date」をもとに「有効期限」と比較するといい。また、Finderで検索してから書き出そうとするとp12を選択できないので注意。) 証明書を選択して「ファイル → 書き出す」をクリック。 デフォルトで「証明書」というファイル名になるが、日本語ファイル名だとAWSへの登録に失敗するので変更する。 ここでは「PushTest1-Dev」として保存する(開発版用。本番用なら「PushTest1」などとする。) 「書き出した項目を保護するために使用されるパスワード」の入力画面になるが、パスワードはカラのまま「OK」をクリック。 「キーチェーンアクセスは、キーチェーンのキー○○を書き出そうとしています。」の入力画面になるが、Macのログインパスワードを入力して「許可」をクリック。 これで「PushTest1-Dev.p12」というファイルが作成される。
iOS: アプリにPush通知受信機能を実装
Xcodeのプロジェクトの「Signing & 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-... プログラムは、主に以下の記事を参考に作成する。 SwiftUIでのプッシュ通知の最小構成 - すいすいSwift https://swiswiswift.com/2022-03-03/ pushtest1App.swift
import SwiftUI @main struct pushtest1App: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate … 追加 var body: some Scene { WindowGroup { ContentView() } } }
ContentView.swift
import SwiftUI struct ContentView: View { … ContentView内の処理を変更 let publisher = NotificationCenter.default.publisher(for: .deviceToken) @State var deviceToken: String = "" var body: some View { VStack { Text("DeviceToken") TextField("Device Token is Empty", text: $deviceToken) .padding() } .onReceive(publisher) { message in if let deviceToken = message.object as? String { self.deviceToken = deviceToken print("Device Token: " + self.deviceToken) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
AppDelegate.swift(新規に作成する)
import UIKit //@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Show Push Notification Dialogue UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in guard granted else { return } DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } UNUserNotificationCenter.current().delegate = self 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. } } extension Notification.Name { static let deviceToken = Notification.Name("DeviceToken") } extension AppDelegate: UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined() print("Device token: \(token)") NotificationCenter.default.post(name: .deviceToken, object: token) } func application(_ application: UIApplication, didReceiveRemoteNotification payload: [AnyHashable: Any]) { print(payload) } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.sound, .badge]) } }
※plistの編集は不要だった。 ※CocoaPodsのインストールは不要だった。 ※実際の実装では、トークンを画面に表示することなく「デバイス内に保存してサーバにも投げる」としておけば良さそう。 後述の「アプリ起動時にデバイストークンをサーバ側に記録」も参照。 ■動作確認 アプリを実行すると、「"〇〇"は通知を送信します。よろしいですか?」が表示されるので「許可」をタップする。 画面にデバイストークンが表示されることを確認する。 また、ログにもデバイストークンが表示されるようにしている。 以下は実際に表示された値。
Device Token: d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2
この文字列をもとに、サーバサイドプログラムからプッシュ通知を送信する。 具体的な送信方法は引き続き後述する。 ※エミュレータではプッシュ通知を受け取れないので、実機で実行する。
iOS: アプリにcurlからPushを送信
■curlでプッシュ通知を送信(p.12) iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた - つばくろぐ @takamii228 https://takamii.hatenablog.com/entry/2020/07/13/190027 まずはcurlで送信する。(この場合、PHPは関係ない。) 以下にデータを送ることで、プッシュ通知を送信できる。 開発環境用 ... api.development.push.apple.com 本番環境用 ... api.push.apple.com 以下のコマンドでプッシュ通知を送信できる。(本番環境用の場合、送信先は api.push.apple.com にする。)
$ curl -v -d '{"aps":{"alert":{"title":"[送信タイトル]","body":"[送信メッセージ]"}}}' -H "Content-Type: application/json" -H "apns-topic: [アプリのID]" -H "apns-priority: 10" --http2 --cert-type P12 --cert [p12ファイル] https://api.development.push.apple.com/3/device/[デバイストークン]
具体的には、以下のように実行する。
$ curl -v -d '{"aps":{"alert":{"title":"テスト","body":"これはp12ファイルによる送信です。"}}}' -H "Content-Type: application/json" -H "apns-topic: net.refirio.pushtest1" -H "apns-priority: 10" --http2 --cert-type P12 --cert PushTest1-Dev.p12 https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * Trying 17.188.143.34:443... * Connected to api.development.push.apple.com (17.188.143.34) port 443 * ALPN: curl offers h2,http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * TLSv1.2 (OUT), TLS handshake, Client hello (1): * CAfile: /etc/pki/tls/certs/ca-bundle.crt * CApath: none * 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 h2 * Server certificate: * subject: C=US; ST=California; O=Apple Inc.; CN=api.development.push.apple.com * start date: Apr 24 18:16:30 2024 GMT * expire date: Apr 10 00:00:00 2025 GMT * subjectAltName: host "api.development.push.apple.com" matched cert's "api.development.push.apple.com" * issuer: CN=Apple Public Server RSA CA 12 - G1; O=Apple Inc.; ST=California; C=US * SSL certificate verify ok. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * [HTTP/2] [1] [:method: POST] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: api.development.push.apple.com] * [HTTP/2] [1] [:path: /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2] * [HTTP/2] [1] [user-agent: curl/8.3.0] * [HTTP/2] [1] [accept: */*] * [HTTP/2] [1] [content-type: application/json] * [HTTP/2] [1] [apns-topic: net.refirio.pushtest1] * [HTTP/2] [1] [apns-priority: 10] * [HTTP/2] [1] [content-length: 97] > POST /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2 HTTP/2 > Host: api.development.push.apple.com > User-Agent: curl/8.3.0 > Accept: */* > Content-Type: application/json > apns-topic: net.refirio.pushtest1 > apns-priority: 10 > Content-Length: 97 > < HTTP/2 200 < apns-id: CF6A16B6-0000-14A5-265B-4DC3C7E52CBE < apns-unique-id: d29eefb2-0000-e6da-dbeb-4f7e7232e32a < * Connection #0 to host api.development.push.apple.com left intact
最後の部分に結果が表示されている。 以下は送信に成功したときの例。
* Connection state changed (MAX_CONCURRENT_STREAMS == 1000)! * We are completely uploaded and fine < HTTP/2 200 < apns-id: BD93B434-0000-2CEE-A4D7-B18540B741D6 < apns-unique-id: 65a06e7a-0000-59c0-0e8d-53d36e448d3b < * Connection #0 to host api.development.push.apple.com left intact
以下は不正なデバイストークンに送信したときの例。
* Connection state changed (MAX_CONCURRENT_STREAMS == 1000)! * We are completely uploaded and fine * Connection state changed (MAX_CONCURRENT_STREAMS == 1)! < HTTP/2 400 < apns-id: BAAD2D41-0000-470D-87A6-399901FD299D < * Connection #0 to host api.development.push.apple.com left intact {"reason":"BadDeviceToken"}
「HTTP/2 200」が返された場合、恐らく送信は成功している。 「HTTP/2 400」が返された場合、恐らく送信は失敗している。デバイストークンなどに間違いが無いか確認する。 証明書が不正だった場合、そもそも通信ができない。 このとき、以下のようなエラーが返された。
* Trying 17.188.168.149:443... * Connected to api.development.push.apple.com (17.188.168.149) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * error reading PKCS12 file 'PushTest1-Dev.p12' * Closing connection 0 curl: (58) error reading PKCS12 file 'PushTest1-Dev.p12'
「Content-Type」「apns-topic」「apns-priority」といったヘッダがあるが、どの解説も大文字はじまりと小文字はじまりが混在している。 「Content-Type」など一般的なものは大文字始まりで、「apns-topic」など独自のものは小文字始まりで…のような慣習があるのかもしれない。 Content-Type: HTTP エンティティー・ヘッダー - IBM Documentation https://www.ibm.com/docs/ja/ibm-mq/7.5?topic=ssfksj-7-5-0-com-ibm-mq-ref-dev-doc-q110690--htm priority: HTTP x-msg-priority エンティティー・ヘッダー - IBM Documentation https://www.ibm.com/docs/ja/ibm-mq/7.5?topic=ssfksj-7-5-0-com-ibm-mq-ref-dev-doc-q110770--htm iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた - つばくろぐ @takamii228 https://takamii.hatenablog.com/entry/2020/07/13/190027 ■pemファイルの作成 ※p12ファイルをpemに変換し、それを使って送信することもできる。 ※AmazonSNSにはpemではなくp12を登録して送信できるので原則この対応は不要だが、 後述の「iOS: アプリにPHPからPushを送信」ではpemを使用する。 opensslコマンドの使える環境で、作成した PushTest1-Dev.p12 をpem形式に変換する。 パスワードの入力を求められるが、p12ファイルをパスワードなしで作成した場合、パスワードは空欄のままEnterでいい。
$ openssl pkcs12 -in PushTest1-Dev.p12 -out PushTest1-Dev.pem -nodes -clcerts Enter Import Password: MAC verified OK
このファイルとデバイストークンを使って、直接プッシュ通知を送信できる。 ■curlでプッシュ通知を送信(pem) 以下のコマンドでプッシュ通知を送信できる(curlで送信するならPHPは関係ない。本番環境用の場合、送信先は api.push.apple.com にする。)
$ curl -v -d '{"aps":{"alert":{"title":"[送信タイトル]","body":"[送信メッセージ]"}}}' -H "Content-Type: application/json" -H "apns-topic: [アプリのID]" -H "apns-priority: 10" --http2 --cert [pemファイル] https://api.development.push.apple.com/3/device/[デバイストークン]
具体的には、以下のようにする。
$ curl -v -d '{"aps":{"alert":{"title":"テスト","body":"これはpemファイルによる送信です。"}}}' -H "Content-Type: application/json" -H "apns-topic: net.refirio.pushtest1" -H "apns-priority: 10" --http2 --cert PushTest1-Dev.pem https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * Trying 17.188.143.98:443... * Connected to api.development.push.apple.com (17.188.143.98) port 443 * ALPN: curl offers h2,http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * TLSv1.2 (OUT), TLS handshake, Client hello (1): * CAfile: /etc/pki/tls/certs/ca-bundle.crt * CApath: none * 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 h2 * Server certificate: * subject: C=US; ST=California; O=Apple Inc.; CN=api.development.push.apple.com * start date: Apr 24 18:16:30 2024 GMT * expire date: Apr 10 00:00:00 2025 GMT * subjectAltName: host "api.development.push.apple.com" matched cert's "api.development.push.apple.com" * issuer: CN=Apple Public Server RSA CA 12 - G1; O=Apple Inc.; ST=California; C=US * SSL certificate verify ok. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * [HTTP/2] [1] [:method: POST] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: api.development.push.apple.com] * [HTTP/2] [1] [:path: /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2] * [HTTP/2] [1] [user-agent: curl/8.3.0] * [HTTP/2] [1] [accept: */*] * [HTTP/2] [1] [content-type: application/json] * [HTTP/2] [1] [apns-topic: net.refirio.pushtest1] * [HTTP/2] [1] [apns-priority: 10] * [HTTP/2] [1] [content-length: 97] > POST /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2 HTTP/2 > Host: api.development.push.apple.com > User-Agent: curl/8.3.0 > Accept: */* > Content-Type: application/json > apns-topic: net.refirio.pushtest1 > apns-priority: 10 > Content-Length: 97 > < HTTP/2 200 < apns-id: A4105CBB-0000-CD3D-ED0C-59DB4F06D2F2 < apns-unique-id: 12cd37b1-0000-95f2-90b8-1a24cce2f645 < * Connection #0 to host api.development.push.apple.com left intact
「HTTP/2 200」が返された場合、恐らく送信は成功している。 「HTTP/2 400」が返された場合、恐らく送信は失敗している。デバイストークンなどに間違いが無いか確認する。 証明書が不正だった場合、そもそも通信ができない。 このとき、以下のようなエラーが返された。
* Trying 17.188.168.149:443... * Connected to api.development.push.apple.com (17.188.168.149) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * could not load PEM client certificate, OpenSSL error error:0906D06C:PEM routines:PEM_read_bio:no start line, (no key found, wrong pass phrase, or wrong file format?) * Closing connection 0 curl: (58) could not load PEM client certificate, OpenSSL error error:0906D06C:PEM routines:PEM_read_bio:no start line, (no key found, wrong pass phrase, or wrong file format?)
PHPプログラムからの送信は、引き続き次の項目にて。
iOS: アプリにPHPからPushを送信
APNs Provider API(HTTP/2)をPHPで試してみる - Qiita https://qiita.com/itosho/items/2402df4de85b360d5bd9 ■プログラムの作成 Pushを送信するためのPHPプログラムを作成する。 (ファイルの文字コードは UTF-8N にする。本番環境用の場合、送信先は api.push.apple.com にする。) apns_test.php
<?php if (defined('CURL_HTTP_VERSION_2_0')) { $ch = curl_init('https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2'); curl_setopt($ch, CURLOPT_POSTFIELDS, '{"aps":{"alert":{"title":"テスト","body":"これはPHPからの送信です。"}}}'); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json','apns-topic: net.refirio.pushtest1','apns-priority: 10']); curl_setopt($ch, CURLOPT_SSLCERT, 'PushTest1-Dev.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); }
以下で実行できる。 (PushTest1-Dev.pem は同じ階層に配置してあるものとする。)
$ php apns_test.php bool(true) int(200)
送信内容に問題があると、JSON形式でエラー内容を返してくれる。 以下はデバイストークンを間違えた場合の例。
$ php apns_test.php {"reason":"BadDeviceToken"}bool(true) int(400)
不正な証明書だと、そもそもエラー内容が返されない。 以下のような結果になる。
$ php apns_test.php bool(false) int(0)
AmazonSNS: プッシュ通知用のキーや証明書の登録
■概要 AndroidとiOSにプッシュ通知を送信できるようになったら、AmazonSNSから一括して送信できるようにする。 プッシュ通知を送信するための鍵ファイルや証明書を、AmazonSNSに登録することで可能になる。 以下で「アプリケーション(Android)」「アプリケーション(iOS)」「トピック」を作成するが、大まかに ・Androidの場合、端末のデバイストークンを「アプリケーション(Android)」に追加すると、デバイストークンに対応したエンドポイントが発行される。 ・iOSの場合、端末のデバイストークンを「アプリケーション(iOS)」に追加すると、デバイストークンに対応したエンドポイントが発行される。 ・AndroidもiOSも、AmazonSNS経由ではエンドポイントを指定してプッシュ通知を送信できる。 ・エンドポイントは「トピック」に追加できる。 ・「トピック」を指定してプッシュ通知を送信すると、容易に一斉送信を実現できる。 という関係になっている。 なお、上記の操作はAWSコンソールから手動で行えるが、SDKを使用してプログラムから行なうこともできる。 ■AmazonSNSアプリケーションの作成(Android) Amazon SNS → プッシュ通知 →プラットフォームアプリケーションの作成 アプリケーション名: PushTest1-FCM-Dev(開発版想定。本番なら「PushTest1-FCM」などとする。iOS用にも作る可能性があるので、単に「PushTest1」ではなく「FCM」の文字を含めておくのが無難そう。) プッシュ通知プラットフォーム: Firebase Cloud Messaging (FCM) 認証方法: トークン Service Json: (「Android: Firebaseの鍵ファイルを取得」で取得したJsonファイル。) ★この時点では「Dev」を付けない方がいいような。(上の手順では本番用に作成しているので。) 「アプリケーションエンドポイントの作成」をクリック。 アプリケーション一覧に追加されたことを確認する。 ■AmazonSNSアプリケーションの作成(iOS) Amazon SNS → プッシュ通知 →プラットフォームアプリケーションの作成 アプリケーション名: PushTest1-APNS-Dev(開発版想定。本番なら「PushTest1-APNS」などとする。Android用にも作る可能性があるので、単に「PushTest1」ではなく「APNS」の文字を含めておくのが無難そう。) プッシュ通知プラットフォーム: Apple iOS/VoIP/Mac サンドボックスでの開発に使用されます: 開発用(Development SSL Certificate)の場合、チェックを入れる。 プッシュサービス: iOS 認証方法: 証明書(p8ファイルの場合は「トークン」にする。詳細は後述の「iOS: p8証明書」を参照。) 証明書: PushTest1-Dev.p12(先の手順で作成したファイル。) パスワードの入力: (p12ファイル作成時、パスワードをカラで作成したなら空欄。) 「認証情報をファイルから読み込み」をクリック。 証明書の情報が表示されたことを確認して「アプリケーションエンドポイントの作成」をクリック。 ※「認証情報をファイルから読み込み」をクリックしたとき。 「Apple の認証情報をファイルから読み込んでいるときにエラーが発生しました」 というエラーになる場合、p12のファイル名に日本語を含めていないか、証明書作成時に通称に日本語を含めていないか、などを確認する。 その他、原則として半角英数字に統一しておくほうが無難。 ※Apple Developer Program での作業は、MacのSafariで行うことが推奨されているみたい。 どうしても意図した操作ができなければ、MacのSafariで試す。 ※証明書が壊れていないかなどは、以下のコマンドで確認できる。 パスワード入力後、ファイルの情報が表示されることを確認する。
$ openssl pkcs12 -in PushTest1-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トピックの作成(すべてのデバイスへ一斉送信するトピックを作成する場合) トピックを作成してそこに端末を登録しておけば、 トピックを指定するだけでトピックに属する端末すべてにプッシュ通知を送信できる。 左メニューから「トピック」を開く。 「トピックの作成」ボタンを押すとトピックの作成画面が開く。 タイプ: スタンダード 名前: PushTest1-Dev-All 表示名: (SMS用の項目なので空欄) 「トピックの作成」をクリック。 トピック一覧に追加されたことを確認する。 (表示されるARNの値は、後ほどPHPプログラムに設定する。) トピックのARNを指定するだけで一斉送信ができるので、運用の際は「全端末配信用」のトピックを作っておくと良さそう。 後からトピックを追加すると、トピックに対して端末を登録する必要があるので注意。 トピックを気軽に増減する設計は避けるほうがいいかもしれない。要検証。 ■アクセスキーの作成 画面右上のアカウント名 → セキュリティ認証情報 → ユーザー → ユーザーを追加 ユーザー名: pushtest1-dev AWSマネジメントコンソールへのユーザーアクセス: 提供しない ポリシー: AmazonSNSFullAccess 以下のとおりアクセスキーを発行。 ユースケース: コマンドラインインターフェイス (CLI) Access key ID: XXXXXXXXXX Secret access key: YYYYYYYYYY
AmazonSNS: PHPプログラムの作成
■ライブラリの準備 AWSのSDKを使ってプッシュ通知を送信できる。 Composerを使う場合、以下でインストールできる。
$ composer require aws/aws-sdk-php
もしくは以下からSDKを入手して、手動で配置することもできる。 https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/getting-started_installation.html ■プログラムの作成 AmazonSNSを扱うためのPHPプログラムを作成する。(ファイルの文字コードは UTF-8N にする。) アクセスキーは、先ほど作成したものを使用する。 ※いったん pushtest1-dev などのフォルダを作って、その中に開発版用として作成するといい。 証明書などが異なるので、本番用を作るなら pushtest1 フォルダなどに、検収版を作るなら pushtest1-stg フォルダなどに、別途作ると良さそう。 (実案件なら、そもそも配置するサーバ自体が異なると思われるが。) アプリからPHPプログラムを呼び出す場合、SSL経由にする必要があるので注意。 (このプログラムはブラウザから操作する前提なので問題ないが、本番用のアプリなら端末からPHPにアクセスしてデバイストークンを渡したり…が必要。)
<?php require 'vendor/autoload.php'; use Aws\Sns\SnsClient; use Aws\Sns\Exception\SnsException; $result_code = null; $result_data = null; try { // AmazonSNSに接続 $client = new SnsClient([ 'credentials' => [ 'key' => 'XXXXXXXXXX', 'secret' => 'YYYYYYYYYY', ], 'region' => 'ap-northeast-1', 'version' => 'latest', ]); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['api'])) { if ($_POST['api'] === 'createPlatformEndpoint') { // アプリケーションに端末を追加 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createplatformendpoint $result = $client->createPlatformEndpoint([ 'PlatformApplicationArn' => $_POST['applicationArn'], 'Token' => $_POST['deviceToken'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['EndpointArn']; } elseif ($_POST['api'] === 'listEndpointsByPlatformApplication') { // アプリケーションに対するエンドポイントの一覧を取得 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listendpointsbyplatformapplic... $result = $client->listEndpointsByPlatformApplication([ 'NextToken' => $_POST['nextToken'], 'PlatformApplicationArn' => $_POST['applicationArn'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['Endpoints']; } elseif ($_POST['api'] === 'getEndpointAttributes') { // アプリケーションに対するエンドポイントの状態を取得 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#getendpointattributes $result = $client->getEndpointAttributes([ 'EndpointArn' => $_POST['endpointArn'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['Attributes']; } elseif ($_POST['api'] === 'setEndpointAttributes') { // アプリケーションに対するエンドポイントの状態を変更 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#setendpointattributes $result = $client->setEndpointAttributes([ 'Attributes' => [ 'Enabled' => $_POST['enabled'] ], 'EndpointArn' => $_POST['endpointArn'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = []; } elseif ($_POST['api'] === 'createTopic') { // トピックを追加 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#createtopic $result = $client->createTopic([ 'Name' => $_POST['name'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['TopicArn']; } elseif ($_POST['api'] === 'subscribe') { // トピックにエンドポイントを追加 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#subscribe $result = $client->subscribe([ 'Endpoint' => $_POST['endpoint'], 'Protocol' => 'application', 'ReturnSubscriptionArn' => false, 'TopicArn' => $_POST['topicArn'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['SubscriptionArn']; } elseif ($_POST['api'] === 'listTopics') { // トピック一覧を取得 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#listtopics $result = $client->listTopics([ 'NextToken' => $_POST['nextToken'], ]); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['Topics']; } elseif ($_POST['api'] === 'publish') { // 指定した端末もしくはトピックに対してプッシュ通知を送信 // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sns-2010-03-31.html#publish $gcm = json_encode([ 'data' => [ 'title' => $_POST['message'], 'body' => $_POST['message'], 'url' => null, ], // 通知タップ時にアプリを開くために必要な追加のフィールド 'notification' => [ 'title' => $_POST['message'], 'body' => $_POST['message'], ], ]); $apns = json_encode([ 'aps' => [ 'alert' => [ 'title' => $_POST['message'], 'body' => $_POST['message'], ], 'badge' => 1, 'sound' => 'default' ], 'url' => null ]); $message = [ 'default' => $_POST['message'], 'GCM' => $gcm, 'APNS' => $apns, 'APNS_SANDBOX' => $apns, ]; $parameter = [ 'Message' => json_encode($message), 'MessageStructure' => 'json', ]; /* $parameter = [ 'Message' => $_POST['message'], ]; */ if ($_POST['targetArn'] != '') { $parameter['TargetArn'] = $_POST['targetArn']; } elseif ($_POST['topicArn'] != '') { $parameter['TopicArn'] = $_POST['topicArn']; } $result = $client->publish($parameter); $result_code = $result->get('@metadata')['statusCode']; $result_data = $result['MessageId']; } } } catch (SnsException $e) { exit('SnsException: ' . $e->getMessage()); } catch (Exception $e) { exit('Exception: ' . $e->getMessage()); } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AmazonSNS</title> </head> <body> <h1>AmazonSNS</h1> <?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['api'])) : ?> <h2>Result</h2> <pre><?php print_r(['result_code' => $result_code, 'result_data' => $result_data]) ?></pre> <?php endif ?> <h2>CreatePlatformEndpoint</h2> <p>アプリケーションに端末を追加。</p> <form action="sns.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="sns.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="sns.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="sns.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="sns.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="sns.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="sns.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="sns.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>
■補足 プッシュ通知送信部分の $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) Amazon SNS → プッシュ通知 → PushTest1-FCM-Dev この画面で、「このアプリケーションにはエンドポイントがありません。」と表示されていることを確認する。 作成したPHPプログラムにアクセスし、 「CreatePlatformEndpoint」で以下を入力して実行する。 applicationArn: arn:aws:sns:ap-northeast-1:949004901725:app/GCM/PushTest1-FCM-Dev deviceToken: (プッシュ通知を送信したいAndroid端末のデバイストークン。) 先ほどの画面に、エンドポイントが追加されていることを確認する。 トークンに対応したARNが作成され、AmazonSNSでプッシュ通知を送るときはこのARNを使用する。 作成したPHPプログラムにアクセスし、 「Publish」で以下を入力して実行する。 message: 任意のメッセージ(日本語可)を入力。 targetArn: (プッシュ通知を送信したいAndroid端末のARN。) topicArn: (空欄) 端末にプッシュ通知が届くことを確認する。 (AmazonSNS経由なら、iOS・Androidとも端末に即座に届いた。) なお、無効になったエンドポイントに送信した場合、以下のエラーが表示される。 これをもとに無効な送信先を削除したりはできそう。 またそれとは別に、アプリを起動するたびにデバイストークンの更新を行うと良さそう。(その時点でのデバイストークンを、AmazonSNSに上書き登録する。)
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) Androidのときのように、「CreatePlatformEndpoint」で以下を入力して実行する。 applicationArn: arn:aws:sns:ap-northeast-1:949004901725:app/APNS_SANDBOX/PushTest1-APNS-Dev deviceToken: (プッシュ通知を送信したいiOSのデバイストークン) Androidのときのように、「Publish」で以下を入力して実行する。 message: 任意のメッセージ(日本語可)を入力 targetArn: (プッシュ通知を送信したいiOSのARN) 端末にプッシュ通知が届くことを確認する。 ■トピックごとに送信 Amazon SNS → トピック → 作成したトピックの名前をクリック この画面で、エンドポイントがカラであることを確認する。 作成したPHPプログラムにアクセスし、 「Subscribe」でトピックのARNと追加したい端末のARNを入力して実行する。(トピックにARNが追加される。) 「Publish」で以下を入力して実行する。 message: 任意のメッセージ(日本語可)を入力。 targetArn: (空欄) topicArn: (プッシュ通知を送信したいトピックのARN。) ※トピック内に無効なエンドポイントが含まれていても、特にエラーなどは返してくれないみたい? 要検証。 ■AmazonSNSから送信(デバッグ用) Amazon SNS → プッシュ通知 → 作成したアプリケーションの名前をクリック 送信先にチェックを入れて「メッセージの発行」をクリック。 メッセージ構造: すべての配信プロトコルに同一のペイロード メッセージ: AmazonSNSから直接送信 「メッセージの発行」をクリックすると送信できる。 また、「メッセージ構造」を「配信プロトコルごとにカスタムペイロード」にすると、 メッセージに以下のようなテキストが表示される。 「Sample message for iOS development endpoints」の部分を任意のメッセージに変更して送信でき、単純なメッセージ以外も送信できるようになる。
{ "APNS_SANDBOX": "{\"aps\":{\"alert\":\"AmazonSNSから直接送信\"}}" }
Amazon SNS → トピック 送信先にチェックを入れて「メッセージの発行」をクリック。 とすれば、トピックに対しても送信できる。
iOS: 証明書の更新
※p12証明書は定期的な更新が必要。p8証明書にすれば更新は不要になる。 詳細は後述の「iOS: p8証明書」を参照。 iOSへプッシュ通知を送信するための証明書は、一年に一回更新する必要がある。 基本的には再度「iOS: 証明書の作成」を行えばいい。 作業の際 Certificates, Identifiers & Profiles → Identifiers → XC jp refirio pushtest1 (net.refirio.pushtest1) で「Push Notifications」にある「Edit」をクリックすると、 登録済みの証明書の情報が「Development SSL Certificate」と「Production SSL Certificate」のそれぞれに表示される。 更新したい方の証明書で「Create Certificate」ボタンを押せば、新しく証明書を作成する画面に遷移できる。 この作業によって今使っている証明書と差し替えることになるが、もし即座に古い方の証明書が失効されてしまうと、プッシュ通知が届かなくなる。 これを防ぐためだと思われるが、証明書は2つを並行して登録できるようになっている。 (古い方の期限が切れたら、自動的に新しい方だけが使われるようになる。) サーバからプッシュ通知を送信する際、当然ながら新しく作成した証明書を使用する。 AWS SNSのiOS向けPush通知(APNs)証明書の更新手順 - Qiita https://qiita.com/b_a_a_d_o/items/e3bf9cd52b6cd9252088 ■AmazonSNSへの登録 Amazon SNS → プッシュ通知 → 対象のアプリケーションを選択 作業前に「Apple の証明書の有効期限」を確認しておく。 「編集」をクリック。 「プッシュ証明書タイプ」を「iOSプッシュ証明書」にする。 「ファイルの選択」からp12ファイルをアップロードする。 アップロードできたら「認証情報をファイルから読み込み」をクリックし、証明書とプライベートキーが表示されることを確認する。 「変更の保存」をクリック 作業が完了したら、「Apple の証明書の有効期限」が更新されていることを確認する。
iOS: p8証明書
p8証明書については、以下で解説されている。 iOSでのPush通知について | 株式会社ウイングドア https://wingdoor.co.jp/blog/ios%E3%81%A7%E3%81%AEpush%E9%80%9A%E7%9F%A5%E3%81%AB%E3%81%A4%E3%81%84%E... 以下のように紹介されている。 ・Apple Developer Programで登録しているApple IDで管理している全てのアプリで使用可能。 ・本番環境と開発環境で同じ証明書を使用できる。 ・有効期限は無期限。 ・証明書は一度しかダウンロードできない。(再度作成することは可能。) ・一つのApple IDで2つまでしか証明書を作成できない。 p8証明書を使うと、Androidと同じく「永続的なキーが発行されるが、使用するには都度トークンを発行する必要がある」となる。 トークンの発行については、AmazonSNSを使うことで容易に扱えるようになる。 p12の方が証明書の影響範囲が限定的というメリットはあるので、必ずしもp8証明書にしなければならないわけでは無い。 ただしAppleとしてはp8証明書を推奨しているようなので、まずはp8証明書の利用を検討するといい。 Should we use p8 Auth Key or P12 for APNs? | by Kieu Van Phuoc | Medium https://kieuvanphuoc.medium.com/should-we-use-p8-auth-key-or-p12-for-apns-e5737938fde9 Apple Developer Programの「Keys」からキーを作成できるみたい。 curlで実際にプッシュ通知を送信するコードも以下で紹介されている。 iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた - つばくろぐ @takamii228 https://takamii.hatenablog.com/entry/2020/07/13/190027 コマンド(curl)でPushテストする方法(iOS) #Swift - Qiita https://qiita.com/dolfalf/items/2b65c77d11c4e8dbdd9a 以下なども参考になりそう。 iOSのプッシュ通知がp8認証キーに対応しました - ニフクラ mobile backend(mBaaS)お役立ちブログ https://blog.mbaas.nifcloud.com/entry/2021/04/08/202417 iOS プッシュ通知の実装に必要な「p12形式の証明書」と「 p8形式の鍵」について #iOS - Qiita https://qiita.com/kokogento/items/405703f3177ebd2e0320 APNs証明書(p8形式)の発行方法 - FANSHIPサポートガイド https://support.fanship.jp/hc/ja/articles/900006538803-APNs%E8%A8%BC%E6%98%8E%E6%9B%B8-p8%E5%BD%A2%E... ■証明書の取得 APNs証明書(p8形式)の発行方法 - FANSHIPサポートガイド https://support.fanship.jp/hc/ja/articles/900006538803-APNs%E8%A8%BC%E6%98%8E%E6%9B%B8-p8%E5%BD%A2%E... iOS プッシュ通知の実装に必要な「p12形式の証明書」と「 p8形式の鍵」について #Firebase - Qiita https://qiita.com/kokogento/items/405703f3177ebd2e0320 Certificates, Identifiers & Profiles → Keys → + Key Nameを「refirio」とし、さらに下の一覧にある「Apple Push Notifications service (APNs)」にチェックを入れた。 キーは「1アカウントにつき2つまでしか作れない」「案件ごと、アプリごと、本番&検収環境、などで分けることができない」なので、単純に「refirio」という名前にした。 「Continue」をクリック。 確認画面が表示されるので「Register」をクリック。 鍵のダウンロードができるので「Download」をクリックして保存。(「AuthKey_XXXXXXXXXX.p8」という名前だった。) さらに「Done」をクリックして完了。 Keysの画面に戻り、発行済みのキーとして以下が表示された。 KEY ID: XXXXXXXXXX SERVICES: 1 NAME: refirio Apple Developer Programにログインし、「メンバーシップの詳細」から「チームID」を確認しておく。 チームID: YYYYYYYYYY ■JWTトークンの発行 JWT(JSON Web Token)を使った認証を行う。 (Androidでも google/apiclient の内部では同様の仕組みが使われているみたい。) JWTについては以下のページなどを参照。 初心者向けJWT講座:JSON Web Tokenを使った認証の仕組み https://zenn.dev/collabostyle/articles/b08c7f29a2e94c まずは、Composerで必要なライブラリをインストール。
$ composer require firebase/php-jwt
以下のとおりプログラムを作成。(今回は firebase-jwt.php としておく。)
<?php require 'vendor/autoload.php'; use Firebase\JWT\JWT; use Firebase\JWT\Key; // 必要な情報を設定 $keyId = 'XXXXXXXXXX'; // Apple DeveloperのKey ID $teamId = 'YYYYYYYYYY'; // Apple DeveloperのTeam ID $p8FilePath = 'AuthKey_XXXXXXXXXX.p8'; // p8ファイルのパス // p8証明書の読み込み $privateKey = file_get_contents($p8FilePath); if (!$privateKey) { die('p8ファイルが見つかりません。'); } // JWTのヘッダーとペイロードを設定 $now = time(); $payload = [ 'iss' => $teamId, // チームID 'iat' => $now // JWTの発行時刻 ]; $headers = [ 'alg' => 'ES256', // アルゴリズム 'kid' => $keyId // キーID ]; // JWTトークンの生成 $jwt = JWT::encode($payload, $privateKey, 'ES256', $keyId); echo "JWTトークン: " . $jwt . PHP_EOL;
プログラムを実行すると、以下のとおりJWTトークンが発行された。
$ php firebase-jwt.php JWTトークン: eyJ0eXAiOi0000000000bGciOiJFUzI1NiIsImtpZCI6Ikc5MjNOWTRXNVkifQ.eyJpc3MiOiJENVpRUVk5OFpMIiwiaWF0IjoxNzI5ODUwOTMzfQ.kySqsqu5SkhS0WhKWmgou3SUf7188n3AHRvlK1_72aMlw6n3m019pk0cEVQuypx2M8nUr8U4K1cf5rO7Br9zIw
このJWTトークンを使って、以下のコマンドでプッシュ通知を送信できる。
$ curl -v -d '{"aps":{"alert":{"title":"[送信タイトル]","body":"[送信メッセージ]"},"sound":"default"}}' -H "Content-Type: application/json" -H "apns-topic: [アプリのID]" -H "apns-priority: 10" -H "authorization: bearer [JWTトークン]" --http2 https://api.development.push.apple.com/3/device/[デバイストークン]
具体的には、以下のように実行する。
$ curl -v -d '{"aps":{"alert":{"title":"テスト","body":"これはp8ファイルによる送信です。"},"sound":"default"}}' -H "Content-Type: application/json" -H "apns-topic: net.refirio.pushtest1" -H "apns-priority: 10" -H "authorization: bearer eyJ0eXAiOi0000000000bGciOiJFUzI1NiIsImtpZCI6Ikc5MjNOWTRXNVkifQ.eyJpc3MiOiJENVpRUVk5OFpMIiwiaWF0IjoxNzI5ODUwOTMzfQ.kySqsqu5SkhS0WhKWmgou3SUf7188n3AHRvlK1_72aMlw6n3m019pk0cEVQuypx2M8nUr8U4K1cf5rO7Br9zIw" --http2 https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * Trying 17.188.143.66:443... * Connected to api.development.push.apple.com (17.188.143.66) port 443 * ALPN: curl offers h2,http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * TLSv1.2 (OUT), TLS handshake, Client hello (1): * CAfile: /etc/pki/tls/certs/ca-bundle.crt * CApath: none * 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 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 h2 * Server certificate: * subject: C=US; ST=California; O=Apple Inc.; CN=api.development.push.apple.com * start date: Apr 24 18:16:30 2024 GMT * expire date: Apr 10 00:00:00 2025 GMT * subjectAltName: host "api.development.push.apple.com" matched cert's "api.development.push.apple.com" * issuer: CN=Apple Public Server RSA CA 12 - G1; O=Apple Inc.; ST=California; C=US * SSL certificate verify ok. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://api.development.push.apple.com/3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad2... * [HTTP/2] [1] [:method: POST] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: api.development.push.apple.com] * [HTTP/2] [1] [:path: /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2] * [HTTP/2] [1] [user-agent: curl/8.3.0] * [HTTP/2] [1] [accept: */*] * [HTTP/2] [1] [content-type: application/json] * [HTTP/2] [1] [apns-topic: net.refirio.pushtest1] * [HTTP/2] [1] [apns-priority: 10] * [HTTP/2] [1] [authorization: bearer eyJ0eXAiOi0000000000bGciOiJFUzI1NiIsImtpZCI6Ikc5MjNOWTRXNVkifQ.eyJpc3MiOiJENVpRUVk5OFpMIiwiaWF0IjoxNzI5ODUwOTMzfQ.kySqsqu5SkhS0WhKWmgou3SUf7188n3AHRvlK1_72aMlw6n3m019pk0cEVQuypx2M8nUr8U4K1cf5rO7Br9zIw] * [HTTP/2] [1] [content-length: 114] > POST /3/device/d6cb5af49500000000002425020838f4d4792c2de946bce49ad240be4f19c9b2 HTTP/2 > Host: api.development.push.apple.com > User-Agent: curl/8.3.0 > Accept: */* > Content-Type: application/json > apns-topic: net.refirio.pushtest1 > apns-priority: 10 > authorization: bearer eyJ0eXAiOi0000000000bGciOiJFUzI1NiIsImtpZCI6Ikc5MjNOWTRXNVkifQ.eyJpc3MiOiJENVpRUVk5OFpMIiwiaWF0IjoxNzI5ODUwOTMzfQ.kySqsqu5SkhS0WhKWmgou3SUf7188n3AHRvlK1_72aMlw6n3m019pk0cEVQuypx2M8nUr8U4K1cf5rO7Br9zIw > Content-Length: 114 > < HTTP/2 200 < apns-id: 5371130C-0127-0D02-AA12-41C29AA8EE5B < apns-unique-id: 7def9a2f-df7f-e8fc-778c-04337c567bf9 < * Connection #0 to host api.development.push.apple.com left intact
■AmazonSNSへの登録 AmazonSNS → プッシュ通知 → PushTest1-APNS-Dev → 編集 認証方法: トークン 署名キー: (上の手順で取得したp8ファイル。) 署名キーID: XXXXXXXXXX チームID: YYYYYYYYYY バンドルID: net.refirio.pushtest1 「変更の保存」ボタンをクリック。 PushTest1-APNS-Dev の画面に戻る。 しばらくしてページを再読み込みすると、 認証方法: Certificate 証明書の有効期限: 2025-11-24T04:45:55Z と表示されていた部分が 認証方法: Token AppleバンドルID: net.refirio.pushtest1 AppleチームID: YYYYYYYYYY と表示されるようになった。 この状態でAmazonSNS経由でプッシュ通知を送り、アプリに届くことを確認する。
アプリから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) ※「OkHttp」を使うといいかもしれない。以下に検証メモがある。 Dropbox\技術\AndroidStudio.txt ※以下は「OkHttp」を使わずに実装する例。 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(Swift) 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() }
アプリ起動時にデバイストークンをサーバ側に記録
「アプリからHTTPリクエストする例」の内容も踏まえて、アプリ起動時にデバイストークンをサーバ側に記録する例。 Androidについては、ChatGPTが「OkHttp」ではなく「Volley」を使ったコードを提示してきたので、いったんそのまま採用している。 Androidの通信ライブラリの歴史を振り返る #Java - Qiita https://qiita.com/Reyurnible/items/33049c293c70bd9924ee ※実際の実装では、さらに「デバイストークン」「サーバ側で発行した一意なID」をデバイス内に保存しておくなどの処理もあると良さそう ■サーバサイド(save_device_token.php)
<?php header('Content-Type: application/json; charset=utf-8'); if (file_put_contents('log/' . date('YmdHis') . '.log', print_r($_POST, true)) === false) { echo json_encode(array( 'status' => 'NG', )); } else { echo json_encode(array( 'status' => 'OK', 'datetime' => date('Y/m/d H:i:s'), )); } exit;
■アプリ(Android)
package net.refirio.pushtest1 import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat import com.android.volley.Request import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley import com.google.firebase.messaging.FirebaseMessaging import net.refirio.pushtest1.ui.theme.PushTest1Theme class MainActivity : ComponentActivity() { private var tokenState: MutableState<String?>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PushTest1Theme { val localTokenState = remember { mutableStateOf<String?>(null) } tokenState = localTokenState Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MainScreen(modifier = Modifier.padding(innerPadding), localTokenState) } } } // Firebaseのトークンを取得 FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w("MainActivity", "Fetching FCM registration token failed", task.exception) return@addOnCompleteListener } // トークンを取得 val token = task.result Log.d("MainActivity", "FCM Token: $token") tokenState?.value = token // サーバにトークンを送信 sendDeviceToken(token) } } private fun sendDeviceToken(token: String) { val url = "https://example.com/app/save_device_token.php" // URLエンコード形式のデータを作成 val postBody = "device_token=${java.net.URLEncoder.encode(token, "UTF-8")}" // リクエストを送信 val request = object : StringRequest( Request.Method.POST, url, { response -> // 成功時の処理 Log.d("MainActivity", "Token saved successfully: $response") }, { error -> // エラー時の処理 Log.e("MainActivity", "Failed to save token: $error") } ) { override fun getHeaders(): Map<String, String> { val headers = HashMap<String, String>() headers["Content-Type"] = "application/x-www-form-urlencoded" // URLエンコード形式 return headers } override fun getBody(): ByteArray { return postBody.toByteArray(Charsets.UTF_8) // ボディにエンコード済みのデータを設定 } } // リクエストキューに追加 Volley.newRequestQueue(this).add(request) } } fun checkAndRequestPermissions( context: Context, permissions: Array<String>, launcher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>> ) { if ( permissions.all { ContextCompat.checkSelfPermission( context, it ) == PackageManager.PERMISSION_GRANTED } ) { // パーミッションが与えられている Log.d("MainActivity", "Already granted") } else { // パーミッションを要求 launcher.launch(permissions) } } @Composable fun MainScreen(modifier: Modifier = Modifier, tokenState: MutableState<String?>) { val context = LocalContext.current val permissions = arrayOf( Manifest.permission.POST_NOTIFICATIONS ) val launcherMultiplePermissions = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissionsMap -> val areGranted = permissionsMap.values.reduce { acc, next -> acc && next } if (areGranted) { // パーミッションが与えられている Log.d("MainActivity", "Already granted") } else { // パーミッションを要求 Log.d("MainActivity", "Show dialog") } } Column(modifier = modifier) { Text("FCM Token: ${tokenState.value ?: "Loading..."}") Button( onClick = { checkAndRequestPermissions( context, permissions, launcherMultiplePermissions ) } ) { Text("通知を許可(Android13用)") } } } @Preview(showBackground = true) @Composable fun MainScreenPreview() { var tokenState: MutableState<String?>? = null val localTokenState = remember { mutableStateOf<String?>(null) } tokenState = localTokenState MainScreen(modifier = Modifier, localTokenState) }
■アプリ(iOS)
import SwiftUI struct ContentView: View { let publisher = NotificationCenter.default.publisher(for: .deviceToken) @State var deviceToken: String = "" var body: some View { VStack { Text("DeviceToken") TextField("Device Token is Empty", text: $deviceToken) .padding() } .onReceive(publisher) { message in if let deviceToken = message.object as? String { sendDeviceToken(deviceToken) } } } private func sendDeviceToken(_ token: String) { guard let url = URL(string: "https://example.com/app/save_device_token.php") else { return } var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = "device_token=\(token)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)?.data(using: .utf8) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { print("Error: \(error)") return } guard let data = data else { return } do { let object = try JSONSerialization.jsonObject(with: data, options: []) print(object) DispatchQueue.main.async { self.deviceToken = token } } catch { print("JSON parsing error: \(error)") } } task.resume() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
トラブル
■プッシュ通知が届かない 以下などを確認する。 ・アプリのパッケージ名を確認し、それに対応する設定が行われているか。 ・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件以上取る場合は工夫が必要かも。 トークン管理のために 「無効になったトークンをすべて取得して何らかの処理をする」 とするよりも、上のリンク先で紹介されているように 「アプリ起動時に毎回トークンの有効/無効を確認する。無効ならトークンを差し替える」 とする方がいいかも。 ■その他 プッシュ通知はどうしても 「GoogleやAppleが作ったブラックボックスを利用する」 となるので、そういった面でのリスクは常に発生する。 プッシュ通知の未達/遅延問題に真正面から取り組むLINE公式アカウント開発チーム - LINE ENGINEERING https://engineering.linecorp.com/ja/interview/aim-at-100-push-notification-knowledge-obtained/ > 通知の送信には、AndroidであればGoogle、iOSはAppleの仕組みを利用することになり、その仕組みは我々からするとブラックボックスであり、すべてをつまびらかにすることはできない > Androidの中に、該当アプリで一定数以上の通知がすでに表示されていると、それ以上の通知を抑止するというロジックが組み込まれていた > LINE公式アカウントのように、通知量が多くなるアプリではクリティカルな問題になる可能性があるので注意が必要でしょう
考察: アプリのIDについて
※アプリは本番用と開発用でパッケージ名を変える方が無難。 (1端末に両方インストールできるようにする。) ※アプリのIDは、アプリ名を初期として自動作成される。例えば「PushTest1-Dev」というアプリ名で普通に作ると Android ... net.refirio.pushtest1dev iOS ... net.refirio.PushTest1-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 ※「PushTest1」などの名前はすべて「PushTest1-Dev」のようにしておく方がいいかも。 プッシュ通知の証明書なども開発用と本番用が必要のため。
考察: 処理の流れについて
※ログインして使うアプリの設計メモ。 ※ユーザ情報のテーブルとデバイス情報のテーブルがある想定。 ※「ログインしなくても基本機能は使える」「プッシュ通知の送信を許可しなかった」も考慮する。 ※デバイストークンは非同期で取得されるので、 「アプリ起動時に端末情報を同期」と「アプリ起動時にデバイストークンを同期」の計2つのAPIが必要になる。 ■前提 起動時にアプリから実行するAPIは以下の2つ。 ただしプッシュ通知が許可されない(デバイストークンを取得できない)場合は前者のみ実行する。 アプリ起動時に端末情報を同期。(OSバージョンやアプリバージョンなど、デバイストークン以外の情報を同期。) /api/device/sync アプリ起動時にデバイストークンを同期。 /api/device/sync_token ■アプリ初回起動時 「アプリ起動時に端末情報を同期」を使用し、アプリからPHPに、OSバージョンやアプリバージョンなどを送る。 PHPがDBに、OSバージョンやアプリバージョンなどを記録する。さらに全端末でユニークな識別コードを作成し、合わせてDBに保存する。 PHPからアプリに、識別コードを返す。アプリはこの値を保存しておく。 プッシュ通知が許可されるとデバイストークンを取得できるようになる。 「アプリ起動時にデバイストークンを同期」を使用し、アプリからPHPに、識別コードとともにデバイストークンを送る。 PHPがAmazonSNSから、エンドポイントを取得する。 PHPがDBに、デバイストークンとエンドポイントを記録する。 PHPからアプリに、識別コードを返す(正常終了の判定などに使う。) ■アプリ次回起動時 「アプリ起動時に端末情報を同期」を使用し、アプリからPHPに、識別コードとともにOSバージョンやアプリバージョンなどを送る。 PHPがDBに、OSバージョンやアプリバージョンなどを記録する。(最新情報として上書き更新する。) プッシュ通知が許可されていればデバイストークンを取得できる。 「アプリ起動時にデバイストークンを同期」を使用し、アプリからPHPに、識別コードとともにデバイストークンを送る。 PHPがAmazonSNSから、エンドポイントを取得する。 PHPがDBに、デバイストークンとエンドポイントを記録する。(対象データは、識別コードをもとに判断する。) PHPからアプリに、識別コードを返す。(正常終了の判定などに使う。) ■プッシュ通知拒否時 アプリ初回起動時にプッシュ通知の送信を拒否した場合、デバイストークンの取得ができない。 この場合、「プッシュ通知が許可されていれば」の処理は行われないので、デバイストークンやエンドポイントは無いままで動作する。 ■端末ごとの設定 端末ごとの設定を持ちたければ devices のレコードに保存する。 必要に応じて、設定は端末内にもキャッシュとして保存しておく。(インターネットに繋がっていなくても設定内容を参照できるように。) ■ログイン時 アプリ内でユーザ名とパスワードを入力し、その値をPHPに送る。 認証情報が正しければ、データベーステーブルのデバイス情報をユーザ情報に紐付ける。 デバイス情報にはログイン中か否かのステータスも持たせておき、そのステータスをログイン中にする。 (厳密なログイン判定が不要なら、デバイス側に単純なログインフラグを持たせておくか。厳密なログイン判定が必要なら、ユーザ情報にも識別コードを持たせてそれをアプリ内に記録させ、アプリ起動時に毎回サーバ側でチェックするか。) 以降のログインは、アプリ内に保存されている識別コードをもとに行う。 ■別端末でログイン時 上とまったく同じ流れでデバイストークンやエンドポイントを記録する。 端末情報テーブルには、同じユーザIDのデータが別途作成される。 ■ログアウト時 アプリからPHPに、デバイストークンを投げてくる。 データベーステーブルのデバイス情報とユーザ情報の紐付けを解除する。 ログイン中か否かのステータスをログアウトにする。 ■ログアウトして別ユーザでログイン時 「ログアウト時」の手順でログアウトし、「ログイン時」の手順でログインする。 ■プッシュ通知送信時 PHPがAmazonSNSを使って、エンドポイントに対してプッシュ通知を送信する。 1ユーザが複数の端末を持っていれば、それぞれにプッシュ通知が送信される。 ■セッションタイムアウト時 デバイストークンはDBにあるので、それをもとにプッシュ通知は届き続ける。 アプリを立ち上げると、その時点でデバイストークンの更新処理が走る。 ※iOSアップデートのタイミングでデバイストークンが変わる可能性があるらしい。 デバイストークンが変わるタイミングは不定らしいので、デバイストークンがいつ変わっても大丈夫な仕組みにする必要がある。 iOS 9からAPNsデバイストークンがアプリインストールの度に変わるようになったようです - Qiita https://qiita.com/mono0926/items/9ef83c8b0de0e84118ac
考察: テーブル設計
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 'エンドポイント', disabled DATETIME COMMENT '無効日時', notification TINYINT(1) UNSIGNED NOT NULL COMMENT '通知', # この設定は不要か。OSレベルで通知がOFFでもとりあえず送信&受け取りは行われる?アプリ内で設定のON/OFFを設ける場合には必要そう 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
考察: アプリ用API
※ログインの不要な実案件用に設計したもの。 ログインも考慮してAPIを追加調整しておきたい。 ■アプリ起動時に端末情報を同期 url: /api/device/sync method: post parameters: _type ... 「json」で固定 code ... 識別コード or 空欄 platform ... プラットフォーム(「ios」もしくは「android」) os_version ... OSバージョン app_version ... アプリバージョン response { "status": "OK", "code": "識別コード", "notification": "通知", ... 「0」もしくは「1」 # OSレベルで通知がOFFでもとりあえず送信&受け取りは行われるみたい。いったん常に1を返すようにした "option1": "デバイスごとの設定例1", ... デバイスごとの設定値 "option2": "デバイスごとの設定例2", ... デバイスごとの設定値 "option3": "デバイスごとの設定例3" ... デバイスごとの設定値 } ※返した各値は、アプリ側で値を保持しておく。 ※初回はプッシュ通知の使用許可を得る前なので、notification は常に 0 が返される。 ■アプリ起動時にデバイストークンを同期 url: /api/device/sync_token method: post parameters: _type ... 「json」で固定 code ... 識別コード platform ... プラットフォーム(「ios」もしくは「android」) token ... デバイストークン response { "status": "OK", "code": "識別コード", "notification": "通知" ... 「0」もしくは「1」 # OSレベルで通知がOFFでもとりあえず送信&受け取りは行われるみたい。いったん常に1を返すようにした。 } ※返した notification は、アプリ側で値を保持しておく。 ※初回起動直後にこのAPIを叩いた場合、プッシュ通知の使用許可を得た直後なので notification には 1 が記録される。つまり必ず 1 が返される。 初回以降は notification は変更せずに現状の値を返す。 (デバイストークンは取れるが、アプリの設定でプッシュ通知は受け取らないようにした…という状態に対応できるように。) ■端末情報を取得 url: /api/device/get method: get parameters: _type ... 「json」で固定 code ... 識別コード or 空欄 response { "status": "OK", "code": "ZZZZZZZZZZ", "notification": "通知", # 設定画面から呼ばれることは無いので削除するか "option1": "デバイスごとの設定例1", "option2": "デバイスごとの設定例2", "option3": "デバイスごとの設定例3" } ■端末情報を更新 url: /api/device/post method: post parameters: _type ... 「json」で固定 code ... 識別コード notification ... 通知(更新したい場合) # 設定画面から呼ばれることは無いので削除するか option1 ... デバイスごとの設定例1 option2 ... デバイスごとの設定例2 option3 ... デバイスごとの設定例3 response { "status": "OK", } ■テスト用 url: /api/test method: get response { "status": "OK", "datetime": "(現在日時)" }
考察: アプリ用プッシュ通知
以下のデータ構造でプッシュ通知を送信する。
$message = 'メッセージ'; $target_arn = '送信先'; // エンドポイント $title = 'タイトル'; // 現状常に null が入る $url = 'URL'; // 「/webview/news」などアプリで開くべきURLが入る。null が入る可能性がある // アプリから「/webview/redirect?url=/webview/news」のように開くと、 // プッシュ通知タップ回数を加算したうえでURLのページへリダイレクトする(要セッション) $gcm = json_encode([ 'data' => [ 'title' => $title, 'body' => $message, 'url' => $url ], ]); $apns = json_encode([ 'aps' => [ 'alert' => [ 'title' => $title, 'body' => $message, ], 'badge' => 1, 'sound' => 'default' ], 'url' => $url ]); $message = [ 'default' => $message, 'GCM' => $gcm, 'APNS' => $apns, 'APNS_SANDBOX' => $apns, ]; $parameter = [ 'Message' => json_encode($message), 'MessageStructure' => 'json', 'TargetArn' => $target_arn, ];
以降の処理で $parameter の内容を送信。
考察: 本番公開用に作成する(上に整理する前の考察メモ)
■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プロジェクトを開発版用に用意する必要がある。 以下、本番用と開発用に分ける手順。(Zabbixからの通知を受け取るためのアプリを作成したときのもの。) まずは本番&開発などを一緒に管理するためのプロジェクトを作成しておく。 通常の手順で、Firebase本番想定のアプリを作成しておく。 Android パッケージ名: net.refirio.zabbix アプリのニックネーム: Zabbix 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.debug などを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',」もコメントアウトすると、プッシュ通知受診時のサウンドやバイブレーションも再生されない。 この仕組を使えば「サーバからの指示によって、裏側でこっそり任意の処理を行わせる」ができそう。 ただし当然ながら、プッシュ通知が許可されていなかったりオフラインになっていたりすると実行できないので注意。 ■その他参考になりそうなページ 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/

Advertisement