Skip to content

Commit

Permalink
feat: add support for Alexa Presentation Language (APL)
Browse files Browse the repository at this point in the history
The Alexa Presentation Language (APL) enables you to build interactive voice experiences that include graphics, images, slideshows, and video, and to customize them for different device types.
  • Loading branch information
Zhang committed Oct 30, 2018
1 parent ad2fcec commit bcdfec8
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 0 deletions.
12 changes: 12 additions & 0 deletions ask-sdk-core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export { CustomSkillBuilder } from './skill/factory/CustomSkillBuilder';
export { CustomSkillFactory } from './skill/factory/CustomSkillFactory';
export { SkillBuilders } from './skill/SkillBuilders';
export { CustomSkillConfiguration as SkillConfiguration } from './skill/CustomSkillConfiguration';
export {
getViewportDpiGroup,
getViewportOrientation,
getViewportProfile,
getViewportSizeGroup,
ViewportDpiGroup,
ViewportDpiGroupOrder,
ViewportOrientation,
ViewportProfile,
ViewportSizeGroup,
ViewportSizeGroupOrder,
} from './util/ViewportUtils';
export {
createAskSdkError,
createAskSdkUserAgent,
Expand Down
238 changes: 238 additions & 0 deletions ask-sdk-core/lib/util/ViewportUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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://www.apache.org/licenses/LICENSE-2.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.
*/

import {
interfaces,
RequestEnvelope,
} from 'ask-sdk-model';
import { createAskSdkError } from 'ask-sdk-runtime';
import Shape = interfaces.viewport.Shape;

export type 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';

export type ViewportOrientation =
'EQUAL' |
'LANDSCAPE' |
'PORTRAIT';

export type ViewportSizeGroup =
'XSMALL' |
'SMALL' |
'MEDIUM' |
'LARGE' |
'XLARGE';

export type ViewportDpiGroup =
'XLOW' |
'LOW' |
'MEDIUM' |
'HIGH' |
'XHIGH' |
'XXHIGH';

export const ViewportSizeGroupOrder : ViewportSizeGroup[] = [ 'XSMALL', 'SMALL', 'MEDIUM', 'LARGE', 'XLARGE'];

export const ViewportDpiGroupOrder : ViewportDpiGroup[] = [ 'XLOW', 'LOW', 'MEDIUM', 'HIGH', 'XHIGH', 'XXHIGH'];

/**
* return the {@link ViewportOrientation} of given width and height value
* @param {number} width
* @param {number} height
* @return {ViewportOrientation}
*/
export function getViewportOrientation(width : number, height : number) : ViewportOrientation {
return width > height
? 'LANDSCAPE'
: width < height
? 'PORTRAIT'
: 'EQUAL';
}

/**
* return the {@link ViewportSizeGroup} of given size value
* @param {number} size
* @return {ViewportSizeGroup}
*/
export function getViewportSizeGroup(size : number) : ViewportSizeGroup {
if (isBetween(size, 0, 600)) {
return 'XSMALL';
} else if (isBetween(size, 600, 960)) {
return 'SMALL';
} else if (isBetween(size, 960, 1280)) {
return 'MEDIUM';
} else if (isBetween(size, 1280, 1920)) {
return 'LARGE';
} else if (isBetween(size, 1920, Number.MAX_VALUE)) {
return 'XLARGE';
}

throw createAskSdkError('ViewportUtils.ts', `unknown size group value ${size}`);
}

/**
* return the {@link ViewportDpiGroup} of given dpi value
* @param {number} dpi
* @return {ViewportDpiGroup}
*/
export function getViewportDpiGroup(dpi : number) : ViewportDpiGroup {
if (isBetween(dpi, 0, 121)) {
return 'XLOW';
} else if (isBetween(dpi, 121, 161)) {
return 'LOW';
} else if (isBetween(dpi, 161, 241)) {
return 'MEDIUM';
} else if (isBetween(dpi, 241, 321)) {
return 'HIGH';
} else if (isBetween(dpi, 321, 481)) {
return 'XHIGH';
} else if (isBetween(dpi, 481, Number.MAX_VALUE)) {
return 'XXHIGH';
}

throw createAskSdkError('ViewportUtils.ts', `unknown dpi group value ${dpi}`);
}

/**
* check if target number is within the range of [min, max);
* @param {number} target
* @param {number} min
* @param {number} max
* @return {boolean}
*/
function isBetween(target : number, min : number, max : number) : boolean {
return target >= min && target < max;
}

/**
* return the {@link ViewportProfile} of given request envelope
* @param {RequestEnvelope} requestEnvelope
* @return {ViewportProfile}
*/
export function getViewportProfile(requestEnvelope : RequestEnvelope) : ViewportProfile {
const viewportState = requestEnvelope.context.Viewport;

if (viewportState) {
const currentPixelWidth = viewportState.currentPixelWidth;
const currentPixelHeight = viewportState.currentPixelHeight;
const dpi = viewportState.dpi;

const shape : Shape = viewportState.shape;
const viewportOrientation = getViewportOrientation(currentPixelWidth, currentPixelHeight);
const viewportDpiGroup = getViewportDpiGroup(dpi);
const pixelWidthSizeGroup = getViewportSizeGroup(currentPixelWidth);
const pixelHeightSizeGroup = getViewportSizeGroup(currentPixelHeight);

if (shape === 'ROUND'
&& viewportOrientation === 'EQUAL'
&& viewportDpiGroup === 'LOW'
&& pixelWidthSizeGroup === 'XSMALL'
&& pixelHeightSizeGroup === 'XSMALL'
) {
return 'HUB-ROUND-SMALL';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& viewportDpiGroup === 'LOW'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) <= ViewportSizeGroupOrder.indexOf('MEDIUM')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) <= ViewportSizeGroupOrder.indexOf('SMALL')
) {
return 'HUB-LANDSCAPE-MEDIUM';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& viewportDpiGroup === 'LOW'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('LARGE')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('SMALL')
) {
return 'HUB-LANDSCAPE-LARGE';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& viewportDpiGroup === 'MEDIUM'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('MEDIUM')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('SMALL')
) {
return 'MOBILE-LANDSCAPE-MEDIUM';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'PORTRAIT'
&& viewportDpiGroup === 'MEDIUM'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('SMALL')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('MEDIUM')
) {
return 'MOBILE-PORTRAIT-MEDIUM';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& viewportDpiGroup === 'MEDIUM'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('SMALL')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('XSMALL')
) {
return 'MOBILE-LANDSCAPE-SMALL';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'PORTRAIT'
&& viewportDpiGroup === 'MEDIUM'
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('XSMALL')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('SMALL')
) {
return 'MOBILE-PORTRAIT-SMALL';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& ViewportDpiGroupOrder.indexOf(viewportDpiGroup) >= ViewportDpiGroupOrder.indexOf('HIGH')
&& ViewportSizeGroupOrder.indexOf(pixelWidthSizeGroup) >= ViewportSizeGroupOrder.indexOf('XLARGE')
&& ViewportSizeGroupOrder.indexOf(pixelHeightSizeGroup) >= ViewportSizeGroupOrder.indexOf('MEDIUM')
) {
return 'TV-LANDSCAPE-XLARGE';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'PORTRAIT'
&& ViewportDpiGroupOrder.indexOf(viewportDpiGroup) >= ViewportDpiGroupOrder.indexOf('HIGH')
&& pixelWidthSizeGroup === 'XSMALL'
&& pixelHeightSizeGroup === 'XLARGE'
) {
return 'TV-PORTRAIT-MEDIUM';
}

if (shape === 'RECTANGLE'
&& viewportOrientation === 'LANDSCAPE'
&& ViewportDpiGroupOrder.indexOf(viewportDpiGroup) >= ViewportDpiGroupOrder.indexOf('HIGH')
&& pixelWidthSizeGroup === 'MEDIUM'
&& pixelHeightSizeGroup === 'SMALL'
) {
return 'TV-LANDSCAPE-MEDIUM';
}
}

return 'UNKNOWN-VIEWPORT-PROFILE';
}
140 changes: 140 additions & 0 deletions ask-sdk-core/tst/util/ViewportUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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://www.apache.org/licenses/LICENSE-2.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.
*/

import { interfaces } from 'ask-sdk-model';
import { expect } from 'chai';
import {
getViewportDpiGroup,
getViewportOrientation,
getViewportProfile,
getViewportSizeGroup,
} from '../../lib/util/ViewportUtils';
import { JsonProvider } from '../mocks/JsonProvider';
import ViewportState = interfaces.viewport.ViewportState;

describe('ViewportUtils.ts', () => {
it('should be able to resolve viewport orientation', () => {
expect(getViewportOrientation(0, 1)).eq('PORTRAIT');
expect(getViewportOrientation(1, 1)).eq('EQUAL');
expect(getViewportOrientation(1, 0)).eq('LANDSCAPE');
});

it('should be able to resolve viewport size group', () => {
expect(getViewportSizeGroup(0)).eq('XSMALL');
expect(getViewportSizeGroup(600)).eq('SMALL');
expect(getViewportSizeGroup(960)).eq('MEDIUM');
expect(getViewportSizeGroup(1280)).eq('LARGE');
expect(getViewportSizeGroup(1920)).eq('XLARGE');
expect(() => {
getViewportSizeGroup(-1);
}).to.throw('unknown size group value -1');
});

it('should be able to resolve viewport dpi group', () => {
expect(getViewportDpiGroup(120)).eq('XLOW');
expect(getViewportDpiGroup(160)).eq('LOW');
expect(getViewportDpiGroup(240)).eq('MEDIUM');
expect(getViewportDpiGroup(320)).eq('HIGH');
expect(getViewportDpiGroup(480)).eq('XHIGH');
expect(getViewportDpiGroup(481)).eq('XXHIGH');
expect(() => {
getViewportDpiGroup(-1);
}).throw('unknown dpi group value -1');
});

it('should return unknown profile if viewport is not present in the request envelope', () => {

const requestEnvelope = JsonProvider.requestEnvelope();
expect(getViewportProfile(requestEnvelope)).eq('UNKNOWN-VIEWPORT-PROFILE');
});

it('should be able to resolve viewport profile', () => {
const requestEnvelope = JsonProvider.requestEnvelope();
requestEnvelope.context.Viewport = {
shape : undefined,
currentPixelWidth : undefined,
currentPixelHeight : undefined,
experiences : [],
pixelWidth : undefined,
pixelHeight : undefined,
dpi : undefined,
keyboard : [],
touch : [],
};
requestEnvelope.context.Viewport.shape = 'ROUND';
requestEnvelope.context.Viewport.currentPixelHeight = 300;
requestEnvelope.context.Viewport.currentPixelWidth = 300;
requestEnvelope.context.Viewport.dpi = 160;
expect(getViewportProfile(requestEnvelope)).eq('HUB-ROUND-SMALL');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 600;
requestEnvelope.context.Viewport.currentPixelWidth = 960;
requestEnvelope.context.Viewport.dpi = 160;
expect(getViewportProfile(requestEnvelope)).eq('HUB-LANDSCAPE-MEDIUM');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 960;
requestEnvelope.context.Viewport.currentPixelWidth = 1280;
requestEnvelope.context.Viewport.dpi = 160;
expect(getViewportProfile(requestEnvelope)).eq('HUB-LANDSCAPE-LARGE');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 300;
requestEnvelope.context.Viewport.currentPixelWidth = 600;
requestEnvelope.context.Viewport.dpi = 240;
expect(getViewportProfile(requestEnvelope)).eq('MOBILE-LANDSCAPE-SMALL');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 600;
requestEnvelope.context.Viewport.currentPixelWidth = 300;
requestEnvelope.context.Viewport.dpi = 240;
expect(getViewportProfile(requestEnvelope)).eq('MOBILE-PORTRAIT-SMALL');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 600;
requestEnvelope.context.Viewport.currentPixelWidth = 960;
requestEnvelope.context.Viewport.dpi = 240;
expect(getViewportProfile(requestEnvelope)).eq('MOBILE-LANDSCAPE-MEDIUM');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 960;
requestEnvelope.context.Viewport.currentPixelWidth = 600;
requestEnvelope.context.Viewport.dpi = 240;
expect(getViewportProfile(requestEnvelope)).eq('MOBILE-PORTRAIT-MEDIUM');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 960;
requestEnvelope.context.Viewport.currentPixelWidth = 1920;
requestEnvelope.context.Viewport.dpi = 320;
expect(getViewportProfile(requestEnvelope)).eq('TV-LANDSCAPE-XLARGE');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 1920;
requestEnvelope.context.Viewport.currentPixelWidth = 300;
requestEnvelope.context.Viewport.dpi = 320;
expect(getViewportProfile(requestEnvelope)).eq('TV-PORTRAIT-MEDIUM');

requestEnvelope.context.Viewport.shape = 'RECTANGLE';
requestEnvelope.context.Viewport.currentPixelHeight = 600;
requestEnvelope.context.Viewport.currentPixelWidth = 960;
requestEnvelope.context.Viewport.dpi = 320;
expect(getViewportProfile(requestEnvelope)).eq('TV-LANDSCAPE-MEDIUM');

requestEnvelope.context.Viewport.shape = 'ROUND';
requestEnvelope.context.Viewport.currentPixelHeight = 600;
requestEnvelope.context.Viewport.currentPixelWidth = 600;
requestEnvelope.context.Viewport.dpi = 240;
expect(getViewportProfile(requestEnvelope)).eq('UNKNOWN-VIEWPORT-PROFILE');
});
});
Loading

0 comments on commit bcdfec8

Please sign in to comment.