しがないエンジニアのブログ

技術的な内容をメモ代わりにつらつら

Google Cloud Messaging(GCM)からFirebase Cloud Messaging(FCM)への移行(android)

GCM/FCMとは

GCMとは、android端末のpush通知をサポートするクラウドサービスのこと
しかし、GCMは2019.4.11でサポートが完全に停止する

そのかわり、googleは新しくFCMの利用を呼びかけている
またFCMはandroidだけでなく、iOSもサポート対応のクロスプラットフォームである

移行のきっかけ

なぜ移行しようという話になったのかだが、サポート終了するっていうのは調べ始めてから発覚したこと。
そもそもの理由としては、targetSdkVersion 26以上じゃないとだめだよっていうgoogleのお達しがあったので、バージョンあげたことが発端だった。
2018年8月からtargetSdkVersionをAPIレベル26(Android 8.0)以上にする必要がある

バージョンアップをしたところ、バックグラウンドでの通知が届かなくなってしまったのである。
アプリが開いている状態なら問題なく通知が届くのだが、アプリを終了すると以下のようにエラーがでて落ちてしまっていた。

f:id:turgure:20180904182731p:plain:w300

そしてログもめっちゃ吐かれてた

09-03 16:30:23.564 27698-27698/your-package E/AndroidRuntime: FATAL EXCEPTION: main
    Process: your-package, PID: 27698
    java.lang.RuntimeException: Unable to start receiver com.google.android.gcm.GCMBroadcastReceiver: java.lang.IllegalStateException: Not allowed to start service Intent { act=com.google.android.c2dm.intent.RECEIVE flg=0x1000010 pkg=your-package cmp=your-package/.GCMIntentService (has extras) }: app is in background uid UidRecord{a11f8da u0a520 RCVR idle procs:1 seq(0,0,0)}
        at android.app.ActivityThread.handleReceiver(ActivityThread.java:3267)
        ...

社内の人に相談したところ、「FCM使ってますけどそんなの見たことないですよ?」って言われたので、FCMに移行することにした。

移行の手順

基本的には以下のURLの通りに進めれば問題ない
Migrate a GCM Client App for Android to Firebase Cloud Messaging  |  Cloud Messaging  |  Google Developers

1 . 以下のURLからfirebaseプロジェクトの作成
  https://console.firebase.google.com

google-services.jsonをプロジェクトに追加する必要があるため、その手順

1.1. 対象のプラットフォームを選ぶ(この記事ではandroidを扱う)
f:id:turgure:20180904174238p:plain:w400

1.2. 必要事項の入力
f:id:turgure:20180904171149p:plain:w400

証明書の確認は以下のように行う

$ keytool -v -list -alias androiddebugkey -keystore ~/.android/debug.keystore
$ keytool -v -list -alias <your-key-name> -keystore <path-to-production-keystore>

https://developers.google.com/android/guides/client-auth

1.3. google-services.jsonをダウンロード
  app/直下に配置する
f:id:turgure:20180904171615p:plain:w400

1.4. それぞれのbuild.gradeに追記してsync
  最新のバージョンがあればandroid studioが教えてくれるのでそれに合わせる
f:id:turgure:20180904171857p:plain:w400

1.5. アプリを実行、同期確認
f:id:turgure:20180904172228p:plain:w400

2 . build.gradleの変更
3 . AndroidManifest.xmlの変更

自分が行ったものをdiff形式でぺたり

4 . server endpointsの変更

https://android.googleapis.com/gcm/send

http://fcm.googleapis.com/fcm/send

に変更

5 . その他設定
  それぞれの実装状況に合わせて対応部分を修正

自分の所属プロジェクトでは、真ん中の
Optional: migrate your GcmListenerService
が修正対象でした

簡単に説明すると、AndroidManifest.xmlの変更と、serviceの変更

remoteMessageからデータを取得するとき、一般的な記事ではgetNotification()とかを使ったら値がnullになっていた
そこで、従来どおりintent経由で取得する方法として、toIntent()というまさに求めていた機能があったのでこれを利用
(あとで確認したらgetData()の方にはデータが入ってたのでこっち使ったほうがいいかも?)

Bundle extras = intent.getExtras();

Bundle extras = remoteMessage.toIntent().getExtras();

認証まわりはPHPを使ってたので元から特に書いてなかった

これで無事に今までどおり通知が届くようになった


参考URL

claspを使ったGoogle App Scriptのローカル開発・自動コンパイル

claspとは

claspは、Google App Script(GAS)をローカル環境で開発するためのgoogle製ツールである
ローカルで作業できる一番のメリットは、やはりgitとの連携が可能になることだろう

しかし、gasはES6には対応しておらず、イマドキのjsのコーディングスタイルは使えない
そこで本記事では、本ブログの前回の記事を利用して、

  1. ES6対応のjsコードで開発
  2. babelを使ってECMAScript5へコンパイル
  3. claspを使ってコンパイルしたファイルをgas上のスクリプトと同期(push)

をすべて自動で行えるようにする

claspのセットアップ

1 . claspのインストール

npm install -g @google/clasp

googleアカウントをプロジェクトごとに使い分ける場合はローカルにインストールし、package.jsonのscriptにloginみたいなのを作ればいいと思う

2 . 以下のサイトでGoogle Apps Script APIをオンにする
  https://script.google.com/home/usersettings

f:id:turgure:20180820145215p:plain

3 . ログインする

clasp login

ここまでの作業は最初の一度だけ行えばよい

ローカル環境の構築

claspの設定

clasp create <project_name>

とすれば、gasが連携したアカウントのgoogle drive上に作成され、ローカルに.clasp.jsonappsscript.jsonの2つのファイルが生成される

それぞれ以下のようなファイルになっている


.clasp.jsonに記述するscript idを取得し、手動で作成する
idの場所は
  ・ gasのURL
  ・ ファイル > プロジェクトのプロパティの「スクリプトID」
のいずれかから取得する(どちらも同じid)

前まではプロジェクトキーを使っていたが、現在は変わったらしい
古いプロジェクトだとurlがプロジェクトキーのものになっていて使えないので要注意

f:id:turgure:20180820155836p:plain
f:id:turgure:20180820170546p:plain

そして

clasp pull

を実行することで、google drive上のgasファイルをローカルに同期することができる

.claspignoreの追加

基本的に、ローカルのファイルをgas上に同期(push)しようとすると、すべてのファイルが同期されてしまう
そのため、.claspignoreファイルを作成することで、どのファイルをpushするかを決める(.gitignoreと同じ役割)
このとき、pushするファイルにはappsscript.jsonが必ず含まれていなくてはいけない
以下のようなファイルを作成するとよい(main.jsはgas上に同期するファイル)

(余談)
本来なら、.claspignoreなど作らずとも、.clasp.jsonのプロパティにrootDirなるものが存在し、それを指定すれば自動的にそのディレクトリ内のファイルのみをターゲットとしてpushするファイルを決めてくれるはず、だった。
しかし、appsscript.jsonがどう頑張ってもignoreされてしまい、2018/08/20現在まだその問題が続いている

$ clasp status
Not ignored files:
└─ dist/main.js

Ignored files:
└─ dist/appsscript.json

なので、今は諦めて.claspignoreを使おう
Command Failed: clasp push · Issue #281 · google/clasp · GitHub

babelの設定

前回の記事を参考に構築
そして、package.jsonを以下のように書き換える

"scripts": {
  "start": "watch 'npm run build' src/",
  "build": "node index.js && npm run push",
  "push": "clasp push"
}

build後にclasp pushを実行するコマンドを追加


これで、前回同様

$ npm start

を実行すれば、自動的にgas上のファイルの変更まで行うことができる

gasのファイルを変更しても、ブラウザ更新しないと反映されないので一瞬ドキッとする…
なにか組み合わせてできないものだろうか


追記:20181218

20181218時点でclaspの最新版が 1.7.0だったので、それに移行した。

$ npm info @google/clasp versions
[ '1.0.0',
  '1.0.1',
  '1.0.2',
  '1.0.3',
  '1.0.4',
  '1.0.5',
  '1.0.6',
  '1.0.7',
  '1.1.0',
  '1.1.1',
  '1.1.2',
  '1.1.3',
  '1.1.4',
  '1.1.5',
  '1.3.0',
  '1.3.1',
  '1.3.2',
  '1.3.3',
  '1.4.0',
  '1.4.1',
  '1.5.0',
  '1.5.1',
  '1.5.2',
  '1.5.3',
  '1.6.0',
  '1.6.1',
  '1.6.2',
  '1.6.3',
  '1.7.0' ]

$ npm install @google/clasp@1.7.0 -g
/usr/local/bin/clasp -> /usr/local/lib/node_modules/@google/clasp/src/index.js
+ @google/clasp@1.7.0
added 16 packages from 11 contributors, removed 4 packages and updated 32 packages in 8.627s

$ npm list --depth=0 -g
/usr/local/lib
├── @google/clasp@1.7.0
├── npm@6.4.0
├── npm-check-updates@2.15.0
└── pure-prompt@1.7.0

また、このときに、このまま使おうとすると、以下のエラーが発生する

$ clasp push
Error retrieving access token: TypeError: Cannot read property 'expiry_date' of undefined

そのため、再度ログインし直す

$ clasp logout
$ clasp login

1.7.0に移行したことで、 .clasp.jsonのrootDirを指定してpushするディレクトリを選べるようになった。


参考サイト

babelを使ったjavascriptの自動コンパイル

Babel

ECMAScript2015 (ES6)やECMAScript7などで書かれたソースコードを一般的なブラウザがサポートしているECMAScript5の形式に出力することができます。

つまり、新しい言語仕様を昔の仕様に落としてトランスコンパイルしてくれるコンパイラ

セットアップ

babel v6.0以降、pluginまたはpresetの指定が必要になっている
基本的には.babelrcというファイルに記述するのだが、設定が少ないため、package.jsonに合わせて記述する

"babel": {
  "presets": [
    "env"
  ]
}

envというのは、コンパイル環境を自動で決定してくれるすぐれもの

追記 20180827】
index.js(下記参照)にenvの設定をして、そこを参照していたので、package.jsonに追加する必要はありませんでした。


あわせて、次の2つのパッケージをインストール

npm install babel-cli
npm install babel-preset-env

コンパイラの作成

実際にコンパイルするためには、babelコマンドが必要になってくる
それをjs側で管理して実行できるようにする
ファイル名は、package.jsonのmainタグで管理されているファイル名に設定(デフォルトはindex.js

const babel  = require('babel-core')
const fs     = require('fs')

const srcFile  = 'src/test.js'
const distFile = 'dist/test.js'
const options  = {"presets": ["env"]}

const code = fs.readFileSync(srcFile, 'utf-8')
const newCode = babel.transform(code, options).code
fs.writeFileSync(distFile, newCode)

console.log('babel build finished!')

サンプルとして、ここでは
src/test.jsの内容を、
dist/test.jsコンパイルして出力するようにしている

例えば、

class Hoge {
  constructor() {
    this.x = 3;
    this.y = 2;
  }
  add() {
    return this.x + this.y;
  }
}
const exp = (x, y) => x ** y;
const hoge = new Hoge();
console.log(exp(hoge.x, hoge.y));
console.log(hoge.add());

を変換する。
$ node index.jsを実行すると、

"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Hoge = function () {
  function Hoge() {
    _classCallCheck(this, Hoge);

    this.x = 3;
    this.y = 2;
  }

  _createClass(Hoge, [{
    key: "add",
    value: function add() {
      return this.x + this.y;
    }
  }]);

  return Hoge;
}();

var exp = function exp(x, y) {
  return Math.pow(x, y);
};
var hoge = new Hoge();
console.log(exp(hoge.x, hoge.y));
console.log(hoge.add());

になる。ちゃんと実行もできる

自動コンパイル環境の構築

今のままだと、変更・保存して毎回node index.jsを叩かなくてはいけない
そのため、ファイルを変更したら自動でコンパイルしてくれるようにする
gulpやgruntは使わない

まず、ファイルの変更を監視するwatchというパッケージがあるので、それをインストールする

npm install watch

次に、package.jsonのscriptを変更する

"scripts": {
  "start": "watch 'npm run build' src/",
  "build": "node index.js"
}

これは、src/下のファイルが変更されたら、npm run buildを実行するということ
そして、buildには、先程も実行していたnode index.jsを記述している

これで、

$ npm start

を実行するだけ
あとは、変更を加えるたびに自動でコンパイルしてくれるようになる


参考サイト