Skip to content

Commit

Permalink
Merge pull request #46 from rayankans/shortcuts
Browse files Browse the repository at this point in the history
Support WebManifest shortcuts when generating the TWA
  • Loading branch information
andreban authored Dec 11, 2019
2 parents 40d6dec + 40de4c0 commit a91af23
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 8 deletions.
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

0 comments on commit a91af23

Please sign in to comment.