Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support WebManifest shortcuts when generating the TWA #46

Merged
merged 3 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/cli/cmds/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ async function confirmTwaConfig(twaManifest) {
default: twaManifest.maskableIconUrl || undefined,
conform: validUrl.isWebUri,
},
shortcuts: {
name: 'shortcuts',
validator: /y[es]*|n[o]?/,
message: 'Include app shortcuts?\n' + twaManifest.shortcuts,
description: 'App shortcuts to display for users TO quickly start common or recommended ' +
'tasks within the app',
warning: 'Must respond yes or no',
default: 'yes',
ask: () => twaManifest.shortcuts !== '[]',
},
packageId: {
name: 'packageId',
description: 'Android Package Name (or Application ID):',
Expand All @@ -114,6 +124,13 @@ async function confirmTwaConfig(twaManifest) {
},
};
const result = await prompt.get(schema);

if (result.shortcuts === 'no') {
result.shortcuts = '[]';
} else {
result.shortcuts = twaManifest.shortcuts;
}

Object.assign(twaManifest, result);
return twaManifest;
}
Expand Down
13 changes: 13 additions & 0 deletions src/lib/TwaGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ const ADAPTIVE_IMAGES = [
{dest: 'app/src/main/res/mipmap-xxxhdpi/ic_maskable.png', size: 328},
];

const SHORTCUT_IMAGES = [
{dest: 'app/src/main/res/drawable-mdpi/', size: 48},
{dest: 'app/src/main/res/drawable-hdpi/', size: 72},
{dest: 'app/src/main/res/drawable-xhdpi/', size: 96},
{dest: 'app/src/main/res/drawable-xxhdpi/', size: 144},
{dest: 'app/src/main/res/drawable-xxxhdpi/', size: 192},
];

// fs.promises is marked as experimental. This should be replaced when stable.
const fsMkDir = promisify(fs.mkdir);
const fsCopyFile = promisify(fs.copyFile);
Expand Down Expand Up @@ -181,6 +189,11 @@ class TwaGenerator {

// Generate images
await this._generateIcons(args.iconUrl, targetDirectory, IMAGES);
await Promise.all(JSON.parse(args.shortcuts).map((shortcut, i) => {
const imageDirs = SHORTCUT_IMAGES.map(
(imageDir) => ({...imageDir, dest: `${imageDir.dest}shortcut_${i}.png`}));
return this._generateIcons(shortcut.chosenIconUrl, targetDirectory, imageDirs);
}));

// Generate adaptive images
if (args.maskableIconUrl) {
Expand Down
42 changes: 41 additions & 1 deletion src/lib/TwaManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ function generatePackageId(host) {
return parts.join('.').replace(DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX, '_');
}

/**
* A wrapper around the WebManifest's ShortcutInfo.
*/
class ShortcutInfo {
/**
* @param {Object} the WebManifest's ShortcutInfo.
* @param {URL} webManifestUrl the URL where the webmanifest is available.
*/
constructor(shortcutInfo, webManifestUrl) {
this.name = shortcutInfo['name'];
this.shortName = shortcutInfo['short_name'] || this.name;
this.url = shortcutInfo['url'] ?
new URL(shortcutInfo['url'], webManifestUrl).toString() : undefined;
this.icons = shortcutInfo['icons'] || [];

// TODO(rayankans): Choose the most suitable icon rather than the first one.
const sutiableIcon = this.icons.length ? this.icons[0] : null;

this.chosenIconUrl = sutiableIcon ?
new URL(sutiableIcon.src, webManifestUrl).toString() : undefined;
}

isValid() {
return this.name && this.url && this.chosenIconUrl;
}
}

/**
* A Manifest used to generate the TWA Project
*
Expand Down Expand Up @@ -82,6 +109,7 @@ class TwaManifest {
this.splashScreenFadeOutDuration = data.splashScreenFadeOutDuration;
this.signingKey = data.signingKey;
this.appVersion = data.appVersion;
this.shortcuts = data.shortcuts;
}

/**
Expand Down Expand Up @@ -135,19 +163,30 @@ class TwaManifest {
return this._hexColor(this.backgroundColor);
}

generateShortcuts() {
return '[' + JSON.parse(this.shortcuts).map((s, i) =>
`[name:'${s.name}', short_name:'${s.shortName}', url:'${s.url}', icon:'shortcut_${i}']`)
.join(',') +
']';
}

/**
* Creates a new TwaManifest, using the URL for the Manifest as a base URL and uses the content
* of the Web Manifest to generate the fields for the TWA Manifest.
*
* @param {URL} webManifestUrl the URL where the webmanifest is available.
* @param {WebManifest} the Web Manifest, used as a base for the TWA Manifest.
* @param {WebManifest} webManifest the Web Manifest, used as a base for the TWA Manifest.
* @returns {TwaManifest}
*/
static fromWebManifestJson(webManifestUrl, webManifest) {
const icon = util.findSuitableIcon(webManifest, 'any');
const maskableIcon = util.findSuitableIcon(webManifest, 'maskable');
const fullStartUrl = new URL(webManifest['start_url'] || '/', webManifestUrl);

const shortcuts = (webManifest.shortcuts || []).map((s) => new ShortcutInfo(s, webManifestUrl))
.filter((s) => s.isValid())
.filter((_, i) => i < 4);

const twaManifest = new TwaManifest({
packageId: generatePackageId(webManifestUrl.host),
host: webManifestUrl.host,
Expand All @@ -167,6 +206,7 @@ class TwaManifest {
splashScreenFadeOutDuration: 300,
useBrowserOnChromeOS: true,
enableNotifications: false,
shortcuts: JSON.stringify(shortcuts, undefined, 2),
});
return twaManifest;
}
Expand Down
19 changes: 19 additions & 0 deletions src/spec/lib/TwaManifestSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ describe('TwaManifest', () => {
}],
'theme_color': '#7cc0ff',
'background_color': '#7cc0ff',
'shortcuts': [{
'name': 'shortcut name',
'short_name': 'short',
'url': '/launch',
'icons': [{
'src': '/shortcut_icon.png',
}],
}],
};
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
const twaManifest = TwaManifest.fromWebManifestJson(manifestUrl, manifest);
Expand All @@ -54,6 +62,15 @@ describe('TwaManifest', () => {
expect(twaManifest.splashScreenFadeOutDuration).toBe(300);
expect(twaManifest.useBrowserOnChromeOS).toBeTrue();
expect(twaManifest.enableNotifications).toBeFalse();
expect(JSON.parse(twaManifest.shortcuts)).toEqual([
{
name: 'shortcut name',
shortName: 'short',
url: 'https://pwa-directory.com/launch',
icons: [{src: '/shortcut_icon.png'}],
chosenIconUrl: 'https://pwa-directory.com/shortcut_icon.png',
},
]);
});

it('Sets correct defaults for unavailable fields', () => {
Expand All @@ -75,6 +92,7 @@ describe('TwaManifest', () => {
expect(twaManifest.splashScreenFadeOutDuration).toBe(300);
expect(twaManifest.useBrowserOnChromeOS).toBeTrue();
expect(twaManifest.enableNotifications).toBeFalse();
expect(twaManifest.shortcuts).toBe('[]');
});

it('Uses "name" when "short_name" is not available', () => {
Expand Down Expand Up @@ -111,6 +129,7 @@ describe('TwaManifest', () => {
splashScreenFadeOutDuration: 300,
useBrowserOnChromeOS: true,
enableNotifications: true,
shortcuts: [{name: 'name', url: '/', icons: [{src: 'icon.png'}]}],
});
expect(twaManifest.validate()).toBeTrue();
});
Expand Down
11 changes: 4 additions & 7 deletions template_project/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import groovy.xml.MarkupBuilder;
import groovy.xml.MarkupBuilder

apply plugin: 'com.android.application'

Expand All @@ -28,15 +28,12 @@ def twaManifest = [
backgroundColor: '<%= backgroundColorHex() %>', // The color used for the splash screen background.
enableNotifications: <%= enableNotifications %>, // Set to true to enable notification delegation.
useBrowserOnChromeOS: <%= useBrowserOnChromeOS %>, // Set to false if you've added interaction with Android system APIs.
// Add shortcuts for your app here. Every shortcut must include the following fields:
// Every shortcut must include the following fields:
// - name: String that will show up in the shortcut.
// - short_name: Shorter string used if |name| is too long.
// - url: Absolute path of the URL to launch the app with (e.g '/create').
// - icon: Name of the resource in the drawable folder to use as an icon.
shortcuts: [
// Insert shortcuts here, for example:
// [name: 'Open SVG', short_name: 'Open', url: '/open', icon: 'splash']
],
shortcuts: <%= generateShortcuts() %>,
// The duration of fade out animation in milliseconds to be played when removing splash screen.
splashScreenFadeOutDuration: <%= splashScreenFadeOutDuration %>
]
Expand Down Expand Up @@ -143,7 +140,7 @@ task generateShorcutsFile {
'android:action': 'android.intent.action.MAIN',
'android:targetPackage': twaManifest.applicationId,
'android:targetClass': 'android.support.customtabs.trusted.LauncherActivity',
'android:data': 'https://' + twaManifest.hostName + s.url)
'android:data': s.url)
'categories'('android:name': 'android.intent.category.LAUNCHER')
}
}
Expand Down