07 มีนาคม 2559

[Android Code] มาทำความรู้จักและลองใช้งาน Google Cloud Messaging กันเถอะ



        ล่าสุดทาง Google ได้เปิดตัว Firebase Cloud Messaging (FCM) เพื่อใช้งานแทน Google Cloud Messaging (GCM) แล้ว โดยตัว GCM จะยังคงใช้งานได้อยู่ก็จริง แต่จะไม่มีอะไรอัปเดตเพิ่มเติมแล้ว ทุกๆอย่างจะไปอยู่ที่ FCM ดังนั้นทางที่ดีควรอัปเดตไปใช้ FCM ดีกว่านะ สำหรับบทความการใช้งาน FCM สามารถอ่านได้ที่ รู้จัก Firebase Analytics ตั้งแต่ Zero จนเป็น Hero

        ในที่สุดก็ได้ฤกษ์เขียนเรื่องนี้เสียที หลังจากปล่อยดองไว้นานมาก ส่วนหนึ่งอาจจะเพราะว่าขี้เกียจคอยอัพเดทตาม Official ด้วยแหละมั้ง (ฮา) ซึ่งเจ้าของบล็อกรู้สึกว่า Documentation ของ Google Cloud Messaging นั้นดูเข้าใจยากไปหน่อย ก็เลยน่าจะเหมาะดีที่จะหยิบมาเขียนบทความ

รู้จักกับ Google Cloud Messaging กันก่อนนะ

        Google Cloud Messaging หรือที่เรียกสั้นๆกันว่า GCM เป็นหนึ่งในบริการของ Google ที่เอาไว้ส่งข้อมูลจาก Server ไปยังอุปกรณ์พกพา ซึ่งในปัจจุบันนี้รองรับทั้ง Android และ iOS เลย ถึงแม้ว่า iOS จะมี Apple Push Notification Service (APNs) ให้ใช้งานก็เถอะ แต่คงจะดีกว่าถ้าใช้แค่ตัวเดียว แล้วรองรับทั้ง Android กับ iOS เลย

        และที่สำคัญคือมันฟรีครับ

หลักการทำงาน

        ส่วนใหญ่มักจะรู้จัก GCM กันว่ามีไว้สำหรับทำ Push Notification (ส่งข้อความไปแสดงเป็นแจ้งเตือนบนมือถือ) แต่นั่นก็คือรูปแบบการใช้งานอย่างหนึ่งเท่านั้น เพราะหน้าที่ของ GCM คือ ให้ Server ส่งข้อมูลใดๆก็ตามไปที่อุปกรณ์พกพาครับ การแสดง Notification เป็นแค่ผลลัพธ์อย่างหนึ่งเฉยๆ

        โดยปกติแล้วเวลาจะรับส่งข้อมูลระหว่าง Client (อุปกรณ์แอนดรอยด์) กับ Server มีข้อจำกัดว่าฝั่ง Client จะต้องเป็นคนเชื่อมต่อไปที่ Server ก่อนแล้วจึงส่งข้อมูลแล้วก็ได้ผลลัพธ์จาก Server กลับมา

        แต่ถ้าอยากจะให้ Server เป็นคนเริ่มส่งข้อความไปให้ Client นี่สิ มันทำไม่ได้ครับ (ก็ควรเป็นแบบนั้นแหละ)
        แล้วทีนี้จะทำยังไงดีล่ะ?

        ส่วนใหญ่ก็จะใช้วิธีสร้าง Service ขึ้นมาซักตัว แล้วสั่งให้มันคอยเช็คกับ Server เป็นระยะๆว่ามีข้อมูลอะไรจะส่งมาหรือไม่ แต่ปัญหาต่อมาก็คือจะต้องมี 1 Service ที่คอยเช็คกับ Server แล้วถ้าเครื่องของผู้ใช้มีซัก 10 แอพที่ทำแบบนี้ล่ะ? นั่นก็หมายความว่าในเครื่องมันจะต้องมีทั้งหมด 10 Service เลยน่ะสิ แบบนั้นก็สิ้นเปลืองเกินไปหรือป่าวนะ

        ด้วยเหตุนี้ Google จึงสร้าง Google Play Services เป็นตัวกลางในการทำส่วนนี้ครับ โดยจะมี Service ของ Google Play Services ที่คอยเช็คกับ GCM Server ให้ว่ามีข้อมูลอะไรที่จะส่งหรือป่าว ถ้ามีก็จะส่งให้แอพตัวนั้นๆทันที
        ซึ่งเป็นวิธีที่ประหยัด Process ในการทำงานได้มาก เพราะแอพหลายๆตัวสามารถใช้งาน GCM พร้อมๆกันได้ (ข้อมูลที่ส่งมาเป็นของแอพตัวไหนมันก็จะจัดการให้เอง) แถมนักพัฒนาไม่ต้องไปจัดการเองด้วย แค่สั่งให้ Server ส่งข้อมูลไปให้ GCM แล้วเดี๋ยว GCM จะจัดการส่งข้อมูลเข้าแอพให้เอง

เตรียมการใช้งาน GCM ให้พร้อม

        การใช้งาน GCM จะต้องตั้งค่าผ่าน Google Developers Console ก่อน ถ้ายังไม่เคยใช้งานก็ไปสมัครแล้วสร้างโปรเจคให้พร้อมซะ

        เมื่อสร้างโปรเจคในนั้นแล้วก็ให้เปิดใช้งาน API ของ Google Cloud Messaging โดยกดที่ Enable and manage APIs ในหน้า Dashboard

        หา API ที่ชื่อว่า Google Cloud Messaging (พิมพ์ในช่องค้นหาก็ได้) แล้ว Enable ให้เรียบร้อยซะ

        ยังไม่เสร็จนะครับ เพราะต้องสร้าง Credential ก่อน เพื่อขอ API Key ให้ Server ของผู้ที่หลงเข้ามาอ่านเอาไปใช้งาน สำหรับ GCM จะมีการขอ API Key ที่ค่อนข้างสะดวกหน่อย โดยเข้าไปที่หน้าเว็ป Enable Google services for your app [Google Developers] แล้วเลือกตามให้ตรงกับที่ต้องการใช้งานซะ

        จะเห็นว่ามี Project ที่สร้างไว้ให้เลือก แล้วกำหนด Package Name ของแอพแอนดรอยด์ให้เรียบร้อย จากนั้นก็ไปขั้นตอนต่อไปได้เลย

        เปิดใช้งาน Google Cloud Messaging ให้เรียบร้อย (กดที่ปุ่มเครื่องหมายบวกที่มุมขวาบนของไอคอน) แล้วไปขั้นตอนต่อไปได้เลย

        ขั้นตอนนี้ค่อนข้างสำคัญนะ เพราะว่าผู้ที่หลงเข้ามาอ่านจะต้องดาวน์โหลดไฟล์ google-services.json ไปไว้ในโปรเจคแอนดรอยด์ เพื่อให้สามารถใช้งาน GCM ได้

        ให้กลับไปหน้า Dashboard ของ Google Developers Console ต่อ เข้าเลือกเมนู Credentials (แถบเมนูซ้ายมือ) จะเห็นว่ามี API Key สร้างไว้ให้เรียบร้อยแล้ว (สร้างจากขั้นตอนก่อนหน้านี้น่ะแหละ) ก็ให้เก็บ API Key ไว้นะ เพราะฝั่ง Server เวลาส่งข้อมูลเข้า GCM จะต้องใช้ API Key ด้วย

        เป็นอันเสร็จสิ้นการตั้งค่าใช้งาน GCM แล้ว ตอนนี้สิ่งที่เตรียมไว้แล้วจะต้องมี 2 อย่างด้วยกันคือไฟล์ google-services.json กับ API Key นะครับ ผู้ที่หลงเข้ามาอ่านคนไหนพลาดอันไหนไปให้กลับขึ้นไปอ่านทวนใหม่อีกรอบนะ

การเตรียมโปรเจคแอนดรอยด์เพื่อใช้งาน GCM

        สร้างโปรเจคเปล่าๆใน Android Studio ขึ้นมาเตรียมไว้เลยครับ แล้วมาดูกันก่อนว่าการจะใช้งาน GCM ในแอพเนี่ย ต้องมีอะไรบ้าง

ใส่ google-services.json ลงในโปรเจคก่อน จะได้ไม่ลืม

        ไฟล์ตัวนี้ให้ไว้ในโฟลเดอร์ Module ของโปรเจคนะ ซึ่งปกติเวลาสร้างโปรเจคขึ้นมาใหม่ๆ Module จะมีชื่อ Default เป็น app เนอะ ก็เอาไฟล์ google-services.json ไปไว้ในนั้นเลย


เพิ่ม Google Services ลงในโปรเจค

        เริ่มจากเปิดไฟล์ build.gradle ตัวที่อยู่ข้างนอกสุดของโฟลเดอร์โปรเจคก่อนเลย แล้วเพิ่ม classpath ของ Google Services เข้าไปแบบนี้

build.gradle (Root)
buildscript {
    ...

    dependencies {
        ...
        classpath 'com.google.gms:google-services:2.0.0-beta6'
    }
}

...

        ส่วนเวอร์ชันก็ใช้เป็นเวอร์ชันล่าสุดนะครับ ซึ่งตอนที่เจ้าของบล็อกเขียนบทความนี้ใช้ Android Studio 2 Beta 6 อยู่ ซึ่งมันบังคับให้ใช้เป็นเวอร์ชัน 2.0.0-beta6

        ต่อมาก็เปิด build.gradle ของ Module ตัวหลัก (Module ที่ชื่อ app น่ะแหละ) แล้วเพิ่ม Plugin ของ Google Service และเพิ่ม Dependency ของ Google Play Services (เฉพาะของ GCM) เข้าไปดังนี้

build.gradle (app)
...

dependencies {
    ...
    
    compile 'com.google.android.gms:play-services-gcm:8.4.0'
}

apply plugin: 'com.google.gms.google-services'

        Dependency ของ Google Play Service ก็ให้อิงเวอร์ชันใหม่สุดตลอดนะ ส่วนคำสั่งเพิ่ม Plugin ของ Google Services ขอบังคับไว้เลยว่าให้ใส่ไว้ที่บรรทัดสุดท้ายของไฟล์นี้ (เพื่อป้องกันปัญหา Build Project ไม่ผ่านเพราะติดปัญหาเรื่องเวอร์ชันของ Google Play Services)

สร้าง Service Class ขึ้นมา 3 ตัว 

        ซึ่งประกอบไปด้วย
        • Intent Service สำหรับลงทะเบียนเครื่องนั้นๆเข้าไปในระบบของ GCM ซึ่งจะได้ Token เอาไว้ใช้งาน (ขอตั้งชื่อคลาสว่า GcmRegisterService)
        • Instance ID Listener Service สำหรับเช็คว่า Token หมดอายุหรือยัง เพื่อที่จะได้ขอ Token จาก GCM ใหม่ (ขอตั้งชื่อคลาสว่า GcmTokenRefreshService)
        • GCM Listener Service เอาไว้รับข้อมูลจาก Server ที่ส่งเข้ามาผ่าน GCM (ขอตั้งชื่อคลาสว่า GcmDownstreamService)

        ซึ่งชื่อ Service ทั้ง 3 สามารถเปลี่ยนได้ตามใจชอบ (ขอให้สื่อแล้วเข้าใจก็พอ)

        ดูเหมือนจะเยอะเนอะ แต่ไม่ห่วง เพราะส่วนใหญ่นั้นเป็นการเอาโค๊ดมาแปะๆเฉยๆ มีแค่บางส่วนเท่านั้นที่ต้องทำเพิ่มเอง

เริ่มจาก GcmRegisterService (เปลี่ยนชื่ออื่นได้)

        คลาสที่ทำหน้าที่จัดการเกี่ยวกับการลงทะเบียนตัวเครื่องเข้าไปที่ Server ของ GCM 

GcmRegisterService.java
import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.google.android.gms.gcm.GcmPubSub;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.android.gms.iid.InstanceID;

import java.io.IOException;

public class GcmRegisterService extends IntentService {
    private static final String TAG = "RegIntentService";
    private static final String[] TOPICS = {"global"};
    public static final String SENT_TOKEN_TO_SERVER = "sentTokenToServer";
    public static final String REGISTRATION_COMPLETE = "registrationComplete";

    public GcmRegisterService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        try {
            // [START register_for_gcm]
            // Initially this call goes out to the network to retrieve the token, subsequent calls
            // are local.
            // R.string.gcm_defaultSenderId (the Sender ID) is typically derived from google-services.json.
            // See https://developers.google.com/cloud-messaging/android/start for details on this file.
            // [START get_token]
            InstanceID instanceID = InstanceID.getInstance(this);
            String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId),
                    GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
            // [END get_token]
            Log.i(TAG, "GCM Registration Token: " + token);

            // TODO: Implement this method to send any registration to your app's servers.
            sendRegistrationToServer(token);

            // Subscribe to topic channels
            subscribeTopics(token);

            // You should store a boolean that indicates whether the generated token has been
            // sent to your server. If the boolean is false, send the token to your server,
            // otherwise your server should have already received the token.
            sharedPreferences.edit().putBoolean(SENT_TOKEN_TO_SERVER, true).apply();
            // [END register_for_gcm]
        } catch (Exception e) {
            Log.d(TAG, "Failed to complete token refresh", e);
            // If an exception happens while fetching the new token or updating our registration data
            // on a third-party server, this ensures that we'll attempt the update at a later time.
            sharedPreferences.edit().putBoolean(SENT_TOKEN_TO_SERVER, false).apply();
        }
        // Notify UI that registration has completed, so the progress indicator can be hidden.
        Intent registrationComplete = new Intent(REGISTRATION_COMPLETE);
        LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
    }

    /**
     * Persist registration to third-party servers.
     *
     * Modify this method to associate the user's GCM registration token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private void sendRegistrationToServer(String token) {
        // Add custom implementation, as needed.
    }

    /**
     * Subscribe to any GCM topics of interest, as defined by the TOPICS constant.
     *
     * @param token GCM token
     * @throws IOException if unable to reach the GCM PubSub service
     */
    // [START subscribe_topics]
    private void subscribeTopics(String token) throws IOException {
        GcmPubSub pubSub = GcmPubSub.getInstance(this);
        for (String topic : TOPICS) {
            pubSub.subscribe(token, "/topics/" + topic, null);
        }
    }
    // [END subscribe_topics]

}

        สำหรับคลาสนี้ สิ่งที่ต้องทำเพิ่มเติมคือในเมธอด sendRegistrationToServer เพราะว่าหลังจากลงทะเบียนกับ GCM แล้ว อุปกรณ์นั้นๆจะถูกระบุด้วย Token ซึ่ง Token นั้นสำคัญมากเพราะว่า Server ของผู้ที่หลงเข้ามาอ่านจะส่งข้อมูลมาที่อุปกรณ์แอนดรอยด์เครื่องไหน ก็ต้องบอกเป็น Token นี่แหละครับ แล้ว GCM ก็จะส่งข้อมูลให้กับเครื่องที่มี Token ตรงกับที่ระบุไว้

        ดังนั้นสิ่งที่ต้องทำในเมธอดตัวนี้ก็คือส่ง Token ขึ้นไปเก็บไว้บน Server ของผู้ที่หลงเข้ามาอ่านซะ (Server ก็ต้องมีฐานข้อมูลเอาไว้เก็บ) ซึ่งวิธีตรงนี้แล้วแต่ผู้ที่หลงเข้ามาอ่านเลย ว่าใช้วิธีไหนอยู่ แต่ถ้าเป็นเจ้าของบล็อกก็จะใช้ Retrofit ส่ง Token ขึ้นไปเก็บบน Server แหละ (เพราะโปรเจคส่วนใหญ่ใช้ Retrofit อยู่)

ไฟล์ต่อมาคือ GcmTokenRefreshService (เปลี่ยนชื่ออื่นได้)

        ในบางครั้ง Token อาจจะหมดอายุหรือเลิกใช้งานไป จึงอาจจะต้องมีการขอ Token ใหม่ ซึ่งคลาสตัวนี้จะจัดการตรงนี้ให้

GcmTokenRefreshService.java
import android.content.Intent;

import com.google.android.gms.iid.InstanceIDListenerService;

public class GcmTokenRefreshService extends InstanceIDListenerService {
    @Override
    public void onTokenRefresh() {
        Intent intent = new Intent(this, GcmRegisterService.class);
        startService(intent);
    }
}

        ซึ่งการทำงานของคลาสนี้ก็โคตรจะง่ายเลย ถ้าเกิด Event ว่า onTokenRefresh ขึ้นมา มันก็แค่ไปเรียกให้คลาส GcmRegisterService ทำงานใหม่อีกครั้งนั่นเอง

ไฟล์สุดท้าย GcmDownstreamService (เปลี่ยนชื่ออื่นได้)

        คลาสนี้จะคอยรับข้อมูลที่ GCM ส่งมานั่นเอง

GcmDownstreamService.java
package com.akexorcist.gcmtest;

import android.os.Bundle;

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

/**
 * Created by Akexorcist on 3/6/2016 AD.
 */
public class GcmDownstreamService extends GcmListenerService {
    private static final String TAG = "DcmDownstreamService"; 

    @Override
    public void onMessageReceived(String from, Bundle data) {
        // TODO Do something here
    }
}

        สิ่งที่ต้องเพิ่มเข้าไปก็คือ เมื่อ GCM ส่งข้อมูลมาให้แล้ว จะทำอะไร อันนี้ขึ้นอยู่กับผู้ที่หลงเข้ามาอ่านครับว่าจะให้มันทำอะไร ไม่ได้มีคำสั่งตายตัว ยกตัวอย่างเช่นแอพพวก Messenger ก็จะแจ้งเตือนผ่าน Notification ว่ามีผู้ใช้คนอื่นส่งข้อความมาหา และจะเก็บข้อมูลที่ว่าไว้ในเครื่องเพื่อให้ผู้ใช้เปิดแอพขึ้นมาแล้วดูได้ทันที

กำหนดค่าที่จำเป็นสำหรับ Android Manifest ให้เรียบร้อย

        ก็จะมี Permission ที่ต้องประกาศ แล้วก็ Receiver ของ GCM โดยประกาศไว้แบบนี้

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    ... >

    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <application
        ... >
        
        ...

        <receiver
            android:name="com.google.android.gms.gcm.GcmReceiver"
            android:exported="true"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />

                <category android:name="com.akexorcist.gcmtest" />
            </intent-filter>
        </receiver>

    </application>

</manifest>

         จริงๆจะมี Permission ของ Internet กับ Access Network State ด้วย แต่ว่า Google Play Services ที่เพิ่มไว้ใน build.gradle ตอนแรกจะใส่ให้อยู่แล้ว ดังนั้นจึงไม่ต้องใส่ก็ได้

ประกาศคลาสทั้ง 3 ตัวที่สร้างไว้ลงใน Android Manifest ด้วย

        เพราะคลาสพวกนี้ล้วนเป็น Service ดังนั้นก็ต้องประกาศไว้ใน AndroidManifest.xml ด้วยสิ ใส่ต่อท้าย Receiver ที่พึ่งเพิ่มเข้าไปเมื่อกี้น่ะแหละ

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    ...>

    ...

    <application
        ...>

        ... 

        <service
            android:name=".GcmDownstreamService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            </intent-filter>
        </service>

        <service
            android:name=".GcmTokenRefreshService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.android.gms.iid.InstanceID" />
            </intent-filter>
        </service>

        <service
            android:name=".GcmRegisterService"
            android:exported="false" />
    </application>

</manifest>

        ถ้าเปลี่ยนชื่อคลาสทั้ง 3 ตัว ก็อย่าลืมแก้ไขในนี้ให้ตรงด้วยล่ะ

ลอง Build Project ดูก่อน

        เพื่อความมั่นใจว่าโปรเจคพร้อมแล้ว ให้ลอง Build Project หรือ Build Gradle ดูก่อนครับ แค่ให้ Build ผ่านก็พอ เพราะในตอนนี้เจ้าของบล็อกยังไม่ได้ให้ใส่คำสั่งใดๆลงใน Activity ดังนั้นมันจะยังทำงานไม่ได้

        ถ้า Build แล้วติดปัญหาก็ลองเช็คให้ดีๆ ทวนขั้นตอนข้างบนใหม่อีกครั้ง ว่าขาดอันไหนไปหรือป่าว

        เมื่อพร้อมแล้วก็เริ่มมาใส่คำสั่งกันต่อในส่วนของ Activity เลยครับ ใกล้จะเสร็จแล้ว อีกนิดนึง~

เครื่องนั้นๆรองรับ Google Play Services มั้ยนะ?

        เนื่องจาก GCM มันต้องใช้ Google Play Services ซึ่งก็คงต้องบอกว่าเกือบทุกเครื่องบนโลกนี้มี Google Play Services อยู่แล้ว แต่เพื่อความแน่ใจอย่างน้อยก็ควรเช็คหน่อยว่าเครื่องนั้นๆมี Google Play Services หรือป่าว ส่วนหนึ่งก็เพราะเชื่อว่าน่าจะมีผู้ที่หลงเข้ามาอ่านเอาไปเทสบน Emulator แล้วไม่รู้ว่ามันทำงานไม่ได้เพราะไม่มี Google Play Services (ต้องลงเพิ่มเอง)

private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

private boolean checkPlayServices() {
    GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
    int resultCode = apiAvailability.isGooglePlayServicesAvailable(this);
    if (resultCode != ConnectionResult.SUCCESS) {
        if (apiAvailability.isUserResolvableError(resultCode)) {
            apiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST).show();
        }
        return false;
    }
    return true;
}

เพิ่มคำสั่งลงทะเบียนกับ GCM Server

        อันนี้ให้แยกเป็น Function เฉยๆครับ เวลาดูจะได้เข้าใจง่าย ซึ่งคำสั่งก็ไม่มีอะไรแค่ให้ Service ที่ชื่อว่า GcmRegisterService ทำงานเฉยๆ

private void registerGcm() {
    Intent intent = new Intent(this, GcmRegisterService.class);
    startService(intent);
}

เพิ่มคำสั่ง Broadcast Receiver สำหรับสำหรับลงทะเบียนกับ GCM Server

        อันนี้คล้ายๆกับ Event Listener เฉยๆครับ เอาไว้เวลาที่ GcmRegisterService ลงทะเบียนกับ GCM Server เสร็จแล้ว ก็จะส่งมาบอกผ่าน Broadcast Receiver ให้ว่าเสร็จแล้วนะ เพราะ Service ไม่สามารถส่งข้อมูลมาที่ Activity ผ่าน Event Listener ได้ครับ เลยต้องใช้วิธีนี้

        หลักๆก็จะมีการสร้าง Broadcast Receiver เตรียมไว้ แล้วก็เตรียมคำสั่งเรียก/ยกเลิกให้ Broadcast Receiver ตัวนี้

private boolean isReceiverRegistered;

private BroadcastReceiver mRegistrationBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        boolean sentToken = sharedPreferences.getBoolean(GcmRegisterService.SENT_TOKEN_TO_SERVER, false);
        // TODO Do something here
    }
};

private void registerReceiver() {
    if (!isReceiverRegistered) {
        LocalBroadcastManager.getInstance(this).registerReceiver(mRegistrationBroadcastReceiver, new IntentFilter(GcmRegisterService.REGISTRATION_COMPLETE));
        isReceiverRegistered = true;
    }
}

private void unregisterReceiver() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mRegistrationBroadcastReceiver);
    isReceiverRegistered = false;
}

        ส่วนวิธีการเช็คว่าลงทะเบียนเสร็จแล้วหรือยังก็ให้ดูใน onReceive จะเห็นใช้วิธีเช็คผลลัพธ์ผ่าน Shared Preferences นั่นเอง เพราะใน GcmRegisterService จะเก็บผลลัพธ์ลงใน Shared Preference เพื่อให้สามารถดึงข้อมูลมาเช็คจากใน Activity ได้ง่ายนั่นเอง

แล้ว Function พวกนี้มันจะเรียกใช้ตรงไหนบ้างล่ะ?

        จริงๆมีแค่ 3 ตำแหน่งที่จะมีการเรียกใช้คำสั่งของ GCM คือ onCreate, onResume และ onStop

public class MainActivity extends AppCompatActivity {
    ...

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

        registerReceiver();

        if (checkPlayServices()) {
            registerGcm();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver();
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver();
    }

    ...
}

        ซึ่ง onCreate เอาไว้ เรียกใช้ Broadcast Receiver แล้วทำการเช็คว่ามี Google Play Services มั้ย ถ้ามีก็จะทำการลงทะเบียนกับ GCM Server

        ส่วน onPause เอาไว้ยกเลิก Broadcast Receiver เวลาที่ Activity ตัวนี้หยุดทำงานครับ ไม่ว่าจะ ออกจากแอพชั่วคราว ปิดแอพ หรือสลับไปแอพอื่น

        ส่วน onResume ก็แค่เรียกใช้ Broadcast Receiver อาจจะดูซ้ำซ้อนกับใน onCreate เนอะ (แต่ไม่เป็นไร คำสั่งนี้เช็คไว้ไม่ให้เรียกใช้งานซ้ำซ้อนแล้ว) แต่ก็ต้องเผื่อกรณีที่ User ออกจากแอพชั่วคราวแล้วกลับเข้ามาใหม่ครับ (เพราะ onCreate มันไม่ทำงานใหม่แล้วไง)

นี่คือสิ่งที่ต้องใส่ใน Activity ทั้งหมด!

        ก็มีแค่นี้แหละครับ

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;

public class MainActivity extends AppCompatActivity {
    private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
    private boolean isReceiverRegistered;

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

        registerReceiver();

        if (checkPlayServices()) {
            registerGcm();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver();
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver();
    }

    private void registerGcm() {
        Intent intent = new Intent(this, GcmRegisterService.class);
        startService(intent);
    }

    private void registerReceiver() {
        if (!isReceiverRegistered) {
            LocalBroadcastManager.getInstance(this).registerReceiver(mRegistrationBroadcastReceiver, new IntentFilter(GcmRegisterService.REGISTRATION_COMPLETE));
            isReceiverRegistered = true;
        }
    }

    private void unregisterReceiver() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mRegistrationBroadcastReceiver);
        isReceiverRegistered = false;
    }

    private BroadcastReceiver mRegistrationBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
            boolean sentToken = sharedPreferences.getBoolean(GcmRegisterService.SENT_TOKEN_TO_SERVER, false);
            // TODO Do something here
        }
    };

    private boolean checkPlayServices() {
        GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
        int resultCode = apiAvailability.isGooglePlayServicesAvailable(this);
        if (resultCode != ConnectionResult.SUCCESS) {
            if (apiAvailability.isUserResolvableError(resultCode)) {
                apiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST).show();
            }
            return false;
        }
        return true;
    }
}

มาดูฝั่ง Server กันต่อ ว่าต้องทำอะไรบ้าง

        ถ้าพูดง่ายๆก็คือ อยากจะส่งข้อมูลไปให้อุปกรณ์แอนดรอยด์เมื่อไรก็คือส่งข้อมูลให้ GCM ครับ ซึ่ง GCM นั่นรองรับทั้งเชื่อมต่อแบบ HTTP และ XMPP โดยแนะนำให้ใช้เป็น HTTP เนอะ เข้าใจง่ายกว่า ส่วนหนึ่งก็เพราะฝั่ง Server เจ้าของบล็อกจะเขียนด้วย NodeJS ซึ่งเวลาส่งข้อมูลไปให้ GCM ผ่าน HTTP แบบง่ายๆ โดยสามารถดูรายละเอียดของการเชื่อมต่อกับ GCM ผ่าน HTTP ได้ที่ Implementing an HTTP Connection Server [Google Developers]

        ทาง GCM ก็ได้กำหนดไว้ว่าให้ใช้ POST Request ส่วนข้อมูลต่างๆจะอยู่ใน Header และ Body สและข้อมูลใน Body จะอยู่ในรูปของ JSON ซึ่งตรงนี้ขออธิบายนิดหน่อยนะครับ

        เวลา Server ต้องการส่งข้อมูลให้ GCM จะต้องเชื่อมต่อเป็นแบบ POST Request ไปที่

https://gcm-http.googleapis.com/gcm/send

        Header ใน Request จะต้องมี 2 อย่างด้วยกัน คือ
                • Authorization
                • Content-Type

        สำหรับ Authorization เอาไว้ใส่ API Key ที่ขอไว้เมื่อตอนแรกนู้นนนนนนน (ยังไม่ลืมเนอะ)

Authorization : key=YOUR_API_KEY

        ส่วน Content-Type เอาไว้กำหนดว่า Body ที่ส่งไปเป็นแบบ JSON หรือ Plain Text

Content-Type : application/json 
หรือ
Content-Type : application/x-www-form-urlencoded;charset=UTF-8

        Body ที่อยู่ในรูป JSON จะมีรายละเอียดปลีกย่อยค่อนข้างเยอะ สามารถดูจาก Reference ในหน้าเว็ปของ GCM ได้นะ

        ซึ่งรูปแบบข้อมูลใน Body จะมีอยู่คร่าวๆ 2 แบบคือ Data กับ Notification

        ยกตัวอย่างเช่น

{
    "to" : "TARGET_DEVICE_TOKEN",
    "data" : {
        "name" : "Akexorcist",
        "id" : "0001",
        "score" : 635709,
        "photo" : "/user_photo/0001"
    }
}

        อันนี้คือข้อมูลที่ Server อยากจะส่งให้กับอุปกรณ์แอนดรอยด์เป็นแบบ Data ครับ ข้างใน data ไม่มีรูปแบบตายตัว ขอแค่อยู่ในรูป JSON ก็พอ ดังนั้นในนี้จึงยัดข้อมูลได้ตามต้องการครับ แต่มีข้อแม้ว่าห้ามเกิน 4KB (สำหรับ iOS ห้ามเกิน 2KB) ส่วน to ก็เอาไว้ระบุ Token ของอุปกรณ์แอนดรอยด์ที่ต้องการจะส่งข้อมูลให้นั่นเอง

        ลองมาดูตัวอย่างของ Notification กันต่อเลย

{
    "to" : "TARGET_DEVICE_TOKEN",
    "notification" : {
        "body" : "Hello Android User",
        "title" : "Akexorcist",
        "icon" : "ic_launcher"
    }    
}

        Notification จะมีไว้สำหรับทำ Push Notification ครับ หรือก็คือแอพจะเอาข้อมูลไปแสดงเป็น Notification เลย จะได้ไม่ต้องมานั่งเขียนคำสั่งเอง

        คงไม่ต้องอธิบายอะไรเพิ่มเนอะ.... น่าจะมีแค่ icon ที่จะใช้ได้เฉพาะแอนดรอยด์เท่านั้น เพราะมันคือการระบุชื่อภาพไอคอนที่อยากจะให้แสดง โดยไฟล์ภาพที่ว่านั้นคือไฟล์ภาพใน drawable นั่นเอง

        และถ้าอยากจะส่งหลายๆคนพร้อมๆกันสามารถใส่ Token ID เป็น Array ลงไปใน registration_ids ได้เลย (เปลี่ยนจาก to เป็น registration_ids) ซึ่งส่งได้สูงสุดแค่ 1,000 เครื่องต่อหนึ่งครั้งเท่านั้น (ก็ส่งหลายๆรอบเอา)

{
    "registration_ids" : ["DEVICE_1", "DEVICE_2", "DEVICE_3"],
    "notification" : {
        "body" : "Hello Android User",
        "title" : "Akexorcist",
        "icon" : "ic_launcher"
    }    
}

        ไม่ยากเลยเนอะ?

โอเคพร้อมแล้ว มาทดสอบกัน

        ถึงจะบอกว่าทดสอบก็เถอะ แต่อย่าลืมอย่างหนึ่งครับ คือตอนนี้ผู้ที่หลงเข้ามาอ่านอาจจะยังไม่ได้เอา Token ส่งขึ้น Server ใช่มั้ยล่ะครับ (ซึ่งเวลาใช้งานจริงๆต้องทำเนอะ) ดังนั้นถ้าอยากจะทดสอบง่ายๆก่อนก็ให้แสดง Token ใน Log แบบชั่วคราวไปก่อน แล้วเอา Token ที่ได้ไปทดสอบดูครับ

        ดังนั้นเปิดไฟล์ GcmRegisterService ขึ้นมาแล้วเพิ่มคำสั่ง Log เข้าไปครับ

public class GcmRegisterService extends IntentService {
    ... 

    private void sendRegistrationToServer(String token) {
        Log.e(TAG, "Token : " + token);
    }

    ...
}

        พอลองทดสอบดูจะเห็น Token ของเครื่องนั้นๆแสดงขึ้นมาใน LogCat ก็ให้เก็บไว้ซะ เดี๋ยวจะเอาไว้ใช้ทดสอบ


       แล้วก็อย่าลืมไฟล์ GcmDownstreamService เพราะเวลา GCM ส่งข้อมูลมามันจะเข้าที่นี่ ซึ่งตอนนี้ยังไม่ได้ใส่โค้ดอะไรลงไป ก็ให้ทดสอบด้วยการแสดง Log บอกว่ามีข้อมูลส่งมาละกันเนอะ

public class GcmDownstreamService extends GcmListenerService {
    private static final String TAG = "GcmDownstreamService";

    @Override
    public void onMessageReceived(String from, Bundle data) {
        Log.e(TAG, "Message Incoming");
    }
}

        ส่วนฝั่ง Server ยังไม่มีเนอะ (ขี้เกียจอธิบายขั้นตอนส่วนนั้นด้วย) เพราะงั้นเจ้าของบล็อกจะใช้ Postman ในการทดสอบแทน เป็น Chrome Extension ที่เอาไว้ทดสอบอะไรแบบนี้ครับ สะดวกมากๆ แต่ต้องใช้บน Chrome Browser เท่านั้นนะ ลองไปติดตั้งได้จาก Postman [Chrome Web Store] ซึ่งตอนนี้เป็นเวอร์ชัน 4 ไปแล้ว แต่เจ้าของบล็อกยังใช้เวอร์ชัน 2 อยู่ (เพราะเวอร์ชันใหม่มันจะให้เปิดหน้าต่างแยก) ซึ่งหน้าต่างไม่ค่อยต่างกันมากนัก อย่าแปลกใจนะครับ


        พอเปิด Postman ขึ้นมาก็กำหนดให้ครบเลย เลือก POST Request ใส่ URL แล้วก็กำหนด Header และ Body ซึ่งขอทดสอบในส่วนของ Notification ละกันนะ

        พอลองกดส่งดู จะเห็น Reponse จาก GCM ส่งกลับมาประมาณนี้ครับ แปลว่าโอเคครับ มีการบอกจำนวนเครื่องที่ส่งสำเร็จและไม่สำเร็จ ซึ่งในนี้ส่งไปเครื่องเดียวเลยขึ้น Success เป็น 1 และ Failure เป็น 0


        แต่ถ้าไม่สำเร็จจะเป็นแบบนี้ พร้อมบอกสาเหตุให้ด้วย



        กลับมาดูที่ LogCat จะเห็นว่ามี Log แจ้งว่าได้รับข้อมูลจาก GCM แล้ว และจะเห็นว่ามี Notification ขึ้นมาทันที


GCM ไม่ได้ Realtime เสมอไปนะ

        ในกรณีที่เปิดแอพตัวนั้นๆไว้อยู่แล้วหรือว่าเปิดแอพตัวอื่นๆเล่นอยู่เวลาส่งข้อมูลมาจากทาง GCM ก็จะได้ข้อมูลในทันทีนะ

        ถ้าแต่เครื่องไม่ได้อยู่ในสถานะ Standby ก็อาจจะทำให้ได้รับข้อมูลจาก GCM ล่าช้าไปบ้าง ไม่ใช่เรื่องแปลกแต่อย่างใด เช่น เครื่องอาจจะอยู่ในสถานะประหยัดการใช้พลังงาน หรือเข้าสู่โหมด Doze (Android 6.0 ขึ้นไป) ถึงแม้ว่า Response จาก GCM จะบอกว่า Success ก็ตาม แต่ข้อมูลจะถูกส่งไปให้อุปกรณ์แอนดรอยด์ในภายหลัง ไม่ต้องห่วง

การเอาข้อมูลที่ GCM ส่งมาให้ ไปใช้งาน

        เจ้าของบล็อกยกตัวอย่างแค่ว่า "เมื่อข้อมูลมาให้บอกผ่าน LogCat" แต่ทีนี้ถ้าผู้ที่หลงเข้ามาอ่านอยากจะดึงข้อมูลในนั้นไปใช้งานล่ะ? ถ้าลองสังเกตดีๆจะเห็นว่า onMessageReceived มี Bundle ส่งเข้ามาด้วย นั่นแหละข้อมูล!!

        จากตัวอย่างที่ใช้ทดสอบเมื่อกี้ ถ้าอยากจะดึงข้อมูลที่อยู่ใน notification ก็จะทำได้ดังนี้ครับ

public class GcmDownstreamService extends GcmListenerService {
    ...

    @Override
    public void onMessageReceived(String from, Bundle data) {
        Log.e(TAG, "Message Incoming");
        Bundle notificationData = data.getBundle("notification");
        Log.e(TAG, "Title : " + notificationData.getString("title"));
        Log.e(TAG, "Body : " + notificationData.getString("body"));
        Log.e(TAG, "icon : " + notificationData.getString("ic_launcher"));
    }
}

        จะเห็นว่า getBundle จาก notification ก่อน แล้วถึงจะ getString ที่อยู่ข้างในนั้นได้

        แต่สำหรับข้อมูลแบบ Data จะแตกต่างกันไปหน่อย สมมติว่าเจ้าของบล็อกส่งข้อมูลมาแบบนี้นะครับ

{
  "to" : "TARGET_DEVICE_TOKEN",
  "data" : {
    "name" : "Akexorcist",
    "id" : "0001",
    "score" : 635709,
    "photo" : "/user_photo/0001"
  }
}

        ใน Bundle ไม่จำเป็นต้องใช้ getBundle เหมือนกับแบบ Notification ครับ แต่สามารถระบุ Data ที่อยู่ข้างในได้เลย

public class GcmDownstreamService extends GcmListenerService {
    ...

    @Override
    public void onMessageReceived(String from, Bundle data) {
        Log.e(TAG, "Message Incoming");
        Log.e(TAG, "Name : " + data.getString("name"));
        Log.e(TAG, "ID : " + data.getString("id"));
        Log.e(TAG, "Score : " + data.getString("score"));
        Log.e(TAG, "Photo : " + data.getString("photo"));
    }
}

        นั่นล่ะครับความแตกต่างระหว่างข้อมูลแบบ Data กับ Notification

GCM กับ Doze ฟีเจอร์ใหม่ใน Android 6.0

        เนื่องจาก Android 6.0 ขึ้นไปจะมาพร้อมกับฟีเจอร์ที่ชื่อว่า Doze ซึ่งจะช่วยลด Process ในการทำงานของอุปกรณ์แอนดรอยด์บางอย่างที่ไม่ได้ใช้ลง เพื่อทำให้เครื่องประหยัดแบตเตอรีมากขึ้น ใช้งานได้นานขึ้น


        สำหรับ User ก็ค่อนข้าง Happy แหละ แต่สำหรับนักพัฒนานี่สิ ทำให้เจอปัญหา GCM ส่งข้อมูลไม่เข้าแอพในเวลาที่เครื่องไม่ได้ใช้งานระยะหนึ่งแล้วเข้าโหมด Doze ซึ่งทาง GCM จึงได้เพิ่มคำสั่งอีกหนึ่งตัวเข้าไปเพื่อให้ข้อมูลสามารถส่งเข้าเครื่องได้ถึงแม้ว่าจะเข้าโหมด Doze อยู่ก็ตาม นั่นก็คือ priority

{
  "to" : "TARGET_DEVICE_TOKEN",
  "data" : {
    "name" : "Akexorcist",
    "id" : "0001",
    "score" : 635709,
    "photo" : "/user_photo/0001"
  },
  "priority" : "high"
}

        โดยปกติแล้วถ้าไม่ได้ใส่ priority ไว้ มันจะกำหนดเป็น normal ให้โดยอัตโนมัติ ซึ่งเวลาอุปกรณ์แอนดรอยด์เข้าโหมด Doze ก็จะยังไม่ได้รับข้อมูลจาก GCM ในทันทีจนกว่าเครื่องจะออกจากโหมด Doze

        แต่เมื่อกำหนดเป็น High ก็จะทำให้แอพสามารถรับข้อมูลจาก GCM ได้โดยไม่สนใจ Doze นั่นเอง

        ลองดูรายละเอียดของเรื่องนี้ได้ที่ How Google Cloud Messaging handles Doze in Android 6.0 Marshmallow

สรุป

        GCM ถือว่าเป็นหนึ่งในตัวช่วยสำคัญที่จะทำให้หลายๆแอพสามารถทำ Push Notification หรือว่าส่งข้อมูลจาก Server ไปยังอุปกรณ์แอนดรอยดืได้ง่ายขึ้น สะดวกขึ้น ไม่ต้องจัดการเอง ซึ่งแอพหลายๆตัวมักจะมีฟีเจอร์นี้อยู่ด้วย ดังนั้นการใช้ GCM ก็ถือเป็นคำตอบที่ดี แถมฟรีด้วย รองรับทั้ง Android และ iOS ที่เหลือก็ขึ้นอยู่กับผู้ที่หลงเข้ามาอ่านแล้วล่ะว่าจะเอาไปประยุกต์ทำอะไรในแอพ

        อ้อ แถมอีกเรื่องหนึ่ง ก็คือ บทความนี้แสดงตัวอย่างการส่งข้อมูลให้ทีละเครื่อง แต่จริงๆแล้ว GCM สามารถส่งข้อมูลแบบกลุ่มได้ด้วยนะครับ ลองไปดูเพิ่มเติมเองได้ที่ Device Group Messaging [Google Developers]

        ในการส่งข้อมูลหลายๆเครื่องนั้น GCM จะสามารถทำได้ 1,000 เครื่องต่อครั้งเท่านั้น ดังนั้นถ้าอยากจะส่งมากกว่านั้นก็ต้องให้ Server ทยอยส่งให้ GCM หลายๆครั้งจนครบทุกเครื่องครับ

        และนอกจากนี้ยังมีความสามารถอื่นๆอีกหลายๆอย่างของ GCM ที่แนะนำให้ลองเข้าไปศึกษาดูได้ใน Cloud Messaging [Google Developers]




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium