/*
* Copyright (c) 2013 Google Inc.
*
* 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.cloud.backend.android;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.api.client.util.DateTime;
import com.google.cloud.backend.android.CloudQuery.Order;
import com.google.cloud.backend.android.CloudQuery.Scope;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Cloud Backend API class that provides pub/sub messaging feature in addition
* to the features of {@link CloudBackendAsync}.
*
*/
public class CloudBackendMessaging extends CloudBackendAsync {
/**
* Creates an instance of {@link CloudBackendAsync}. Caller need to pass a
* {@link Context} such as {@link Activity} that will be used to Google
* Cloud Messaging.
*
* @param context
* {@link Context}
*/
public CloudBackendMessaging(Context context) {
super(context);
}
/**
* Returns {@link SharedPreferences} that can be used for storing any for
* Cloud Backend related preferences.
*
* @return {@link SharedPreferences}
*/
public SharedPreferences getSharedPreferences() {
return context.getSharedPreferences(Consts.PREF_KEY_CLOUD_BACKEND, Context.MODE_PRIVATE);
}
/**
* Kind name of {@link CloudEntity} for Cloud Message.
*/
public static final String KIND_NAME_CLOUD_MESSAGES = "_CloudMessages";
/**
* Property name of _CloudMessages kind that holds topicId.
*/
public static final String PROP_TOPIC_ID = "topicId";
/**
* TopicId for broadcast messages.
*/
public static final String TOPIC_ID_BROADCAST = "_broadcast";
// SharedPreference key for timestamp of the last message
private static final String PREF_KEY_PREFIX_MSG_TIMESTAMP = "PREF_KEY_PREFIX_MSG_TIMESTAMP";
// subscription for Cloud Message would last for 7 days
private static final int SUBSCRIPTION_DURATION_FOR_PUSH_MESSAGE = 60 * 60 * 24 * 7;
// max number of past messages to receive
private static final int DEFAULT_MAX_MESSAGES_TO_RECEIVE = 100;
// Map of handlers for Cloud Messages (key = topicId)
private static final Map<String, CloudCallbackHandler<List<CloudEntity>>> cloudMessageHandlers = new HashMap<String, CloudCallbackHandler<List<CloudEntity>>>();
/**
* Subscribes to Cloud Messages for the specified Topic ID. Developer may
* use any string (that does not include any space chars) as a Topic ID,
* such as an ID for user, group, hash tag, keyword, or etc.
*
* By specifying the optional maxOfflineMessages number, you can receive
* off-line messages after the last message received. These include the
* messages sent to the topicId while the app have been off-line, wasn't
* running, or before the app launched for the first time.
*
* @param topicId
* Topic ID for the subscription
* @param cloudMsgHandler
* {@link CloudCallbackHandler} that will handle Cloud Message
* for the topic. It will receive {@link List} of
* {@link CloudEntity}s that contain the messages.
* @param maxOfflineMessages
* (optional) if you want to receive all the off-line messages
* sent after the last message received, set max number of the
* messages to receive.
*/
public void subscribeToCloudMessage(String topicId, CloudCallbackHandler<List<CloudEntity>> handler,
int... maxOfflineMessages) {
// register the hander for this topic id
cloudMessageHandlers.put(topicId, handler);
// create and execute a query for Cloud Message
int maxOfflineMsgs = maxOfflineMessages.length > 0 ? maxOfflineMessages[0] : 0;
CloudQuery cq = createQueryForCloudMessage(topicId, maxOfflineMsgs);
super.list(cq, new CloudMessageHandler());
}
private CloudQuery createQueryForCloudMessage(String topicId, int maxOfflineMessages) {
// whether the query should include past messages since the last message
boolean includeOfflineMessages = maxOfflineMessages > 0;
long lastTime = (new Date()).getTime();
if (includeOfflineMessages) {
lastTime = getSharedPreferences().getLong(getPrefKeyForTopicId(topicId), lastTime);
}
// create query
CloudQuery cq = new CloudQuery(KIND_NAME_CLOUD_MESSAGES);
cq.setFilter(F.and(F.eq(PROP_TOPIC_ID, topicId), F.gt(CloudEntity.PROP_CREATED_AT, new DateTime(lastTime))));
cq.setSort(CloudEntity.PROP_CREATED_AT, Order.DESC);
cq.setSubscriptionDurationSec(SUBSCRIPTION_DURATION_FOR_PUSH_MESSAGE);
if (includeOfflineMessages) {
cq.setLimit(maxOfflineMessages);
cq.setScope(Scope.FUTURE_AND_PAST);
} else {
cq.setLimit(DEFAULT_MAX_MESSAGES_TO_RECEIVE);
cq.setScope(Scope.FUTURE);
}
// set topicId as a queryId. Any queries on the same topic will
// be treated as just one query.
cq.setQueryId(topicId);
return cq;
}
/**
* Unsubscribes from Cloud Message for the specified topicId.
*
* @param topicId
*/
public void unsubscribeFromCloudMessage(String topicId) {
cloudMessageHandlers.remove(topicId);
CloudBackendAsync.continuousQueries.remove(topicId);
}
private String getPrefKeyForTopicId(String topicId) {
return PREF_KEY_PREFIX_MSG_TIMESTAMP + ":" + topicId;
}
// handles Cloud Message
private class CloudMessageHandler extends CloudCallbackHandler<List<CloudEntity>> {
@Override
public void onComplete(List<CloudEntity> messages) {
// skip if it's empty
if (messages.isEmpty()) {
return;
}
// get the last message and store the timestamp in the shared pref
CloudEntity lastMsg = messages.get(0);
String topicId = (String) lastMsg.get(PROP_TOPIC_ID);
long lastTime = lastMsg.getCreatedAt().getTime();
SharedPreferences.Editor e = getSharedPreferences().edit();
e.putLong(getPrefKeyForTopicId(topicId), lastTime);
e.commit();
// find handler for this topic
CloudCallbackHandler<List<CloudEntity>> handler = cloudMessageHandlers.get(topicId);
if (handler == null) {
return;
}
// refresh subscriber query to receive new messages from now
CloudQuery cq = createQueryForCloudMessage(topicId, DEFAULT_MAX_MESSAGES_TO_RECEIVE);
ContinuousQueryHandler cqh = CloudBackendAsync.continuousQueries.get(topicId);
CloudBackendAsync.continuousQueries.put(topicId, new ContinuousQueryHandler(cqh.getHandler(), cq,
getCredential()));
// sort messages by createdAt ASC
Collections.reverse(messages);
// call the handler
handler.onComplete(messages);
}
}
/**
* Sends a Cloud Message to the specified topicId.
*
* @param message
* properties to include in the message
*/
public void sendCloudMessage(CloudEntity message) {
super.insert(message, null); // no callback
}
/**
* Sends a Cloud Message to the specified topicId.
*
* @param message
* properties to include in the message
* @param handler
* {@link CloudCallbackHandler} that has
* {@link #onComplete(List)} which will be called after sending
* specified message to backend, or {@link #onError(IOException)}
* which will be called on error.
*/
public void sendCloudMessage(CloudEntity message, CloudCallbackHandler<CloudEntity> handler) {
super.insert(message, handler);
}
/**
* Creates a {@link CloudEntity} that represent a Cloud Message. You can use
* {@link #sendCloudMessage(CloudEntity)} to send out the message.
*
* @param topicId
* @return
*/
public CloudEntity createCloudMessage(String topicId) {
CloudEntity ce = new CloudEntity(KIND_NAME_CLOUD_MESSAGES);
ce.put(PROP_TOPIC_ID, topicId);
return ce;
}
/**
* Creates a {@link CloudEntity} for message broadcasting. You can use
* {@link #sendCloudMessage(CloudEntity)} to send out the message.
*
* @return {@link CloudEntity}
*/
public CloudEntity createBroadcastMessage() {
return createCloudMessage(TOPIC_ID_BROADCAST);
}
}