ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePush.java

547 lines
20 KiB
Java

/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code ParsePush} is a local representation of data that can be sent as a push notification.
* <p/>
* The typical workflow for sending a push notification from the client is to construct a new
* {@code ParsePush}, use the setter functions to fill it with data, and then use
* {@link #sendInBackground()} to send it.
*/
public class ParsePush {
/* package for test */ static String KEY_DATA_MESSAGE = "alert";
/* package for test */ static ParsePushController getPushController() {
return ParseCorePlugins.getInstance().getPushController();
}
/* package for test */ static ParsePushChannelsController getPushChannelsController() {
return ParseCorePlugins.getInstance().getPushChannelsController();
}
private static ParseObjectSubclassingController getSubclassingController() {
return ParseCorePlugins.getInstance().getSubclassingController();
}
private static void checkArgument(boolean expression, Object errorMessage) {
if (!expression) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
}
}
/* package */ static class State {
/* package */ static class Builder {
private Set<String> channelSet;
private ParseQuery<ParseInstallation> query;
private Long expirationTime;
private Long expirationTimeInterval;
private Long pushTime;
private Boolean pushToIOS;
private Boolean pushToAndroid;
private JSONObject data;
public Builder() {
// do nothing
}
public Builder(State state) {
this.channelSet = state.channelSet() == null
? null
: Collections.unmodifiableSet(new HashSet<>(state.channelSet()));
this.query = state.queryState() == null
? null
: new ParseQuery<>(new ParseQuery.State.Builder<ParseInstallation>(state.queryState()));
this.expirationTime = state.expirationTime();
this.expirationTimeInterval = state.expirationTimeInterval();
this.pushTime = state.pushTime();
this.pushToIOS = state.pushToIOS();
this.pushToAndroid = state.pushToAndroid();
// Since in state.build() we check data is not null, we do not need to check it again here.
JSONObject copyData = null;
try {
copyData = new JSONObject(state.data().toString());
} catch (JSONException e) {
// Swallow this silently since it is impossible to happen
}
this.data = copyData;
}
public Builder expirationTime(Long expirationTime) {
this.expirationTime = expirationTime;
expirationTimeInterval = null;
return this;
}
public Builder expirationTimeInterval(Long expirationTimeInterval) {
this.expirationTimeInterval = expirationTimeInterval;
expirationTime = null;
return this;
}
public Builder pushTime(Long pushTime) {
if (pushTime != null) {
long now = System.currentTimeMillis() / 1000;
long twoWeeks = 60*60*24*7*2;
checkArgument(pushTime > now, "Scheduled push time can not be in the past");
checkArgument(pushTime < now + twoWeeks, "Scheduled push time can not be more than " +
"two weeks in the future");
}
this.pushTime = pushTime;
return this;
}
public Builder pushToIOS(Boolean pushToIOS) {
checkArgument(query == null, "Cannot set push targets (i.e. setPushToAndroid or " +
"setPushToIOS) when pushing to a query");
this.pushToIOS = pushToIOS;
return this;
}
public Builder pushToAndroid(Boolean pushToAndroid) {
checkArgument(query == null, "Cannot set push targets (i.e. setPushToAndroid or " +
"setPushToIOS) when pushing to a query");
this.pushToAndroid = pushToAndroid;
return this;
}
public Builder data(JSONObject data) {
this.data = data;
return this;
}
public Builder channelSet(Collection<String> channelSet) {
checkArgument(channelSet != null, "channels collection cannot be null");
for (String channel : channelSet) {
checkArgument(channel != null, "channel cannot be null");
}
this.channelSet = new HashSet<>(channelSet);
query = null;
return this;
}
public Builder query(ParseQuery<ParseInstallation> query) {
checkArgument(query != null, "Cannot target a null query");
checkArgument(pushToIOS == null && pushToAndroid == null, "Cannot set push targets " +
"(i.e. setPushToAndroid or setPushToIOS) when pushing to a query");
checkArgument(
query.getClassName().equals(
getSubclassingController().getClassName(ParseInstallation.class)),
"Can only push to a query for Installations");
channelSet = null;
this.query = query;
return this;
}
public State build() {
if (data == null) {
throw new IllegalArgumentException(
"Cannot send a push without calling either setMessage or setData");
}
return new State(this);
}
}
private final Set<String> channelSet;
private final ParseQuery.State<ParseInstallation> queryState;
private final Long expirationTime;
private final Long expirationTimeInterval;
private final Long pushTime;
private final Boolean pushToIOS;
private final Boolean pushToAndroid;
private final JSONObject data;
private State(Builder builder) {
this.channelSet = builder.channelSet == null ?
null : Collections.unmodifiableSet(new HashSet<>(builder.channelSet));
this.queryState = builder.query == null ? null : builder.query.getBuilder().build();
this.expirationTime = builder.expirationTime;
this.expirationTimeInterval = builder.expirationTimeInterval;
this.pushTime = builder.pushTime;
this.pushToIOS = builder.pushToIOS;
this.pushToAndroid = builder.pushToAndroid;
// Since in builder.build() we check data is not null, we do not need to check it again here.
JSONObject copyData = null;
try {
copyData = new JSONObject(builder.data.toString());
} catch (JSONException e) {
// Swallow this silently since it is impossible to happen
}
this.data = copyData;
}
public Set<String> channelSet() {
return channelSet;
}
public ParseQuery.State<ParseInstallation> queryState() {
return queryState;
}
public Long expirationTime() {
return expirationTime;
}
public Long expirationTimeInterval() {
return expirationTimeInterval;
}
public Long pushTime() {
return pushTime;
}
public Boolean pushToIOS() {
return pushToIOS;
}
public Boolean pushToAndroid() {
return pushToAndroid;
}
public JSONObject data() {
// Since in builder.build() we check data is not null, we do not need to check it again here.
JSONObject copyData = null;
try {
copyData = new JSONObject(data.toString());
} catch (JSONException e) {
// Swallow this exception silently since it is impossible to happen
}
return copyData;
}
}
private static final String TAG = "com.parse.ParsePush";
/* package for test */ final State.Builder builder;
/**
* Creates a new push notification.
*
* The default channel is the empty string, also known as the global broadcast channel, but this
* value can be overridden using {@link #setChannel(String)}, {@link #setChannels(Collection)} or
* {@link #setQuery(ParseQuery)}. Before sending the push notification you must call either
* {@link #setMessage(String)} or {@link #setData(JSONObject)}.
*/
public ParsePush() {
this(new State.Builder());
}
/**
* Creates a copy of {@code push}.
*
* @param push
* The push to copy.
*/
public ParsePush(ParsePush push) {
this(new State.Builder(push.builder.build()));
}
private ParsePush(State.Builder builder) {
this.builder = builder;
}
/**
* Adds 'channel' to the 'channels' list in the current {@link ParseInstallation} and saves it in
* a background thread.
*
* @param channel
* The channel to subscribe to.
* @return A Task that is resolved when the the subscription is complete.
*/
public static Task<Void> subscribeInBackground(String channel) {
return getPushChannelsController().subscribeInBackground(channel);
}
/**
* Adds 'channel' to the 'channels' list in the current {@link ParseInstallation} and saves it in
* a background thread.
*
* @param channel
* The channel to subscribe to.
* @param callback
* The SaveCallback that is called after the Installation is saved.
*/
public static void subscribeInBackground(String channel, SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(subscribeInBackground(channel), callback);
}
/**
* Removes 'channel' from the 'channels' list in the current {@link ParseInstallation} and saves
* it in a background thread.
*
* @param channel
* The channel to unsubscribe from.
* @return A Task that is resolved when the the unsubscription is complete.
*/
public static Task<Void> unsubscribeInBackground(String channel) {
return getPushChannelsController().unsubscribeInBackground(channel);
}
/**
* Removes 'channel' from the 'channels' list in the current {@link ParseInstallation} and saves
* it in a background thread.
*
* @param channel
* The channel to unsubscribe from.
* @param callback
* The SaveCallback that is called after the Installation is saved.
*/
public static void unsubscribeInBackground(String channel, SaveCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(unsubscribeInBackground(channel), callback);
}
/**
* A helper method to concisely send a push message to a query. This method is equivalent to
* ParsePush push = new ParsePush(); push.setMessage(message); push.setQuery(query);
* push.sendInBackground();
*
* @param message
* The message that will be shown in the notification.
* @param query
* A ParseInstallation query which specifies the recipients of a push.
* @return A task that is resolved when the message is sent.
*/
public static Task<Void> sendMessageInBackground(String message,
ParseQuery<ParseInstallation> query) {
ParsePush push = new ParsePush();
push.setQuery(query);
push.setMessage(message);
return push.sendInBackground();
}
/**
* A helper method to concisely send a push message to a query. This method is equivalent to
* ParsePush push = new ParsePush(); push.setMessage(message); push.setQuery(query);
* push.sendInBackground(callback);
*
* @param message
* The message that will be shown in the notification.
* @param query
* A ParseInstallation query which specifies the recipients of a push.
* @param callback
* callback.done(e) is called when the send completes.
*/
public static void sendMessageInBackground(String message, ParseQuery<ParseInstallation> query,
SendCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(sendMessageInBackground(message, query), callback);
}
/**
* A helper method to concisely send a push to a query. This method is equivalent to ParsePush
* push = new ParsePush(); push.setData(data); push.setQuery(query); push.sendInBackground();
*
* @param data
* The entire data of the push message. See the push guide for more details on the data
* format.
* @param query
* A ParseInstallation query which specifies the recipients of a push.
* @return A task that is resolved when the data is sent.
*/
public static Task<Void> sendDataInBackground(JSONObject data,
ParseQuery<ParseInstallation> query) {
ParsePush push = new ParsePush();
push.setQuery(query);
push.setData(data);
return push.sendInBackground();
}
/**
* A helper method to concisely send a push to a query. This method is equivalent to ParsePush
* push = new ParsePush(); push.setData(data); push.setQuery(query);
* push.sendInBackground(callback);
*
* @param data
* The entire data of the push message. See the push guide for more details on the data
* format.
* @param query
* A ParseInstallation query which specifies the recipients of a push.
* @param callback
* callback.done(e) is called when the send completes.
*/
public static void sendDataInBackground(JSONObject data, ParseQuery<ParseInstallation> query,
SendCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(sendDataInBackground(data, query), callback);
}
/**
* Sets the channel on which this push notification will be sent. The channel name must start with
* a letter and contain only letters, numbers, dashes, and underscores. A push can either have
* channels or a query. Setting this will unset the query.
*/
public void setChannel(String channel) {
builder.channelSet(Collections.singletonList(channel));
}
/**
* Sets the collection of channels on which this push notification will be sent. Each channel name
* must start with a letter and contain only letters, numbers, dashes, and underscores. A push can
* either have channels or a query. Setting this will unset the query.
*/
public void setChannels(Collection<String> channels) {
builder.channelSet(channels);
}
/**
* Sets the query for this push for which this push notification will be sent. This query will be
* executed in the Parse cloud; this push notification will be sent to Installations which this
* query yields. A push can either have channels or a query. Setting this will unset the channels.
*
* @param query
* A query to which this push should target. This must be a ParseInstallation query.
*/
public void setQuery(ParseQuery<ParseInstallation> query) {
builder.query(query);
}
/**
* Sets a UNIX epoch timestamp at which this notification should expire, in seconds (UTC). This
* notification will be sent to devices which are either online at the time the notification is
* sent, or which come online before the expiration time is reached. Because device clocks are not
* guaranteed to be accurate, most applications should instead use
* {@link #setExpirationTimeInterval(long)}.
*/
public void setExpirationTime(long time) {
builder.expirationTime(time);
}
/**
* Sets the time interval after which this notification should expire, in seconds. This
* notification will be sent to devices which are either online at the time the notification is
* sent, or which come online within the given number of seconds of the notification being
* received by Parse's server. An interval which is less than or equal to zero indicates that the
* message should only be sent to devices which are currently online.
*/
public void setExpirationTimeInterval(long timeInterval) {
builder.expirationTimeInterval(timeInterval);
}
/**
* Clears both expiration values, indicating that the notification should never expire.
*/
public void clearExpiration() {
builder.expirationTime(null);
builder.expirationTimeInterval(null);
}
/**
* Sets a UNIX epoch timestamp at which this notification should be delivered, in seconds (UTC).
* Scheduled time can not be in the past and must be at most two weeks in the future.
*/
public void setPushTime(long time) {
builder.pushTime(time);
}
/**
* Set whether this push notification will go to iOS devices.
* <p/>
* Setting this to {@code true} will set {@link #setPushToAndroid(boolean)} to {@code false}.
* <p/>
* <strong>Note:</strong> You must set up iOS push certificates before sending pushes to iOS.
*
* @deprecated Please use {@link #setQuery(ParseQuery)} with a {@link ParseQuery} targeting
* {@link ParseInstallation}s with a constraint on the {@code deviceType} field. If you use
* {@code #setPushToIOS(boolean)} or {@link #setPushToAndroid(boolean)}, then you will only be
* able to send to one of these two device types (e.g. and not Windows).
*/
@Deprecated
public void setPushToIOS(boolean pushToIOS) {
builder.pushToIOS(pushToIOS);
}
/**
* Set whether this push notification will go to Android devices.
* <p/>
* Setting this to {@code true} will set {@link #setPushToIOS(boolean)} to {@code false}.
*
* @deprecated Please use {@link #setQuery(ParseQuery)} with a {@link ParseQuery} targeting
* {@link ParseInstallation}s with a constraint on the {@code deviceType} field. If you use
* {@code #setPushToAndroid(boolean)} or {@link #setPushToIOS(boolean)}, then you will only be
* able to send to one of these two device types (e.g. and not Windows).
*/
@Deprecated
public void setPushToAndroid(boolean pushToAndroid) {
builder.pushToAndroid(pushToAndroid);
}
/**
* Sets the entire data of the push message. See the push guide for more details on the data
* format. This will overwrite any data specified in {@link #setMessage(String)}.
*/
public void setData(JSONObject data) {
builder.data(data);
}
/**
* Sets the message that will be shown in the notification. This will overwrite any data specified
* in {@link #setData(JSONObject)}.
*/
public void setMessage(String message) {
JSONObject data = new JSONObject();
try {
data.put(KEY_DATA_MESSAGE, message);
} catch (JSONException e) {
PLog.e(TAG, "JSONException in setMessage", e);
}
setData(data);
}
/**
* Sends this push notification in a background thread. Use this when you do not have code to run
* on completion of the push.
*
* @return A Task is resolved when the push has been sent.
*/
public Task<Void> sendInBackground() {
// Since getCurrentSessionTokenAsync takes time, we build the state before it.
final State state = builder.build();
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
return getPushController().sendInBackground(state, sessionToken);
}
});
}
/**
* Sends this push notification while blocking this thread until the push notification has
* successfully reached the Parse servers. Typically, you should use {@link #sendInBackground()}
* instead of this, unless you are managing your own threading.
*
* @throws ParseException
* Throws an exception if the server is inaccessible.
*/
public void send() throws ParseException {
ParseTaskUtils.wait(sendInBackground());
}
/**
* Sends this push notification in a background thread. This is preferable to using
* <code>send()</code>, unless your code is already running from a background thread.
*
* @param callback
* callback.done(e) is called when the send completes.
*/
public void sendInBackground(SendCallback callback) {
ParseTaskUtils.callbackOnMainThreadAsync(sendInBackground(), callback);
}
}