顯示具有 notification 標籤的文章。 顯示所有文章
顯示具有 notification 標籤的文章。 顯示所有文章

2017年10月18日 星期三

Firebase 開發筆記 - FCM 與 Web app debug

之前已經小試身手了,在此把 Web app notification debug 訊息紀錄一下。

1. 關於發 push 要用的 Serverkey 擺在 Firebase -> Project -> Settings -> Project settings -> CLOUD MESSAGING -> Project credentials 的 Server key

2. 初始化 index.html ,請至 Firebase -> Project -> Overview -> Add Firebase to your web app,把他埋在 <head></head> 即可

<script src="https://www.gstatic.com/firebasejs/4.5.2/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "YourAPIKey",
    authDomain: "your-app.firebaseapp.com",
    databaseURL: "https://your-app.firebaseio.com",
    projectId: "your-app",
    storageBucket: "your-app.appspot.com",
    messagingSenderId: "your-app-id"
  };
  firebase.initializeApp(config);
</script>


3. 使用 const messaging = firebase.messaging(); 來操作,如要求收訊權限,取得後則進行要 FCM token:
<script>
const messaging = firebase.messaging();
messaging.requestPermission()
.then(function() {
console.log('Notification permission granted.');
try_to_get_token();
})
.catch(function(err) {
console.log('Unable to get permission to notify.', err);
});
function try_to_get_token() {
messaging.getToken()
.then(function(currentToken) {
if (currentToken) {
console.log("currentToken:", currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
})
.catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
});
}
</script>


4. 開發上請使用 Chrome browser ,想要重置 FCM token 可以在 chrome://settings/content/notifications 將指定網域清除

5. 碰到要不到 FCM token 錯誤訊息

browserErrorMessage: "Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script."
code: "messaging/failed-serviceworker-registration"
message: "Messaging: We are unable to register the default service worker. Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script. (messaging/failed-serviceworker-registration)."


解法一:在根目錄建置一個空的檔案,檔名為 firebase-messaging-sw.js (最方便的解法)

$ touch /path/www/document_root/firebase-messaging-sw.js

解法二:刻一個 ServiceWorker 處理 (彈性的解法)

$ touch sw.js (與 index.html 同層即可)
$ vim index.html
<script>
const messaging = firebase.messaging();

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
messaging.useServiceWorker(registration);
//try_to_get_token();
messaging.requestPermission()
.then(function() {
console.log('Notification permission granted.');
try_to_get_token();
})
.catch(function(err) {
console.log('Unable to get permission to notify.', err);
});

}).catch(function(err) {
//registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}
</script>


6. 建置收訊息的部分:

前景:
<script>
const messaging = firebase.messaging();
messaging.onTokenRefresh(function() {
console.log("onTokenRefresh");
try_to_get_token();
});

messaging.onMessage(function(payload) {
console.log("Message received. ", payload);
});

if ('serviceWorker' in navigator) {
console.log("test 'serviceWorker' in navigator");
navigator.serviceWorker.register('sw.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
messaging.useServiceWorker(registration);
//try_to_get_token();
messaging.requestPermission()
.then(function() {
console.log('Notification permission granted.');
try_to_get_token();
})
.catch(function(err) {
console.log('Unable to get permission to notify.', err);
});

}).catch(function(err) {
//registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}

function try_to_get_token() {
messaging.getToken()
.then(function(currentToken) {
if (currentToken) {
console.log("currentToken:", currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
})
.catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
});
}
</script>


背景(/firebase-messaging-sw.js 或是自訂的 ServiceWorker):

$ vim sw.js
// Import and configure the Firebase SDK
// These scripts are made available when the app is served or deployed on Firebase Hosting
// If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup
importScripts('https://www.gstatic.com/firebasejs/4.5.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.5.2/firebase-messaging.js');
importScripts('https://www.gstatic.com/firebasejs/4.5.2/firebase.js');

  // Initialize Firebase
  var config = {
    apiKey: "YourAppApiKey",
    authDomain: "your-app.firebaseapp.com",
    databaseURL: "https://your-app.firebaseio.com",
    projectId: "your-app",
    storageBucket: "your-app.appspot.com",
    messagingSenderId: "your-app-id"
  };
  firebase.initializeApp(config);

const messaging = firebase.messaging();

// If you would like to customize notifications that are received in the
// background (Web app is closed or not in browser focus) then you should
// implement this optional method.
// [START background_handler]
messaging.setBackgroundMessageHandler(function(payload) {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // Customize notification here
  const notificationTitle = 'Background Message Title';
  const notificationOptions = {
    body: 'Background Message body.',
    icon: '/firebase-logo.png'
  };

  return self.registration.showNotification(notificationTitle,
      notificationOptions);
});
// [END background_handler]


7. 背景工作補充 /firebase-messaging-sw.js (或是自訂的 ServiceWorker)

當 /firebase-messaging-sw.js (或是自訂的 ServiceWorker) 是空白時,這時 web app (chrome browser) 都到訊息時,只會提醒有訊息在背景更新,且前景都不會觸發 messaging.onMessage 的事件。

8. 若發現異常,例如 Web 前景有 token 又收不到訊息時,請留意背景工作是否有實作,建議把 chrome browser 重開,仍不行時,甚至在 chrome://settings/content/notifications 清除重新來過

9.善用 curl command 測試:

curl -X POST -H "Authorization: key=YourAppServerKey" -H "Content-Type: application/json" -d '{
  "notification": {
    "title": "Hello",
    "body": "World",
    "icon": "firebase-logo.png",
    "click_action": "http://localhost:8081"
  },
  "to": "FCM token"
}' "https://fcm.googleapis.com/fcm/send"


10. 若 user 移除接收,送訊息時會收到回饋:

{"multicast_id":####,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"NotRegistered"}]}

11. FCM 跟用戶有沒有登入(Google帳號)無關

2017年10月17日 星期二

Firebase 開發筆記 - Firebase Cloud Messaging (FCM) 初體驗、 Topic 管理與 Web notification

firebase-web-console

最近強者同事推薦使用 FCM ,看了一下,對於計費方式有點抖,若一切免費的話,真的超佛心 :P 由於支援潮流的訊息訂閱架構,基本上幾乎可以不用自行紀錄 notification token 了,用起來很直觀又方便。例如,想要個別通知時,可以每一個 user 給予一個 topic id (如 user id) ,就能夠不用紀錄 raw notification token 單獨發訊給對方!有一點點像用運算取代空間感。

當發送 push 給使用者後,且使用者取得訊息時,關注 Realtime db 的 Download 流量有變動,不知是不是真的要計算流量?若是的話,大約發送一則訊息算 2KB ,而免費版流量 10GB/month,約一個月可以發送 500萬則訊息。

firebase-rt-db-download

若真的要計算流量費的話,那 FCM 就不是免錢,接著要花錢則是 25 美金方案有 20GB/month ,接著更高級則是 $1/GB 計費。

Updated @ 2017/10/18: 與 Firebase 客服聯繫,確認 FCM 不佔用 Realtime Database 流量!並且一口氣發送 100 則後,流量也沒有大量增加,符合預期!超佛心。強者同事提醒,對於 Database Download 流量,可能是 Firebase dashboard/console 的資訊讀寫 ( https://firebase.google.com/docs/database/usage/billing - Firebase console data )

聊聊趣味的地方,關於 Topic 管理機制,當你有 FCM token 後,可以在 server site 幫 device 訂閱 topic 喔!這功能很便利,就像...你紀錄了一堆 GCM token / APNs token 後,自己過濾對象,再進行發送。然而,topic 概念在於"使用情境"知道後,直接刻在 app 端,只是突然新增使用情境時,就得變成新版 app user 可以接受,舊版則無法接受到訊息,這時 server site 若有記錄 FCM token 時,就可以代勞幫忙訂閱:

https://developers.google.com/instance-id/reference/server#manage_relationship_maps_for_multiple_app_instances

https://iid.googleapis.com/iid/v1:batchAdd
Content-Type:application/json
Authorization:key=API_KEY
{
   "to": "/topics/movies",
   "registration_tokens": ["nKctODamlM4:CKrh_PC8kIb7O...", "1uoasi24:9jsjwuw...", "798aywu:cba420..."],
}


同理,也可以取消訂閱:https://iid.googleapis.com/iid/v1:batchRemove

若 server site 是 node.js 的,可以直接用 SDK :https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions

最後,筆記一下 Web app 的部分:假設網站服務位置為 https://example.com/fcm/ ,預設都要在根目錄埋上 firebase-messaging-sw.js 檔案,可以空白。

$ touch /var/www/firebase-messaging-sw.js
$ vim /var/www/fcm/index.html
<html>
<head>
<script src="https://www.gstatic.com/firebasejs/4.5.2/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "your-app-apikey",
authDomain: "your-app.firebaseapp.com",
databaseURL: "https://your-app.firebaseio.com",
projectId: "your-app",
storageBucket: "your-app.appspot.com",
messagingSenderId: "your-app-id"
};
firebase.initializeApp(config);
</script>
</head>
<body>
<script>
const messaging = firebase.messaging();

messaging.onTokenRefresh(function() {
messaging.getToken()
.then(function(refreshedToken) {
console.log('Token refreshed.', refreshedToken);
})
.catch(function(err) {
console.log('Unable to retrieve refreshed token ', err);
});
});

messaging.onMessage(function(payload) {
console.log("Message received. ", payload);
});

messaging.requestPermission()
.then(function() {
console.log('Notification permission granted.');
resetUI();
})
.catch(function(err) {
console.log('Unable to get permission to notify.', err);
});

function resetUI() {
console.log('resetUI');
messaging.getToken()
.then(function(currentToken) {
if (currentToken) {
console.log("currentToken:", currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
})
.catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
});
}
</script>
</body>
</html>


如此一來,用 chrome browser 瀏覽 https://example.com/fcm/ 會彈跳視窗詢問是否接收訊息,點擊接受後,在 dev tools 的 console 可以看到:

Notification permission granted.
resetUI
currentToken: thisClientFCMToken

如此一來,就可以對他發訊、設定此 client 去訂閱 topic 了:

$ curl -X POST -H "Authorization: key=YourFCMAppServerKey" -H "Content-Type: application/json" -d '{
  "notification": {
    "title": "Hello",
    "body": "World",
    "icon": "firebase-logo.png",
    "click_action": "http://localhost:8081"
  },
  "to": "thisClientFCMToken"
}' "https://fcm.googleapis.com/fcm/send"

$ curl -X POST -H "Authorization: key= YourFCMAppServerKey" -H "Content-Type: application/json" -d '{
  "registration_tokens":["thisClientFCMToken"],
  "to": "/topics/foo-bar"
}' "https://iid.googleapis.com/iid/v1:batchAdd"

$ curl -X POST -H "Authorization: key= YourFCMAppServerKey" -H "Content-Type: application/json" -d '{
  "notification": {
    "title": "Hello",
    "body": "World",
    "icon": "firebase-logo.png",
    "click_action": "http://localhost:8081"
  },
  "to": "/topics/foo-bar"
}' "https://fcm.googleapis.com/fcm/send"

2016年10月9日 星期日

iOS 開發筆記 - 建立/更新 Apple Push Notification Service (APNs) 憑證、P12轉PEM、驗證 PEM、PHP 範例程式

APNs_export_key

以前寫過幾篇 APNs 筆記,但好像都沒很完整,結果每年要更新憑證時,都要再找資料複習一次 Orz 所以就把它筆記下來吧!
首先,在 iOS Developer 網站上可以維護 APNs 憑證,然而,一旦過期,其實會自動消失 Orz 而 APNs 這服務滿像開發一次後就一直擺在那,沒用行事曆紀錄肯定忘掉。

如果,當初的 Certificate Signing Request (CSR) 以及 private key(.p12) 還留著的話,可以繼續拿來用,若沒有的話,就重新建立。整個就像第一次建立一樣 :P

Step 1:

iOS Developer Website -> Certificate -> All -> [+] -> Production -> Apple Push Notification service SSL (Sandbox & Production) -> App ID: -> Your App ID -> About Creating a Certificate Signing Request (CSR) -> Upload CRS file

若之前的 CSR 跟 p12 都還在,就可以省去製作 CSR,直接拿舊的上傳;若要新建立就從 Keychain -> 憑證輔助程式 -> 從憑證授權要求憑證 -> 輸入 email -> 預設將產生 CertificateSigningRequest.certSigningRequest 檔案,把他拿去上傳吧。另外,記得要把下載到的 aps.cer 點擊匯入至 keychain 中。

Step 2:

在 keychain -> 類別 -> 我的憑證 -> 可以看到 Apple Push Service 資訊,展開後,點擊專用密鑰,在點擊上方的憑證後,可以以憑證為單位輸出 p12 資料,這是跟 APN server 溝通的通行證。

Step 3:

使用 openssl 將通行證轉檔,從 p12 成 pem 檔

$ openssl pkcs12 -in App.p12 -out App.pem -nodes

轉換後,可以用文字編輯器打開 App.pem 檔案,會看到兩段:

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----


而 CERTIFICATE 上頭也會標記是哪個 App ID 的,未來忘記都可以翻來看

Step 4:

做簡單的 pem 驗證即可。

Production:

$ openssl s_client -connect gateway.push.apple.com:2195 -cert App.pem

Sandbox:

$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert App.pem

若 pem 是正常的,應該連線完就停在那邊:

...
---
SSL handshake has read 3385 bytes and written 2515 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : AES256-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: #######
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1476026971
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---
^C


Step 5:

可以拿 pem 跟 APNs 互動了!以下是片段 PHP 程式碼,有興趣可再試試 changyy/codeigniter-library-notification 包好的 library。

<?php

$ssl_ctx = stream_context_create();
stream_context_set_option($ssl_ctx, 'ssl', 'local_cert', $pem_file_path);

if( is_resource( $fp = stream_socket_client(
$use_sansbox ? 'ssl://gateway.sandbox.push.apple.com:2195' : 'ssl://gateway.push.apple.com:2195',
$err,
$errstr,
60,
STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT,
$ssl_ctx
) ) ) {

//$payload = array(
// 'aps' => array(
// 'alert' => array(
// 'title' => $title,
// 'body' => $message,
// )
// )
//);

$packed_data =
// Command (Simple Notification Format)
chr(0)
// device token
. pack('n', 32) . pack('H*', $notification_token)
// payload
//. pack('n', strlen($raw_cmd)) . $raw_cmd;
. $raw_data;

if( @fwrite($fp, $packed_data, strlen($packed_data)) !== false ) {
echo "SUCCESS";
} else {
echo "FAILURE";
}

fclose($fp);
}

2015年9月3日 星期四

iOS 開發筆記 - Push Notification 收到 SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry 和 SSL Broken

網路上的一些討論:

  • get SSL Broken pipe error when try to make push notification - http://stackoverflow.com/questions/2626054/get-ssl-broken-pipe-error-when-try-to-make-push-notification
  • APNS SSL operation failed with code 1 - http://stackoverflow.com/questions/18378534/apns-ssl-operation-failed-with-code-1
簡言之,批次處理發訊動作時,其中有一個 notification token 失效時,會導致與 Apple Notification Server 的連線中斷 Orz 例如發 500 個,其中第二個壞了,會導致後面 498 個無法使用前一次的連線,要記得處理一下。解法就是要去偵測連線是否中斷,一旦中斷後要重連。

2014年9月18日 星期四

iOS 開發筆記 - 驗證 Apple Push Notification PEM File 以及 Remove PEM Password

半年前曾開發過: 使用 Apple Push Notification service (APNs),最近更新 PEM  後,要驗證一下 PEM 是否正確。

驗證還滿簡單的,單純用 openssl 進行,假設是 dev 模式,對象就是 snadbox:

$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert cert.pem -key key.pem

其中 cert.pem 跟 key.pem 也可以合在一起:

$ cat cert.pem key.pem > out.pem
$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert out.pem


假設產出的 key.pem 有密碼保護,為了圖方便想要去密碼的話,可以用:

$ openssl rsa -in key.pem -out nopass.pem

如此一來就搞定了,若測試的是 Production PEM ,記得改用 gateway.push.apple.com:2195 即可。

$ openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -key key.pem

2014年8月25日 星期一

[PHP] Mantis Email Notification Settings @ Ubuntu 12.04

在 Ubuntu 12.04 透過 apt-get 安裝後,配置好系統管理後,又手動把它生成 Mantis 1.2.17 了。預設 Mantis 是有啟動 Email Notification,包含新增帳號後,還要由對方 email 去設定密碼等。對於 Email Notification 的設定,如事件通知的寄信者資訊等,都是直接更改 /path/mantis/config_inc.php。

可以參考 http://www.mantisbt.org/manual/admin.config.email.htmlhttp://www.mantisbt.org/manual/admin.customize.email.html,簡易筆記:

//$g_enable_email_notification = OFF;
$g_mail_receive_own = OFF;
$g_default_notify_flags = array(
'reporter' => ON,
'handler' => ON,
'monitor' => ON,
'bugnotes' => ON,
'threshold_min' => NOBODY,
'threshold_max' => NOBODY
);

//
// Select the method to mail by: PHPMAILER_METHOD_MAIL for use of mail() function, PHPMAILER_METHOD_SENDMAIL for sendmail (or postfix), PHPMAILER_METHOD_SMTP for SMTP. Default is PHPMAILER_METHOD_MAIL.
//
$g_phpMailer_method = PHPMAILER_METHOD_MAIL;
$g_smtp_host = 'localhost';
$g_smtp_username = '';
$g_smtp_password = '';
$g_administrator_email = 'admin@example.com';
$g_from_email = 'noreply@example.com';
$g_from_name = 'Mantis Bug Tracker';

2014年3月14日 星期五

Android 開發筆記 - 使用 Google Cloud Messaging for Android (GCM)

原來 Android Cloud to Device Messaging Framework (C2DM) has been officially deprecated as of June 26, 2012 ... 現在改用 Google Cloud Messaging for Android
Google Cloud Messaging for Android (GCM) is a free service that helps developers send data from servers to their Android applications on Android devices, and upstream messages from the user's device back to the cloud. This could be a lightweight message telling the Android application that there is new data to be fetched from the server (for instance, a "new email" notification informing the application that it is out of sync with the back end), or it could be a message containing up to 4kb of payload data (so apps like instant messaging can consume the message directly). The GCM service handles all aspects of queueing of messages and delivery to the target Android application running on the target device.
一遍後,其實 GCM 的架構跟 Apple Push Notification service (APNs) 很像,主要都是先由 Mobile device 跟 Android server (Apple server) 註冊一個 token,接著再請 Mobile device 將此 token 交給 Service Provide。而 Service Provider 就利用這個 token 跟 Mobile device 傳訊息。比較不一樣的是 GCM 提供的架構多了一些設計,詳細請參考官網資料,在此僅簡易筆記一下。

Google Developer console:
  1. 首先,先到 Google Developer Console 建立一個 project,可得到 Service Provider ID
  2. 啟用 GCM service
  3. 建立 keys,APIs & auth > Credentials > Create a new key > Server key  (測試上 IP 設定為 0.0.0.0/0)
  4. 得到一組 API key,之後就是透過這個 key 跟 Mobile device's token 丟訊息
Android app:
  1. 下載 Google Play service library 並安置好
  2. 在 AndroidManifest.xml 宣告相關權限、並新增 <receiver> 來接收 Service Provider 的訊息
  3. 接著就是程式啟動時,透過 Google library 跟 GCM 註冊一個 token,此時需要指定 Service Provider ID 資訊
  4. 將 token 交給 Service Provider,如此一來 service provider 就可以傳訊息
  5. 實作好 <receiver>,當訊息進來時,啟動一個 service 發 notification 出去
接著是一些片段範例:
  • Service Provider ID: Your-Sender-ID
  • API KEY: API_Key
  • Android App Package Name: com.example.gcm
  • Mobile device token: MobileDeviceToken
  • Message:hello world
  • Payload: {"to":"MobileDeviceToken","data":{"message":"hello world","action":"com.example.gcm"}}
Service Provider 發訊息給指定的 Mobile device:

$ curl --header "Authorization: key=API_Key" --header Content-Type:"application/json" https://android.googleapis.com/gcm/send  -d '{"to":"MobileDeviceToken","data":{"message":"hello world","action":"com.example.gcm"}}'

Android app - AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gcm"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />

    <!--  ADD FOR Google Cloud Messaging ### Begin -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:name="com.example.gcm.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
<uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
    <!--  ADD FOR Google Cloud Messaging ### End -->


    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.gcm.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
   
        <!--  ADD FOR Google Cloud Messaging ### Begin -->
        <meta-data android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />
        <receiver
            android:name=".GcmBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="com.example.gcm" />
            </intent-filter>
        </receiver>
        <service android:name=".GcmIntentService" />
        <!--  ADD FOR Google Cloud Messaging ### End -->

   
    </application>
</manifest>


Android app - Receiver:

package com.example.gcm;

import com.google.android.gms.gcm.GoogleCloudMessaging;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
private static final String TAG = "GCM";

@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
Log.i(TAG, "GcmBroadcastReceiver");

GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
String messageType = gcm.getMessageType(intent);
Log.i(TAG, "GcmBroadcastReceiver messageType:"+messageType);

Bundle extras = intent.getExtras();
if (!extras.isEmpty()) {
if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {

} else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) {

} else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {

}
Log.i(TAG, "GcmBroadcastReceiver Received: " + extras.toString());
}
}
}


Android app - MainActivity:

public class MainActivity extends ActionBarActivity {
private static final String TAG = "GCM";
public static final String EXTRA_MESSAGE = "message";
public static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";
private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
String SENDER_ID = "Your-Sender-ID";

GoogleCloudMessaging gcm;
AtomicInteger msgId = new AtomicInteger();
String regid;
Context context;

private boolean checkPlayServices() {
   int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
   if (resultCode != ConnectionResult.SUCCESS) {
       if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
           GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                   PLAY_SERVICES_RESOLUTION_REQUEST).show();
       } else {
           Log.i(TAG, "This device is not supported.");
           finish();
       }
       return false;
   }
   return true;
}

private String getRegistrationId(Context context) {
   final SharedPreferences prefs = getGCMPreferences(context);
   String registrationId = prefs.getString(PROPERTY_REG_ID, "");
   if (registrationId.isEmpty()) {
       Log.i(TAG, "Registration not found.");
       return "";
   }
   // Check if app was updated; if so, it must clear the registration ID
   // since the existing regID is not guaranteed to work with the new
   // app version.
   int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
   int currentVersion = getAppVersion(context);
   if (registeredVersion != currentVersion) {
       Log.i(TAG, "App version changed.");
       return "";
   }
   return registrationId;
}

private SharedPreferences getGCMPreferences(Context context) {
   // This sample app persists the registration ID in shared preferences, but
   // how you store the regID in your app is up to you.
   return getSharedPreferences(MainActivity.class.getSimpleName(),
           Context.MODE_PRIVATE);
}

private static int getAppVersion(Context context) {
   try {
       PackageInfo packageInfo = context.getPackageManager()
               .getPackageInfo(context.getPackageName(), 0);
       return packageInfo.versionCode;
   } catch (NameNotFoundException e) {
       // should never happen
       throw new RuntimeException("Could not get package name: " + e);
   }
}
private void registerInBackground() {
new AsyncTask<Object, Object, Object>() {

@Override
protected Object doInBackground(final Object... arg0) {
// TODO Auto-generated method stub
String msg = "";
           try {
               if (gcm == null) {
                   gcm = GoogleCloudMessaging.getInstance(context);
               }
               regid = gcm.register(SENDER_ID);
               msg = "Device registered, registration ID=" + regid;

               // You should send the registration ID to your server over HTTP,
               // so it can use GCM/HTTP or CCS to send messages to your app.
               // The request to your server should be authenticated if your app
               // is using accounts.
               sendRegistrationIdToBackend();

               // For this demo: we don't need to send it because the device
               // will send upstream messages to a server that echo back the
               // message using the 'from' address in the message.

               // Persist the regID - no need to register again.
               storeRegistrationId(context, regid);
           } catch (IOException ex) {
               msg = "Error :" + ex.getMessage();
               // If there is an error, don't just keep trying to register.
               // Require the user to click a button again, or perform
               // exponential back-off.
           }
           return msg;
}

}.execute(null, null, null);
}

private void sendRegistrationIdToBackend() {
   // Your implementation here.
}

private void storeRegistrationId(Context context, String regId) {
   final SharedPreferences prefs = getGCMPreferences(context);
   int appVersion = getAppVersion(context);
   Log.i(TAG, "Saving regId on app version " + appVersion);
   Log.i(TAG, "Saving regId: " + regId);
   SharedPreferences.Editor editor = prefs.edit();
   editor.putString(PROPERTY_REG_ID, regId);
   editor.putInt(PROPERTY_APP_VERSION, appVersion);
   editor.commit();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

        context = getApplicationContext();

        // Check device for Play Services APK. If check succeeds, proceed with
        //  GCM registration.
        if (checkPlayServices()) {
            gcm = GoogleCloudMessaging.getInstance(this);
            regid = getRegistrationId(context);

            if (regid.isEmpty()) {
                registerInBackground();
            }
        } else {
            Log.i(TAG, "No valid Google Play Services APK found.");
        }

if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment()).commit();
}
}
// ...
}


更完整的範例請參考官網:GCM Client

2013年12月10日 星期二

iOS 開發筆記 - 使用 NSNotification 傳遞與接收事件

通常會設計多個 UIViewController 供使用者觀看與操作,如果 AUIViewController 提供設定參數後,當使用者切換到 BUIViewController 可以馬上就使用到新資料來操作時,這時可以把資料儲存在指定地方,當 BUIViewController 之 viewWillAppear 裡就能夠去重讀資料來用。

然而,有時希望 AUIViewController 改完資料時,可以馬上讓 BUIViewController 也得知,這時做法至少有兩種,第一種就是讓 AUIViewController 可以接觸到 BUIViewController ,例如把 BUIViewController 當作一個 member variable ,也就是 AUIViewController.member = BUIViewController object,只是這招有點破壞架構的美感,但偶爾使用還滿堪用的!第二種,就是採用 Notification 架構,讓 BUIViewController 聆聽專屬於它的事件,而 AUIViewController 在需要的時候送事件出去即可,此架構更加符合 async 。

此例筆記 NSNotification 的用法(此架構也稱 radio station,Android 派則是 broadcast 啦):

BUIViewController.h:

#define kTargetDataUpdate @"TargetDataUpdate"

BUIViewController.m 之註冊聆聽事件:

- (void)setupOptions:(NSNotification *)event
{
        NSLog(@"event:%@, object:%@", event, event.object);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupOptions:) name:kTargetDataUpdate object:nil];

}


BUIViewController.m 之移除聆聽事件:

- (void)viewWillDisappear:(BOOL)animated
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name: kTargetDataUpdate object:nil];
    [super viewWillDisappear:animated];
}


AUIViewController (或其他物件亦可) 之發送事件:

NSDictionary *info = @{@"status":@"ok"};
[[NSNotificationCenter defaultCenter] postNotificationName: kTargetDataUpdate object:info userInfo:nil];