Repository: relayrides/pushy
Pull request #148 (2015-02-04 12:50:13)

Adding support for title and its localized version - new in iOS 8.2
The new `title`, `title-loc-key` and `title-loc-args` are described in here:
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html

Especially interesting for Apple Watch:

* _"On Apple Watch, the title key alerts the user to the new request."_
* _"Apple Watch displays this string as part of the notification interface. This string is displayed only briefly and should be crafted so that it can be understood quickly. This key was added in iOS 8.2."_

jchambers: @matzew Looks good to me, except for that one minor documentation comment. Out of curiosity, how is it that you notice these changes? I'm subscribed to everything I can find from Apple, but stuff like this still catches me by surprise.

Also, it's bizarre to me that they updated the docs for this new stuff, but *still* haven't updated the docs to reflect the changes from WWDC 2014.

Patch:
diff --git a/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java b/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
index 14c53802..0a1ea3dc 100644
--- a/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
+++ b/src/main/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilder.java
@@ -42,6 +42,9 @@ public class ApnsPayloadBuilder {
 	private String alertBody = null;
 	private String localizedAlertKey = null;
 	private String[] localizedAlertArguments = null;
+	private String alertTitle = null;
+	private String localizedAlertTitleKey = null;
+	private String[] localizedAlertTitleArguments = null;
 	private String launchImageFileName = null;
 	private boolean showActionButton = true;
 	private String localizedActionButtonKey = null;
@@ -57,7 +60,10 @@ public class ApnsPayloadBuilder {
 	private static final String CATEGORY_KEY = "category";
 	private static final String CONTENT_AVAILABLE_KEY = "content-available";
 
+	private static final String ALERT_TITLE_KEY = "title";
 	private static final String ALERT_BODY_KEY = "body";
+	private static final String ALERT_TITLE_LOC_KEY = "title-loc-key";
+	private static final String ALERT_TITLE_ARGS_KEY = "title-loc-args";
 	private static final String ACTION_LOC_KEY = "action-loc-key";
 	private static final String ALERT_LOC_KEY = "loc-key";
 	private static final String ALERT_ARGS_KEY = "loc-args";
@@ -127,6 +133,50 @@ public class ApnsPayloadBuilder {
 		return this;
 	}
 
+	/**
+	 * <p>Sets a short description of the notification purpose. The Apple Watch will display this as part of the notification.
+	 * According to Apple's documentation, this should be:</p>
+	 *
+	 *  <blockquote>A short string describing the purpose of the notification. Apple Watch displays this string as part of the notification interface.
+	 *  This string is displayed only briefly and should be crafted so that it can be understood quickly.</blockquote>
+	 *
+	 * @param alertTitle the description to be shown for this push notification
+	 *
+	 * @see ApnsPayloadBuilder#setLocalizedAlertTitle(String, String[])
+	 */
+	public ApnsPayloadBuilder setAlertTitle(final String alertTitle) {
+		if (alertTitle != null && this.localizedAlertTitleKey != null) {
+			throw new IllegalStateException("Cannot set a literal alert title when a localized alert title key has already been set.");
+		}
+
+		this.alertTitle = alertTitle;
+
+		return this;
+	}
+
+	/**
+	 * <p>Sets the key of the title string in the receiving app's localized string list to be shown for the push notification.
+	 * The message in the app's string list may optionally have placeholders, which will be populated by values from the
+	 * given {@code alertArguments}.</p>
+	 *
+	 * @param localizedAlertTitleKey a key to a string in the receiving app's localized string list
+	 * @param alertTitleArguments arguments to populate placeholders in the localized alert string; may be {@code null}
+	 */
+	public ApnsPayloadBuilder setLocalizedAlertTitle(final String localizedAlertTitleKey, final String[] alertTitleArguments) {
+		if (localizedAlertTitleKey != null && this.alertTitle != null) {
+			throw new IllegalStateException("Cannot set a localized alert key when a literal alert body has already been set.");
+		}
+
+		if (localizedAlertTitleKey == null && alertTitleArguments != null) {
+			throw new IllegalArgumentException("Cannot set localized alert arguments without a localized alert message key.");
+		}
+
+		this.localizedAlertTitleKey = localizedAlertTitleKey;
+		this.localizedAlertTitleArguments = alertTitleArguments;
+
+		return this;
+	}
+
 	/**
 	 * <p>Sets the image to be shown when the receiving app launches in response to this push notification. According
 	 * to Apple's documentation, this should be:</p>
@@ -390,6 +440,10 @@ public class ApnsPayloadBuilder {
 					alert.put(ALERT_BODY_KEY, this.alertBody);
 				}
 
+				if (this.alertTitle != null) {
+					alert.put(ALERT_TITLE_KEY, this.alertTitle);
+				}
+
 				if (this.showActionButton) {
 					if (this.localizedActionButtonKey != null) {
 						alert.put(ACTION_LOC_KEY, this.localizedActionButtonKey);
@@ -413,6 +467,20 @@ public class ApnsPayloadBuilder {
 					}
 				}
 
+				if (this.localizedAlertTitleKey != null) {
+					alert.put(ALERT_TITLE_LOC_KEY, this.localizedAlertTitleKey);
+
+					if (this.localizedAlertTitleArguments != null) {
+						final JSONArray alertTitleArgs = new JSONArray();
+
+						for (final String arg : this.localizedAlertTitleArguments) {
+							alertTitleArgs.add(arg);
+						}
+
+						alert.put(ALERT_TITLE_ARGS_KEY, alertTitleArgs);
+					}
+				}
+
 				if (this.launchImageFileName != null) {
 					alert.put(LAUNCH_IMAGE_KEY, this.launchImageFileName);
 				}
@@ -431,6 +499,8 @@ public class ApnsPayloadBuilder {
 	 */
 	private boolean hasAlertContent() {
 		return	this.alertBody != null ||
+				this.alertTitle != null ||
+				this.localizedAlertTitleKey != null ||
 				this.localizedAlertKey != null ||
 				this.localizedActionButtonKey != null ||
 				this.launchImageFileName != null ||


Test patch:
diff --git a/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java b/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
index a6323095..3ffc4e5b 100644
--- a/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
+++ b/src/test/java/com/relayrides/pushy/apns/util/ApnsPayloadBuilderTest.java
@@ -68,6 +68,20 @@ public class ApnsPayloadBuilderTest {
 		}
 	}
 
+	@Test
+	public void testSetAlertTitle() throws ParseException {
+		final String alertTitle = "This is a test alert message.";
+
+		this.builder.setAlertTitle(alertTitle);
+
+		{
+			final JSONObject aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+			final JSONObject alert = (JSONObject) aps.get("alert");
+
+			assertEquals(alertTitle, alert.get("title"));
+		}
+	}
+
 	@SuppressWarnings("unchecked")
 	@Test
 	public void testSetLocalizedAlertMessage() throws ParseException {
@@ -109,6 +123,18 @@ public class ApnsPayloadBuilderTest {
 		this.builder.setLocalizedAlertMessage("Test", null);
 	}
 
+	@Test(expected = IllegalStateException.class)
+	public void testSetAlertTitleWithExistingLocalizedAlertTitle() {
+		this.builder.setLocalizedAlertTitle("Test", null);
+		this.builder.setAlertTitle("Test");
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testSetLocalizedAlertTitleWithExistingAlertTitle() {
+		this.builder.setAlertTitle("Test");
+		this.builder.setLocalizedAlertTitle("Test", null);
+	}
+
 	@Test
 	public void testSetLaunchImage() throws ParseException {
 		final String launchImageFilename = "launch.png";