From 932c4c3df0e341a775ee55e57fd5dd4ef9824113 Mon Sep 17 00:00:00 2001 From: Rayan Kanso Date: Wed, 4 Dec 2019 17:51:12 +0000 Subject: [PATCH 1/3] Support WebManifest shortcuts when generating the TWA --- src/cli/cmds/init.js | 16 ++++++++++++ src/lib/TwaGenerator.js | 12 +++++++++ src/lib/TwaManifest.js | 41 ++++++++++++++++++++++++++++++- src/spec/lib/TwaManifestSpec.js | 11 +++++++++ template_project/app/build.gradle | 11 +++------ 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/cli/cmds/init.js b/src/cli/cmds/init.js index 90ac2f72..83541d47 100644 --- a/src/cli/cmds/init.js +++ b/src/cli/cmds/init.js @@ -89,6 +89,15 @@ async function confirmTwaConfig(twaManifest) { default: twaManifest.maskableIconUrl || undefined, conform: validUrl.isWebUri, }, + shortcuts: { + name: 'shortcuts', + validator: /y[es]*|n[o]?/, + message: 'Include app shortcuts?', + description: 'App shortcuts to display for users to users quickly start common or '+ + 'recommended tasks within the app', + warning: 'Must respond yes or no', + default: 'yes', + }, packageId: { name: 'packageId', description: 'Android Package Name (or Application ID):', @@ -114,6 +123,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; } diff --git a/src/lib/TwaGenerator.js b/src/lib/TwaGenerator.js index 33f5908f..168fba82 100644 --- a/src/lib/TwaGenerator.js +++ b/src/lib/TwaGenerator.js @@ -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); @@ -181,6 +189,10 @@ 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) { diff --git a/src/lib/TwaManifest.js b/src/lib/TwaManifest.js index 62f3c0e4..9245e610 100644 --- a/src/lib/TwaManifest.js +++ b/src/lib/TwaManifest.js @@ -41,6 +41,31 @@ 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.} shortcutInfo + */ + 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 * @@ -82,6 +107,7 @@ class TwaManifest { this.splashScreenFadeOutDuration = data.splashScreenFadeOutDuration; this.signingKey = data.signingKey; this.appVersion = data.appVersion; + this.shortcuts = data.shortcuts; } /** @@ -135,12 +161,20 @@ 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) { @@ -148,6 +182,10 @@ class TwaManifest { 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, @@ -167,6 +205,7 @@ class TwaManifest { splashScreenFadeOutDuration: 300, useBrowserOnChromeOS: true, enableNotifications: false, + shortcuts: JSON.stringify(shortcuts), }); return twaManifest; } diff --git a/src/spec/lib/TwaManifestSpec.js b/src/spec/lib/TwaManifestSpec.js index cc2a6281..c92f4006 100644 --- a/src/spec/lib/TwaManifestSpec.js +++ b/src/spec/lib/TwaManifestSpec.js @@ -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); @@ -54,6 +62,7 @@ describe('TwaManifest', () => { expect(twaManifest.splashScreenFadeOutDuration).toBe(300); expect(twaManifest.useBrowserOnChromeOS).toBeTrue(); expect(twaManifest.enableNotifications).toBeFalse(); + expect(twaManifest.shortcuts).toBe('[{"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', () => { @@ -75,6 +84,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', () => { @@ -111,6 +121,7 @@ describe('TwaManifest', () => { splashScreenFadeOutDuration: 300, useBrowserOnChromeOS: true, enableNotifications: true, + shortcuts: [{name: 'name', url: '/', icons:[{src: 'icon.png'}]}], }); expect(twaManifest.validate()).toBeTrue(); }); diff --git a/template_project/app/build.gradle b/template_project/app/build.gradle index 4ce5d65c..c3d2da62 100644 --- a/template_project/app/build.gradle +++ b/template_project/app/build.gradle @@ -14,7 +14,7 @@ * limitations under the License. */ -import groovy.xml.MarkupBuilder; +import groovy.xml.MarkupBuilder apply plugin: 'com.android.application' @@ -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 %> ] @@ -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') } } From 13effa991fd16c5faaca1fc27dd623ce69364767 Mon Sep 17 00:00:00 2001 From: Rayan Kanso Date: Thu, 5 Dec 2019 12:11:40 +0000 Subject: [PATCH 2/3] Fix lint errors and UI strings --- src/cli/cmds/init.js | 4 ++-- src/lib/TwaGenerator.js | 5 +++-- src/lib/TwaManifest.js | 21 +++++++++++---------- src/spec/lib/TwaManifestSpec.js | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/cli/cmds/init.js b/src/cli/cmds/init.js index 83541d47..33f9d4b9 100644 --- a/src/cli/cmds/init.js +++ b/src/cli/cmds/init.js @@ -93,8 +93,8 @@ async function confirmTwaConfig(twaManifest) { name: 'shortcuts', validator: /y[es]*|n[o]?/, message: 'Include app shortcuts?', - description: 'App shortcuts to display for users to users quickly start common or '+ - 'recommended tasks within the app', + 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', }, diff --git a/src/lib/TwaGenerator.js b/src/lib/TwaGenerator.js index 168fba82..ef268c44 100644 --- a/src/lib/TwaGenerator.js +++ b/src/lib/TwaGenerator.js @@ -81,7 +81,7 @@ const SHORTCUT_IMAGES = [ {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); @@ -190,7 +190,8 @@ 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`})); + const imageDirs = SHORTCUT_IMAGES.map( + (imageDir) => ({...imageDir, dest: `${imageDir.dest}shortcut_${i}.png`})); return this._generateIcons(shortcut.chosenIconUrl, targetDirectory, imageDirs); })); diff --git a/src/lib/TwaManifest.js b/src/lib/TwaManifest.js index 9245e610..06618547 100644 --- a/src/lib/TwaManifest.js +++ b/src/lib/TwaManifest.js @@ -47,18 +47,20 @@ function generatePackageId(host) { class ShortcutInfo { /** * @param {Object} the WebManifest's ShortcutInfo. - * @param {URL} webManifestUrl the URL where the webmanifest is available.} 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.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; + this.chosenIconUrl = sutiableIcon ? + new URL(sutiableIcon.src, webManifestUrl).toString() : undefined; } isValid() { @@ -162,10 +164,9 @@ class TwaManifest { } generateShortcuts() { - return '[' + - JSON.parse(this.shortcuts).map((s, i) => - `[name: '${s.name}', short_name: '${s.shortName}', url: '${s.url}', icon: 'shortcut_${i}']`) - .join(',') + + return '[' + JSON.parse(this.shortcuts).map((s, i) => + `[name:'${s.name}', short_name:'${s.shortName}', url:'${s.url}', icon:'shortcut_${i}']`) + .join(',') + ']'; } @@ -182,9 +183,9 @@ class TwaManifest { 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 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), diff --git a/src/spec/lib/TwaManifestSpec.js b/src/spec/lib/TwaManifestSpec.js index c92f4006..9f4d8dc7 100644 --- a/src/spec/lib/TwaManifestSpec.js +++ b/src/spec/lib/TwaManifestSpec.js @@ -121,7 +121,7 @@ describe('TwaManifest', () => { splashScreenFadeOutDuration: 300, useBrowserOnChromeOS: true, enableNotifications: true, - shortcuts: [{name: 'name', url: '/', icons:[{src: 'icon.png'}]}], + shortcuts: [{name: 'name', url: '/', icons: [{src: 'icon.png'}]}], }); expect(twaManifest.validate()).toBeTrue(); }); From 40de4c01dcb83bbd07a1b367173db84aa4c42577 Mon Sep 17 00:00:00 2001 From: Rayan Kanso Date: Thu, 5 Dec 2019 14:17:02 +0000 Subject: [PATCH 3/3] Display shortcut info --- src/cli/cmds/init.js | 3 ++- src/lib/TwaManifest.js | 2 +- src/spec/lib/TwaManifestSpec.js | 10 +++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cli/cmds/init.js b/src/cli/cmds/init.js index 33f9d4b9..9df291a5 100644 --- a/src/cli/cmds/init.js +++ b/src/cli/cmds/init.js @@ -92,11 +92,12 @@ async function confirmTwaConfig(twaManifest) { shortcuts: { name: 'shortcuts', validator: /y[es]*|n[o]?/, - message: 'Include app shortcuts?', + 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', diff --git a/src/lib/TwaManifest.js b/src/lib/TwaManifest.js index 06618547..9938a558 100644 --- a/src/lib/TwaManifest.js +++ b/src/lib/TwaManifest.js @@ -206,7 +206,7 @@ class TwaManifest { splashScreenFadeOutDuration: 300, useBrowserOnChromeOS: true, enableNotifications: false, - shortcuts: JSON.stringify(shortcuts), + shortcuts: JSON.stringify(shortcuts, undefined, 2), }); return twaManifest; } diff --git a/src/spec/lib/TwaManifestSpec.js b/src/spec/lib/TwaManifestSpec.js index 9f4d8dc7..0b43d5b5 100644 --- a/src/spec/lib/TwaManifestSpec.js +++ b/src/spec/lib/TwaManifestSpec.js @@ -62,7 +62,15 @@ describe('TwaManifest', () => { expect(twaManifest.splashScreenFadeOutDuration).toBe(300); expect(twaManifest.useBrowserOnChromeOS).toBeTrue(); expect(twaManifest.enableNotifications).toBeFalse(); - expect(twaManifest.shortcuts).toBe('[{"name":"shortcut name","shortName":"short","url":"https://pwa-directory.com/launch","icons":[{"src":"/shortcut_icon.png"}],"chosenIconUrl":"https://pwa-directory.com/shortcut_icon.png"}]'); + 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', () => {