/* * Copyright (C) 2013-2017 microG Project Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.gms.gcm; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Looper; import android.text.TextUtils; import com.google.android.gms.iid.InstanceID; import org.microg.gms.common.PublicApi; import org.microg.gms.gcm.CloudMessagingRpc; import org.microg.gms.gcm.GcmConstants; import java.io.IOException; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE; import static org.microg.gms.gcm.GcmConstants.EXTRA_DELAY; import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID; import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_TYPE; import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER; import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER_LEGACY; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_FROM; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_TO; import static org.microg.gms.gcm.GcmConstants.EXTRA_TTL; /** * GoogleCloudMessaging (GCM) enables apps to communicate with their app servers * using simple messages. *
* To send or receive messages, the app must get a * registrationToken from {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}, which authorizes an * app server to send messages to an app instance. Pass sender ID and * {@link com.google.android.gms.gcm.GoogleCloudMessaging#INSTANCE_ID_SCOPE} as parameters to the method. * A sender ID is a project number acquired from the API console, as described in * Getting Started. * * In order to receive GCM messages, declare {@link com.google.android.gms.gcm.GcmReceiver} * and an implementation of {@link com.google.android.gms.gcm.GcmListenerService} in the app manifest. * {@link com.google.android.gms.gcm.GcmReceiver} will pass the incoming messages to the implementation * of {@link com.google.android.gms.gcm.GcmListenerService}. To process messages, override base class * methods to handle any events required by the application. * * Client apps can send upstream messages back to the app server using the XMPP-based * Cloud Connection Server, * For example: * * gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data); * See Implementing GCM Client on Android for more details. */ @PublicApi public class GoogleCloudMessaging { /** * The GCM {@link com.google.android.gms.gcm.GoogleCloudMessaging#register(java.lang.String...)} and {@link com.google.android.gms.gcm.GoogleCloudMessaging#unregister()} methods are * blocking. Blocking methods must not be called on the main thread. */ public static final String ERROR_MAIN_THREAD = "MAIN_THREAD"; /** * The device can't read the response, or there was a 500/503 from the * server that can be retried later. The application should use exponential * back off and retry. */ public static final String ERROR_SERVICE_NOT_AVAILABLE = GcmConstants.ERROR_SERVICE_NOT_AVAILABLE; /** * Specifies scope used in obtaining GCM registrationToken when calling * {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)} */ public static final String INSTANCE_ID_SCOPE = GcmConstants.INSTANCE_ID_SCOPE_GCM; /** * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate that the server deleted * some pending messages because they exceeded the storage limits. The * application should contact the server to retrieve the discarded messages. * * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onDeletedMessages()} */ @Deprecated public static final String MESSAGE_TYPE_DELETED = GcmConstants.MESSAGE_TYPE_DELETED_MESSAGE; /** * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a regular message. * * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onMessageReceived(java.lang.String, android.os.Bundle)} */ @Deprecated public static final String MESSAGE_TYPE_MESSAGE = GcmConstants.MESSAGE_TYPE_GCM; /** * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a send error. * The intent includes the message ID of the message and an error code. * * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onSendError(java.lang.String, java.lang.String)} */ @Deprecated public static final String MESSAGE_TYPE_SEND_ERROR = GcmConstants.MESSAGE_TYPE_SEND_ERROR; /** * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a sent message has been received by the GCM * server. The intent includes the message ID of the message. * * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onMessageSent(java.lang.String)} */ @Deprecated public static final String MESSAGE_TYPE_SEND_EVENT = GcmConstants.MESSAGE_TYPE_SEND_EVENT; private static GoogleCloudMessaging instance; private CloudMessagingRpc rpc; private Context context; public GoogleCloudMessaging() { } /** * Must be called when your application is done using GCM, to release * internal resources. */ public synchronized void close() { instance = null; rpc.close(); } /** * Return the singleton instance of GCM. */ public static GoogleCloudMessaging getInstance(Context context) { if (instance == null) { instance = new GoogleCloudMessaging(); instance.context = context.getApplicationContext(); instance.rpc = new CloudMessagingRpc(instance.context); } return instance; } /** * Return the message type from an intent passed into a client app's broadcast receiver. There * are two general categories of messages passed from the server: regular GCM messages, * and special GCM status messages. * * The possible types are: * {@link #MESSAGE_TYPE_MESSAGE}, {@link #MESSAGE_TYPE_DELETED}, {@link #MESSAGE_TYPE_SEND_EVENT} and {@link #MESSAGE_TYPE_SEND_ERROR} * * You can use this method to filter based on message type. Since it is likely that GCM will * be extended in the future with new message types, just ignore any message types you're not * interested in, or that you don't recognize. * * @return The message type or null if the intent is not a GCM intent */ public String getMessageType(Intent intent) { if (intent == null || !ACTION_C2DM_RECEIVE.equals(intent.getAction())) return null; if (!intent.hasExtra(EXTRA_MESSAGE_TYPE)) return MESSAGE_TYPE_MESSAGE; return intent.getStringExtra(EXTRA_MESSAGE_TYPE); } /** * Register the application for GCM and return the registration ID. You must call this once, * when your application is installed, and send the returned registration ID to the server. * * Repeated calls to this method will return the original registration ID. * * If you want to modify the list of senders, you must callunregister()
first.
*
* Most applications use a single sender ID. You may use multiple senders if different
* servers may send messages to the app or for testing.
*
* @param senderIds list of project numbers or Google accounts identifying who is allowed to
* send messages to this application.
* @return registration id
* @throws IOException
* @deprecated Instead, for GCM registration, use
* {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}.
* Set authorizedEntity to a sender ID and scope to {@link com.google.android.gms.gcm.GoogleCloudMessaging#INSTANCE_ID_SCOPE}.
*/
@Deprecated
public String register(String... senderIds) throws IOException {
if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
if (senderIds == null || senderIds.length == 0) throw new IllegalArgumentException("No sender ids");
StringBuilder sb = new StringBuilder(senderIds[0]);
for (int i = 1; i < senderIds.length; i++) {
sb.append(',').append(senderIds[i]);
}
String sender = sb.toString();
if (isLegacyFallback()) {
Bundle extras = new Bundle();
extras.putString(EXTRA_SENDER_LEGACY, sender);
return InstanceID.getInstance(context).getToken(sb.toString(), INSTANCE_ID_SCOPE, extras);
} else {
Bundle extras = new Bundle();
extras.putString(EXTRA_SENDER, sender);
return rpc.handleRegisterMessageResult(rpc.sendRegisterMessageBlocking(extras));
}
}
/**
* Send an upstream ("device to cloud") message. You can only use the upstream feature
* if your GCM implementation uses the XMPP-based
* Cloud Connection Server.
*
* The current limits for max storage time and number of outstanding messages per
* application are documented in the
* GCM Developers Guide.
*
* @param to string identifying the receiver of the message in the format of
* SENDER_ID@gcm.googleapis.com
. The SENDER_ID
should be one of the sender
* IDs used when calling {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}
* @param msgId ID of the message. This is generated by the application. It must be
* unique for each message. This allows error callbacks and debugging.
* @param timeToLive If 0, we'll attempt to send immediately and return an
* error if we're not connected. Otherwise, the message will be queued.
* As for server-side messages, we don't return an error if the message has been
* dropped because of TTL—this can happen on the server side, and it would require
* extra communication.
* @param data key/value pairs to be sent. Values must be String, any other type will
* be ignored.
* @throws IllegalArgumentException
* @throws IOException
*/
public void send(String to, String msgId, long timeToLive, Bundle data) throws IOException {
if (TextUtils.isEmpty(to)) throw new IllegalArgumentException("Invalid 'to'");
if (isLegacyFallback()) {
Bundle extras = new Bundle();
for (String key : data.keySet()) {
Object o = extras.get(key);
if (o instanceof String) {
extras.putString("gcm." + key, (String) o);
}
}
extras.putString(EXTRA_SEND_TO, to);
extras.putString(EXTRA_MESSAGE_ID, msgId);
InstanceID.getInstance(context).requestToken("GCM", "upstream", extras);
} else {
Bundle extras = data != null ? new Bundle(data) : new Bundle();
extras.putString(EXTRA_SEND_TO, to);
extras.putString(EXTRA_SEND_FROM, getFrom(to));
extras.putString(EXTRA_MESSAGE_ID, msgId);
extras.putLong(EXTRA_TTL, timeToLive);
extras.putInt(EXTRA_DELAY, -1);
rpc.sendGcmMessage(extras);
}
}
/**
* Send an upstream ("device to cloud") message. You can only use the upstream feature
* if your GCM implementation uses the XMPP-based
* Cloud Connection Server.
*
* When there is an active connection the message will be sent immediately, otherwise the
* message will be queued for the maximum interval.
*
* @param to string identifying the receiver of the message in the format of
* SENDER_ID@gcm.googleapis.com
. The SENDER_ID
should be one of the sender
* IDs used when calling {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}
* @param msgId ID of the message. This is generated by the application. It must be
* unique for each message. This allows error callbacks and debugging.
* @param data key/value pairs to be sent. Values must be String—any other type will
* be ignored.
* @throws IllegalArgumentException
* @throws IOException
*/
public void send(String to, String msgId, Bundle data) throws IOException {
send(to, msgId, -1, data);
}
/**
* Unregister the application. Calling unregister()
stops any
* messages from the server. This is a blocking call—you shouldn't call
* it from the UI thread.
*
* You should rarely (if ever) need to call this method. Not only is it
* expensive in terms of resources, but it invalidates all your registration IDs
* returned from register() or subscribe(). This should not be done
* unnecessarily. A better approach is to simply have your server stop
* sending messages.
*
* @throws IOException if we can't connect to server to unregister.
* @deprecated Instead use
* {@link com.google.android.gms.iid.InstanceID#deleteToken(java.lang.String, java.lang.String)} or
* {@link com.google.android.gms.iid.InstanceID#deleteInstanceID()}.
*/
@Deprecated
public void unregister() throws IOException {
if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
InstanceID.getInstance(context).deleteInstanceID();
}
private boolean isLegacyFallback() {
String gcmPackageName = CloudMessagingRpc.getGcmPackageName(context);
return gcmPackageName != null && gcmPackageName.endsWith(".gsf");
}
private String getFrom(String to) {
int i = to.indexOf('@');
if (i > 0) {
to = to.substring(0, i);
}
return InstanceID.getInstance(context).getStore().getToken("", to, INSTANCE_ID_SCOPE);
}
}