출처: http://loeelver.blogspot.kr/2014/01/gcmgoogle-cloud-message.html

android - GCM(Google Cloud Message)

GCM(Google Cloud Message) 사용을 위한 준비단계

1. Google Developer Console 접속
 - 접속 URL : https://cloud.google.com/console

2. API Project를 생성하지 않았다면 "CREATE PROJECT" 버튼 클릭으로 생성
 - Project가 생성되면 Project ID와 Number가 표시됨
 - Project Number는 이후 GCM Sender에서 사용하게 됨

3. GCM Service 활성화
 1) 왼쪽 사이드바에서 APIs & auth 선택
 2) API 목록에서 Google Cloud Messaging for Android 항목을 ON으로 변경

4. Android API Key 얻기
 1) 왼쪽 사이드바에서  Credentials 선택
 2) 아래 Public API access에서 "Create new key" 버튼 클릭
 3) Create new key 다이얼로그에서 "Android key" 버튼 클릭
 4) 결과 다이얼로그의 입력창에 SHA1 fingerprint와 packageName 입력
   - SHA1 fingerprint 추출방법
    keytool -list -v -keystore mystore.keystore
   - 입력양식 예
    B6:F1:37:71:BB:1D:A6:D8:59:61:A6:D3:02:5D:54:64:5F:FA:23:F6;com.myexample
  5) "Create" 버튼 클릭
  6) 갱신된 화면에 표시된 API key는 Android Application의 인증에 사용됨

5. Server API Key 얻기
 1) 왼쪽 사이드바에서  Credentials 선택
 2) 아래 Public API access에서 "Create new key" 버튼 클릭
 3) Create new key 다이얼로그에서 "Server key" 버튼 클릭
 4) 결과 다이얼로그의 입력창에 GCM Message를 전송할 서버주소 또는 네트웍 주소를 입력한다.(예 : 192.168.0.1 또는 192.168.0.0/24)
 5) "Create" 버튼 클릭
 6) 갱신된 화면에 표시된 API key는 3rd-Party Application Server의 인증에 사용됨


GCM 클라이언트 구현

GCM enabled app 구현시 이전버전의 client helper library를 아직도 지원하고는 있지만 GoogleCloudMessaging로 대체되었기에  GoogleCloudMessaging API를 사용할 것을 천한다.

1. Google Play Service 설정
 GoogleCloudMesasging API를 사용하기 위해선 Google Play Service를 설정해야 한다.

Google Play Service 설정 참고 URL : http://developer.android.com/google/play-services/setup.html

2. Manifest.xml 수정
아래 해당항목을 추가
 1)  GCM 메시지 수신을 위해 다음 퍼미션 추가
  com.google.android.c2dm.permission.RECEIVE
 2) android app가 3rd-party app sever에 registration id를 전송하기 위해 다음 퍼미션 추가
  android.permission.INTERNET
 3) GCM은 Google account를 필요로 하며 android 4.0.4 미만 버전에서는 다음 퍼미션 추가
  android.permission.GET_ACCOUNTS
 4) 메시지가 수신될때 sleep모두에서 프로세스를 보호하기 위해 다음 퍼미션 추가(해당 퍼미션은 부가적인 옵션)
  android.permission.WAKE_LOCK
 5) 다른 안드로이드 어플에서 메시지 수신하는것을 막기 위해 다음 퍼미션 추가(해당 양식을 지켜야 함)
 " + applicationPackage + ".permission.C2D_MESSAGE
 6) com.google.android.c2dm.intent.RECEIVE 메시지를 수신하기 위한 receiver 등록
  - applicationPackage 이름의 category
  - com.google.android.c2dm.SEND 퍼미션을 요구
 7) GCM Message를 처리할 Service를 등록(해당 기능은 부가적인 옵션)

<manifest package="com.example.gcm" ...>

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
    <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" />

    <application ...>
        <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" />
    </application>
</manifest>

  8) Google Play Service 사용을 위해 application 태그에 com.google.android.gms.version meta-data 태그 추가(gcm helper libraray : gcm.jar 사용시 불 필요)

    <application ...>
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
    </application>
3. App 구현

1) Check Google Play Service APK
 Google Play Service가 사용가능한지 체크하며 불가시 다운로드를 유도하는 다이얼로그 표시
 Google Play Service version 3.1 이상을 필요로한다.

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;
}

 2) GCM 등록
메시지를 수신하기 위해 GCM server 등록이 필요하며 등록되면 registration id를 받는다.
등록시 Project Number를 사용함(SENDER_ID는 Project Number)

gcm = GoogleCloudMessaging.getInstance(this);
regid = getRegistrationId(context);
if (regid.isEmpty()) {
    registerInBackground();
}

private String getRegistrationId(Context context) {
    // 저장된 regId}
private void registerInBackground() {
    new AsyncTask() {
        @Override
        protected String doInBackground(Void... params) {
            String msg = "";
            try {
                if (gcm == null) {
                    gcm = GoogleCloudMessaging.getInstance(context);
                }
                regid = gcm.register(SENDER_ID);
                msg = "Device registered, registration ID=" + regid;

                sendRegistrationIdToBackend();

                storeRegistrationId(context, regid);
            } catch (IOException ex) {
                msg = "Error :" + ex.getMessage();
            }
            return msg;
        }

        @Override
        protected void onPostExecute(String msg) {
            mDisplay.append(msg + "\n");
        }
    }.execute(null, null, null);
    ...
}
private void sendRegistrationIdToBackend() {
    // 등록 성공시 리턴받은 regId를 자신이 구현한 3rd-party server에 알려준다.
}
private void storeRegistrationId(Context context, String regId) {
    // regId를 저장
}

 3) 메시지 수신
  - 시스템은 들어는 메시지를 수신하여 메시지 payload 에서 key/value를 분류한다.
  - 시스템은 대상 어플에게 Intent의 extra에 com.google.android.c2dm.intent.RECEIVE 값으로 전달한다.
  - 안드로이드 어플은 해당 메시지를 broadcast receiver로 전달받아 값을 처리한다.

device에 메시지는 broadcast message로 전달되는 방식이이며 WakefulBroadcastReceiver는 wake lock에 특화된 broadcast receiver이다.
WakefulBroadcastReceiver sleep모드에서 GCM message를 수신하기 위해 사용하며 sleep 모드에서 GCM message를 받을 필요가 없다면 일반 broadcast receiver를 사용하면 된다.

startWakefulService 메소드로 GCM message를 처리할 서비스를 실행시킨다.
이는 startService와 동일하나 service가 시작되면 wake lock 잡고 있는다.
service가 작업을 완료하면 wake lock을 풀어주기 위해  GcmBroadcastReceiver.completeWakefulIntent()를 호출한다.

pblic class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ComponentName comp = new ComponentName(context.getPackageName(),
                GcmIntentService.class.getName());
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);
    }
}
public class GcmIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;

    public GcmIntentService() {
        super("GcmIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
        String messageType = gcm.getMessageType(intent);
        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)) {
                sendNotification("Received: " + extras.toString());
                Log.i(TAG, "Received: " + extras.toString());
            }
        }
        GcmBroadcastReceiver.completeWakefulIntent(intent);
    }


GCM Server 구현

 Google에서 제공된 GCM Connection Server는 3rd-party app server로 부터 메시지를 수신하여 GCM enabled된 android app에 전달(Google에서 제공하는 HTTP 또는 CCS(XMPP))

참고)
 XMPP(Extensible Messaging and Presence Protocol) : XML에 기반한 메시지 지향 미들웨어용 통신 프로토콜

 smack :android를 위한 XMPP library (참고 URL : https://github.com/Flowdalic/asmack, http://www.igniterealtime.org/projects/smack/)

1. GCM Connection Server 프로토콜 선택
 HTTP와 CCS(XMPP) 방식중 아래 특징을 확인 후 선택
 1) 양방향 메시지
  - HTTP : cloud-to-device 만 가능
  - CCS : cloud-to-device, device-to-cloud 모두 가능
 2) 비동기 메시지
  - HTTP : post 방식을 사용하며 요청 후 응답을 기다리는 동기방식
  - CCS : server나 client 모두에서 메시지를 전송하면 CCS에서 이에 대한 결과를 알려주는 비동기 방식
 3) JSON
  - HTTP : post 방식으로 전송
  - CCS : XMPP 메시지에서 JSON을 암호화 시킴

참고)
 - HTTP를 사용할 경우 GCM server helper library를 사용하거나 직접 HTTP 통신 구현
 - XMPP를 사용할 경우 GooglePlayService의 GCM또는 java smack demo app 참조

데이터 보안이나 메시지 전송의 성능 및 양방향 메시지 구현을 위해서 CCS 방식이 추천되나 단지 메시지 통보가 목적이라면 HTTP 방식도 괜찮겠다.

2. 3rd-party app server 구현 필요사항
 3rd-party app server는 다음과 같은 기준을 만족해야 함
 1) client와 통신이 가능해야 함
 2) GCM connect server에 올바르게 요청해야 함
 3) 지연시간 기반의 전송장애를 체크하여 요청이나 재전송을 할 수 있어야 함
 4) API keys와 registration id를 저장할 수 있어야 함
 5) 각각의 메시지는 유니크한 id를 가져야 함

3. 메시지 전송
 일반적인 형태의 메시지 전송 순서
 1) app server가 GCM connection server로 메시지 전송
 2) device가 offline이면 메시지를 저장하고 큐에 넣음
 3) device가 online일때 메시지를 device에 전송
 4) device내에서 부합하는 퍼미션을 갖는 어플에 시스템 메시지가 브로드캐스트로 전달되어진다.(메시지를 받기 위해 어플이 사전에 실행되고 있을 필요가 없음)
 5) android app가 해당 메시지를 처리

4. 메시지 전송을 위한 최소 필요사항
 1) Target
  - HTTP 방식에서 다음중 하나를 명시해야 한다.
   registration_ids : 1 ~ 1000대까지 복수의 디바이스에 복수의 registration_id를 사용하여 메시지 전송시(멀티캐스트 메시지)
   notification_key : 한명 소유의 복수 단말에 메시지 전송시 사용
  - CCS 방식
   "to" 필드로 단일의 registration_id나 notification_key를 가져야 하며 멀티캐스트 메시지는 지원하지 않음
 2) Payload
  -  부가적으로 메시지에 payload를 갖고 있다면 data 파라미터를 가져야함(HTTP와 CCS 모두 해당)
 3) Message Parameters
  - JSON 메시지 파라미터(HTTP와 CCS 모두 해당)
  - Plain text (HTTP only)

  참고 URL : http://developer.android.com/google/gcm/server.html#send-msg

5. 제약사항
 - GCM Message는 최대 4kbytes까지만 가능하다.
 - Message는 순서대로 전달되는것을 보장하지 않는다.

 참고 URL : http://developer.android.com/training/cloudsync/gcm.html

6. XMPP 방식의 GCM Message 전송(Google Play Service)
device에서 GCM Server로 메시지를 전송하느느 send_event 타입

AtomicInteger msgId = new AtomicInteger();

private void sendGCMMessage() throws IOException {
    Bundle data = new Bundle();
    data.putString("message", "GCM test message");

    String id = Integer.toString(msgId.incrementAndGet());

    if (gcm == null) {
        gcm = GoogleCloudMessaging.getInstance(context);
    }
    gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data);
}

7. XMPP 방식의 GCM Message 전송(Smack Library 이용)
- 라이브러리는 http://www.igniterealtime.org/projects/smack/에서 다운로드 받도록한다.
- 실제 구현시 로그인, 로그아웃, 메시지 전송을 분리하여 로그인된 커넥션을 재사용하여 메시지를 전송하게 하여야 한다.
- PacketListener를 통해 메시지 패킷을 수신한다.

private void sendGCMMessage() {
    final String SENDER_ID = // Project Number
    final String apiKey = // Server API Key에서 얻은 API Key

    SASLAuthentication.supportSASLMechanism("PLAIN", 0);

    ConnectionConfiguration config = new ConnectionConfiguration("gcm.googleapis.com", 5235);
    config.setSASLAuthenticationEnabled(true);
    config.setSocketFactory(SSLSocketFactory.getDefault());
    config.setDebuggerEnabled(true);

    Connection connection = new XMPPConnection(config);

    try {
        PacketListener recvListener = new PacketListener() {
            public void processPacket(Packet packet) {
                System.out.println("PacketListener:processPacket:\n" + packet.toXML());
            }
        };
   
        Packet sendPacket = null;// create Packet ...
   
        connection.connect();
        connection.login(SENDER_ID + "@gcm.googleapis.com", apiKey);
        connection.addPacketListener(recvListener, null);
        connection.sendPacket(sendPacket);
    } catch (XMPPException e) {
        e.printStackTrace();
    }
}

참고 URL : http://developer.android.com/google/gcm/ccs.html#implement

8. HTTP 방식의 GCM Message 전송
 - Client에서 전달받은 registrationId를 최대 1000개까지 배열 형태로 세팅하여 전송가능
    --> 단일 메시지 전송으로 최대 1000대의 디바이스에 메시지를 멀티캐스팅할 수 있음.
 - registration_ids만 필수항목이고 나머진 옵션이다.
 - data가 전송할 메시지의 payload가 된다.

private void sendGCMMessage() {
    try {
        String regId = ""; // GCM Client에서 등록할때 받은 registrationId
        String apiKey = ""; // Server API Key 얻기에서 생성된 API Key
        String json = "{ \"registration_ids\" : [\"" + regId + "\"], \"data\" : { \"message\" : \"GCM test message\" } }";

        URL url = new URL("https://android.googleapis.com/gcm/send");
        HttpsURLConnection urlConn = (HttpsURLConnection) url.openConnection();
        urlConn.setRequestMethod("POST");
        urlConn.addRequestProperty("Authorization", "key=" + apiKey);
        urlConn.addRequestProperty("Content-Type", "application/json");

        urlConn.setDoInput(true);
        urlConn.setDoOutput(true);

        OutputStream os = urlConn.getOutputStream();
        os.write(json.getBytes());
        os.close();

        System.out.println("(" + urlConn.getResponseCode() + "):" + urlConn.getResponseMessage());
   
       InputStream is = urlConn.getInputStream();
       byte[] buf = new byte[1024];
       while(is.available() > 0) {
           int readCount = is.read(buf);
           if(readCount > 0) {
           System.out.println(new String(buf, 0, readCount));
      }
    }
} catch (Exception e) {
    e.printStackTrace();
}

9. GCM Server Helper Library(gcm-server.jar)를 이용한 GCM Message 전송
 - HTTP 방식으로 구현된것으로 판단됨
 - Client에서 전달받은 registrationId를 최대 1000개까지 컬렉션 형태로 세팅하여 전송가능
    --> 단일 메시지 전송으로 최대 1000대의 디바이스에 메시지를 멀티캐스팅할 수 있음.

private void sendGCMMessage() {
    String regId = ""; // GCM Client에서 등록할때 받은 registrationId
    String apiKey = ""; // Server API Key 얻기에서 생성된 API Key
    Sender sender = new Sender(apiKey);
    Message message = new Message.Builder();addData("message", "GCM test message").build();

    try {
        Result result = sender.send(message, regId, 5);
        System.out.println("result:" + result);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

...

참고 사항

에러 대응하기

1. XMPP로 upstream이 되지 않을때,
XMPP 기반의 upstream을 사용하고자 한다면 아래 URL에 접속하여 신청서를 작성하세요.
https://services.google.com/fb/forms/gcm/

2. Server returned HTTP response code: 401 for URL: https://android.googleapis.com/gcm/send
--> 사용된 apiKey가 잘못되었을 수 있음(Server API Key를 사용해야 함)

3. smack 라이브러리로 CCI(XMPP) 방식으로 메시지 전송시 다음과 같은 에러는 1번처럼 CCI 기반의 upstream 허가가 나지 않은 경우임
 1) "SASL authentication PLAIN failed: text:"
 2) Project not whitelisted.
Posted by outliers
,