Skip to content

Commit

Permalink
Add APL Viewport Mapper and Predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
breedloj committed Oct 30, 2018
1 parent 190c13f commit 3cb283f
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 0 deletions.
6 changes: 6 additions & 0 deletions ask-sdk-core/src/com/amazon/ask/request/Predicates.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.amazon.ask.model.IntentRequest;
import com.amazon.ask.model.Request;
import com.amazon.ask.model.interfaces.display.ElementSelectedRequest;
import com.amazon.ask.request.viewport.ViewportUtils;
import com.amazon.ask.request.viewport.ViewportProfile;

import java.util.function.Predicate;

Expand Down Expand Up @@ -115,4 +117,8 @@ public static Predicate<HandlerInput> persistentAttribute(String key, Object val
&& value.equals(i.getAttributesManager().getPersistentAttributes().get(key));
}

public static Predicate<HandlerInput> viewportProfile(ViewportProfile viewportProfile) {
return i -> viewportProfile.equals(ViewportUtils.getViewportProfile(i.getRequestEnvelope()));
}

}
23 changes: 23 additions & 0 deletions ask-sdk-core/src/com/amazon/ask/request/viewport/Density.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file 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.amazon.ask.request.viewport;

public enum Density {
XLOW,
LOW,
MEDIUM,
HIGH,
XHIGH,
XXHIGH;
}
21 changes: 21 additions & 0 deletions ask-sdk-core/src/com/amazon/ask/request/viewport/Orientation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file 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.amazon.ask.request.viewport;

public enum Orientation {
LANDSCAPE,
EQUAL,
PORTRAIT;

}
23 changes: 23 additions & 0 deletions ask-sdk-core/src/com/amazon/ask/request/viewport/Size.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file 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.amazon.ask.request.viewport;

public enum Size {
XSMALL,
SMALL,
MEDIUM,
LARGE,
XLARGE;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file 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.amazon.ask.request.viewport;

public enum ViewportProfile {
HUB_ROUND_SMALL,
HUB_LANDSCAPE_MEDIUM,
HUB_LANDSCAPE_LARGE,
MOBILE_LANDSCAPE_SMALL,
MOBILE_PORTRAIT_SMALL,
MOBILE_LANDSCAPE_MEDIUM,
MOBILE_PORTRAIT_MEDIUM,
TV_LANDSCAPE_XLARGE,
TV_PORTRAIT_MEDIUM,
TV_LANDSCAPE_MEDIUM,
UNKNOWN_VIEWPORT_PROFILE;
}
148 changes: 148 additions & 0 deletions ask-sdk-core/src/com/amazon/ask/request/viewport/ViewportUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file 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.amazon.ask.request.viewport;

import com.amazon.ask.exception.AskSdkException;
import com.amazon.ask.model.RequestEnvelope;
import com.amazon.ask.model.interfaces.viewport.Shape;
import com.amazon.ask.model.interfaces.viewport.ViewportState;

public class ViewportUtils {

public static ViewportProfile getViewportProfile(RequestEnvelope requestEnvelope) {
ViewportState viewportState = requestEnvelope.getContext().getViewport();
Shape shape = viewportState.getShape();
int currentPixelWidth = viewportState.getCurrentPixelWidth().intValueExact();
int currentPixelHeight = viewportState.getCurrentPixelHeight().intValueExact();
int dpi = viewportState.getDpi().intValueExact();
Orientation orientation = getOrientation(currentPixelHeight, currentPixelWidth);

if (shape == Shape.ROUND &&
orientation == Orientation.EQUAL &&
getSize(currentPixelHeight) == Size.XSMALL &&
getSize(currentPixelWidth) == Size.XSMALL &&
getDensity(dpi) == Density.LOW) {
return ViewportProfile.HUB_ROUND_SMALL;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth).ordinal() <= Size.MEDIUM.ordinal() &&
getSize(currentPixelHeight).ordinal() <= Size.SMALL.ordinal() &&
getDensity(dpi) == Density.LOW) {
return ViewportProfile.HUB_LANDSCAPE_MEDIUM;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth).ordinal() >= Size.LARGE.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.SMALL.ordinal() &&
getDensity(dpi) == Density.LOW) {
return ViewportProfile.HUB_LANDSCAPE_LARGE;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth).ordinal() >= Size.MEDIUM.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.SMALL.ordinal() &&
getDensity(dpi) == Density.MEDIUM) {
return ViewportProfile.MOBILE_LANDSCAPE_MEDIUM;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.PORTRAIT &&
getSize(currentPixelWidth).ordinal() >= Size.SMALL.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.MEDIUM.ordinal() &&
getDensity(dpi) == Density.MEDIUM) {
return ViewportProfile.MOBILE_PORTRAIT_MEDIUM;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth).ordinal() >= Size.SMALL.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.XSMALL.ordinal() &&
getDensity(dpi) == Density.MEDIUM) {
return ViewportProfile.MOBILE_LANDSCAPE_SMALL;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.PORTRAIT &&
getSize(currentPixelWidth).ordinal() >= Size.XSMALL.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.SMALL.ordinal() &&
getDensity(dpi) == Density.MEDIUM) {
return ViewportProfile.MOBILE_PORTRAIT_SMALL;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth).ordinal() >= Size.XLARGE.ordinal() &&
getSize(currentPixelHeight).ordinal() >= Size.MEDIUM.ordinal() &&
getDensity(dpi).ordinal() >= Density.HIGH.ordinal()) {
return ViewportProfile.TV_LANDSCAPE_XLARGE;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.PORTRAIT &&
getSize(currentPixelWidth) == Size.XSMALL &&
getSize(currentPixelHeight) == Size.XLARGE &&
getDensity(dpi).ordinal() >= Density.HIGH.ordinal()) {
return ViewportProfile.TV_PORTRAIT_MEDIUM;

} else if (shape == Shape.RECTANGLE &&
orientation == Orientation.LANDSCAPE &&
getSize(currentPixelWidth) == Size.MEDIUM &&
getSize(currentPixelHeight) == Size.SMALL &&
getDensity(dpi).ordinal() >= Density.HIGH.ordinal()) {
return ViewportProfile.TV_LANDSCAPE_MEDIUM;
}
return ViewportProfile.UNKNOWN_VIEWPORT_PROFILE;
}

private static Density getDensity(int dpi) {
if (isBetween(dpi, 0, 121)) {
return Density.XLOW;
} else if (isBetween(dpi, 121, 161)) {
return Density.LOW;
} else if (isBetween(dpi, 161, 241)) {
return Density.MEDIUM;
} else if (isBetween(dpi, 241, 321)) {
return Density.HIGH;
} else if (isBetween(dpi, 321, 481)) {
return Density.XHIGH;
} else if (dpi >= 481) {
return Density.XXHIGH;
}
throw new AskSdkException("Unknown density dpi value " + dpi);
}

private static Orientation getOrientation(int height, int width) {
if (height > width) {
return Orientation.PORTRAIT;
} else if (height < width) {
return Orientation.LANDSCAPE;
}
return Orientation.EQUAL;
}

private static Size getSize(int dpi) {
if (isBetween(dpi, 0, 600)) {
return Size.XSMALL;
} else if (isBetween(dpi, 600, 960)) {
return Size.SMALL;
} else if (isBetween(dpi, 960, 1280)) {
return Size.MEDIUM;
} else if (isBetween(dpi, 1280, 1920)) {
return Size.LARGE;
} else if (dpi >= 1920) {
return Size.XLARGE;
}
throw new AskSdkException("Unknown size group value " + dpi);
}

private static boolean isBetween(int x, int lower, int upper) {
return lower <= x && x < upper;
}
}
31 changes: 31 additions & 0 deletions ask-sdk-core/tst/com/amazon/ask/request/PredicatesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import com.amazon.ask.attributes.AttributesManager;
import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.model.Context;
import com.amazon.ask.model.Intent;
import com.amazon.ask.model.IntentRequest;
import com.amazon.ask.model.LaunchRequest;
Expand All @@ -24,8 +25,12 @@
import com.amazon.ask.model.SessionEndedRequest;
import com.amazon.ask.model.Slot;
import com.amazon.ask.model.interfaces.display.ElementSelectedRequest;
import com.amazon.ask.model.interfaces.viewport.Shape;
import com.amazon.ask.model.interfaces.viewport.ViewportState;
import com.amazon.ask.request.viewport.ViewportProfile;
import org.junit.Test;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.Map;

Expand Down Expand Up @@ -179,6 +184,18 @@ public void slot_value_predicate_matching_slot_value_returns_true() {
assertTrue(slotValue("foo", "bar").test(input));
}

@Test
public void viewport_profile_predicate_matching_viewport_profile_returns_true() {
HandlerInput input = getInputWithViewport();
assertTrue(viewportProfile(ViewportProfile.HUB_ROUND_SMALL).test(input));
}

@Test
public void viewport_profile_predicate_non_matching_viewport_profile_returns_false() {
HandlerInput input = getInputWithViewport();
assertFalse(viewportProfile(ViewportProfile.MOBILE_LANDSCAPE_MEDIUM).test(input));
}

private HandlerInput getInputWithSessionAttributes(Map<String, Object> attributes, boolean inSessionRequest) {
AttributesManager manager = mock(AttributesManager.class);
when(manager.getSessionAttributes()).thenReturn(attributes);
Expand Down Expand Up @@ -215,4 +232,18 @@ private HandlerInput getInputForRequest(Request request) {
return input;
}

private HandlerInput getInputWithViewport() {
ViewportState viewportState = ViewportState.builder()
.withShape(Shape.ROUND)
.withDpi(BigDecimal.valueOf(160))
.withCurrentPixelHeight(BigDecimal.valueOf(300))
.withCurrentPixelWidth(BigDecimal.valueOf(300))
.build();

HandlerInput input = mock(HandlerInput.class);
RequestEnvelope envelope = RequestEnvelope.builder().withContext(Context.builder().withViewport(viewportState).build()).build();
when(input.getRequestEnvelope()).thenReturn(envelope);
return input;
}

}
Loading

11 comments on commit 3cb283f

@javier-ochoa
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@breedloj Hi there. I see the changes this new commit introduces to the SDK. Is there some more changes to come? Could you explain me how to test all those new components that the link to APL post on Alexa blog talks about? Thanks!

@Chris-Liao
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @javier-ochoa, thanks for keeping an eye on SDK update.
Basically your skill will receive APL related information from Input - RequestEnvelope - Context - ViewPort, including dpi, pixel height, pixel width, shape and etc.
You can leverage ViewPortUtils and Predicate to find out what kind of ViewportProfile it is, for example HUB_LANDSCAPE_MEDIUM which matches Medium Hub in developer console, and what RequestHandler should handle this request. Then you can build up APL Command, wire them up into Directive and send back response to device.
Enjoy!

@sungolivia
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javier-ochoa not sure if this helps but there are some samples in Node.js if you would like to test it quickly!

@javier-ochoa
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chris-Liao @sungolivia Thank you guys for your help.

Perhaps you guys can provide some further help.
I went to the APL authoring tool and copied one example directly into my project.
Finally I just loaded that json into a RenderDocumentDirective and push it to the response.

 RenderDocumentDirective directive = RenderDocumentDirective.builder()
          .withDocument(jsonObject.map()).build();

      return input.getResponseBuilder()
          .withSpeech("Some speech here")
          .addDirective(directive)
          .build();

I am testing it with the Alexa Developer Console but i dont seem to get anything out regardless of the HUB i use or even on the Extra Large TV mode. Is the ADC already working with APL directives?

This is the json log I get from the Developer Console and i can see that the directive is there:

{
	"body": {
		"version": "1.0",
		"response": {
			"outputSpeech": {
				"type": "SSML",
				"ssml": "<speak>Some speech here</speak>"
			},
			"directives": [
				{
					"type": "Alexa.Presentation.APL.RenderDocument",
					"document": {
						"listTemplate2Metadata": {
							"type": "object",
							"objectId": "lt1Metadata",
							"backgroundImage": {
								"sources": [
									{
										"url": "https://d2o906d8ln7ui1.cloudfront.net/images/LT2_Background.png",
										"size": "small",
										"widthPixels": 0,
										"heightPixels": 0
									},
									{
										"url": "https://d2o906d8ln7ui1.cloudfront.net/images/LT2_Background.png",
										"size": "large",
										"widthPixels": 0,
										"heightPixels": 0
									}
								]
							},
							"title": "Results for \"Cow's Milk Cheese\"",
							"logoUrl": "https://d2o906d8ln7ui1.cloudfront.net/images/cheeseskillicon.png"
						},
						"listTemplate2ListData": {
							"type": "list",
							"listId": "lt2Sample",
							"totalNumberOfItems": 10,
							"hintText": "Try, \"Alexa, select number 1\"",
							"listPage": {
								"listItems": [
									{
										"listItemIdentifier": "brie",
										"ordinalNumber": 1,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Brie"
											},
											"secondaryText": {
												"type": "PlainText",
												"text": "Origin: France"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_brie.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_brie.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "brie"
									},
									{
										"listItemIdentifier": "gruyere",
										"ordinalNumber": 2,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Gruyere"
											},
											"secondaryText": {
												"type": "RichText",
												"text": "Origin: Switzerland"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_gruyere.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_gruyere.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "gruyere"
									},
									{
										"listItemIdentifier": "gorgonzola",
										"ordinalNumber": 3,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Gorgonzola"
											},
											"secondaryText": {
												"type": "RichText",
												"text": "Origin: Italy"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_gorgonzola.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/md_gorgonzola.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "gorgonzola"
									},
									{
										"listItemIdentifier": "brie",
										"ordinalNumber": 1,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Brie"
											},
											"secondaryText": {
												"type": "PlainText",
												"text": "Origin: France"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_brie.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_brie.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "tl_brie"
									},
									{
										"listItemIdentifier": "gruyere",
										"ordinalNumber": 2,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Gruyere"
											},
											"secondaryText": {
												"type": "RichText",
												"text": "Origin: Switzerland"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_gruyere.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_gruyere.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "tl_gruyere"
									},
									{
										"listItemIdentifier": "gorgonzola",
										"ordinalNumber": 3,
										"textContent": {
											"primaryText": {
												"type": "PlainText",
												"text": "Gorgonzola"
											},
											"secondaryText": {
												"type": "RichText",
												"text": "Origin: Italy"
											}
										},
										"image": {
											"sources": [
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_gorgonzola.png",
													"size": "small",
													"widthPixels": 0,
													"heightPixels": 0
												},
												{
													"url": "https://d2o906d8ln7ui1.cloudfront.net/images/tl_gorgonzola.png",
													"size": "large",
													"widthPixels": 0,
													"heightPixels": 0
												}
											]
										},
										"token": "tl_gorgonzola"
									}
								]
							}
						}
					}
				}
			]
		},
		"userAgent": "ask-java/2.8.0 Java/1.8.0_181"
	}
}

Thank you!

@nikhilym
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @javier-ochoa , lets move this discussion to the Issues board so that it is helpful for other community members as well. Do you mind logging an issue with this content, so that we can investigate further? Thanks!!

@javier-ochoa
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikhilym

issue opened => #153

Thank you.

@LorenzNickel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone provide a working example with this how to build a response with a picture?

@Chris-Liao
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @BlockLabTV, would you also open an issue so we can keep track and help others?

@LorenzNickel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is worth an issue since you probably won't add a sample just for me, just thought maybe someone already has published one somewhere and could share it with me.

@nikhilym
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @BlockLabTV , posting these requests/discussions on the 'issues' dashboard will make easier for the community to search through and respond faster, than on the release commits.

That being said, we currently do not have an APL sample to show case, but putting it on the issues/ user voice as feature request would drive either the community or the SDK developers to work on it, depending on the priorities.

@LorenzNickel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> #154

Please sign in to comment.