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

Add support for fullscreen display mode #212

Merged
merged 5 commits into from
Jun 30, 2020
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
10 changes: 9 additions & 1 deletion packages/cli/src/lib/cmds/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import * as fs from 'fs';
import Color = require('color');
import * as inquirer from 'inquirer';
import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, util} from '@bubblewrap/core';
import {Config, DisplayModes, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest,
util} from '@bubblewrap/core';
import {validateColor, validateKeyPassword, validateUrl, notEmpty} from '../inputHelpers';
import {ParsedArgs} from 'minimist';
import {APP_NAME} from '../constants';
Expand Down Expand Up @@ -49,6 +50,12 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise<TwaManifest>
message: 'Name to be shown on the Android Launcher:',
default: twaManifest.launcherName,
validate: async (input): Promise<boolean> => notEmpty(input, 'Launcher name'),
}, {
name: 'display',
type: 'list',
message: 'Display mode to be used:',
default: twaManifest.display,
choices: DisplayModes,
}, {
name: 'themeColor',
type: 'input',
Expand Down Expand Up @@ -131,6 +138,7 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise<TwaManifest>
twaManifest.host = result.host;
twaManifest.name = result.name;
twaManifest.launcherName = result.launcherName;
twaManifest.display = result.display;
twaManifest.themeColor = new Color(result.themeColor);
twaManifest.backgroundColor = new Color(result.backgroundColor);
twaManifest.startUrl = result.startUrl;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {GradleWrapper} from './lib/GradleWrapper';
import Log from './lib/Log';
import {JdkHelper} from './lib/jdk/JdkHelper';
import {KeyTool} from './lib/jdk/KeyTool';
import {TwaManifest} from './lib/TwaManifest';
import {TwaManifest, DisplayModes} from './lib/TwaManifest';
import {TwaGenerator} from './lib/TwaGenerator';
import {DigitalAssetLinks} from './lib/DigitalAssetLinks';
import * as util from './lib/util';
Expand All @@ -34,5 +34,6 @@ export {AndroidSdkTools,
Log,
TwaGenerator,
TwaManifest,
DisplayModes,
util,
};
19 changes: 18 additions & 1 deletion packages/core/src/lib/TwaManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ const MIN_SHORTCUT_ICON_SIZE = 96;
// The minimum size needed for the notification icon
const MIN_NOTIFICATION_ICON_SIZE = 48;

// Supported display modes for TWA
const DISPLAY_MODE_VALUES = ['standalone', 'fullscreen'];
type DisplayMode = typeof DISPLAY_MODE_VALUES[number];
export const DisplayModes: DisplayMode[] = [...DISPLAY_MODE_VALUES];

export function asDisplayMode(input: string): DisplayMode | null {
return DISPLAY_MODE_VALUES.includes(input) ? input as DisplayMode : null;
}

// Default values used on the Twa Manifest
const DEFAULT_SPLASHSCREEN_FADEOUT_DURATION = 300;
const DEFAULT_APP_NAME = 'My TWA';
const DEFAULT_DISPLAY_MODE = 'standalone';
const DEFAULT_THEME_COLOR = '#FFFFFF';
const DEFAULT_NAVIGATION_COLOR = '#000000';
const DEFAULT_BACKGROUND_COLOR = '#FFFFFF';
Expand Down Expand Up @@ -72,6 +82,7 @@ export class ShortcutInfo {
* hostName: '<%= host %>', // The domain being opened in the TWA.
* launchUrl: '<%= startUrl %>', // The start path for the TWA. Must be relative to the domain.
* name: '<%= name %>', // The name shown on the Android Launcher.
* display: '<%= display %>', // The display mode for the TWA.
* themeColor: '<%= themeColor %>', // The color used for the status bar.
* navigationColor: '<%= themeColor %>', // The color used for the navigation bar.
* backgroundColor: '<%= backgroundColor %>', // The color used for the splash screen background.
Expand All @@ -94,6 +105,7 @@ export class TwaManifest {
host: string;
name: string;
launcherName: string;
display: DisplayMode;
themeColor: Color;
navigationColor: Color;
backgroundColor: Color;
Expand All @@ -117,7 +129,10 @@ export class TwaManifest {
this.packageId = data.packageId;
this.host = data.host;
this.name = data.name;
this.launcherName = data.launcherName || data.name; // Older Manifests may not have this field.
// Older manifests may not have this field:
this.launcherName = data.launcherName || data.name;
// Older manifests may not have this field:
this.display = asDisplayMode(data.display!) || DEFAULT_DISPLAY_MODE;
this.themeColor = new Color(data.themeColor);
this.navigationColor = new Color(data.navigationColor);
this.backgroundColor = new Color(data.backgroundColor);
Expand Down Expand Up @@ -254,6 +269,7 @@ export class TwaManifest {
name: webManifest['name'] || webManifest['short_name'] || DEFAULT_APP_NAME,
launcherName: webManifest['short_name'] ||
webManifest['name']?.substring(0, SHORT_NAME_MAX_SIZE) || DEFAULT_APP_NAME,
display: asDisplayMode(webManifest['display']!) || DEFAULT_DISPLAY_MODE,
themeColor: webManifest['theme_color'] || DEFAULT_THEME_COLOR,
navigationColor: DEFAULT_NAVIGATION_COLOR,
backgroundColor: webManifest['background_color'] || DEFAULT_BACKGROUND_COLOR,
Expand Down Expand Up @@ -306,6 +322,7 @@ export interface TwaManifestJson {
host: string;
name: string;
launcherName?: string; // Older Manifests may not have this field.
display?: string; // Older Manifests may not have this field.
themeColor: string;
navigationColor: string;
backgroundColor: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/lib/types/WebManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ export interface WebManifestShortcutJson {
icons?: Array<WebManifestIcon>;
}

type WebManifestDisplayMode = 'browser' | 'minimal-ui' | 'standalone' | 'fullscreen';

export interface WebManifestJson {
name?: string;
short_name?: string;
start_url?: string;
display?: WebManifestDisplayMode;
theme_color?: string;
background_color?: string;
icons?: Array<WebManifestIcon>;
Expand Down
33 changes: 31 additions & 2 deletions packages/core/src/spec/lib/TwaManifestSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

import {TwaManifest, TwaManifestJson} from '../../lib/TwaManifest';
import {TwaManifest, TwaManifestJson, asDisplayMode} from '../../lib/TwaManifest';
import {WebManifestJson} from '../../lib/types/WebManifest';
import Color = require('color');

describe('TwaManifest', () => {
Expand All @@ -24,6 +25,7 @@ describe('TwaManifest', () => {
'name': 'PWA Directory',
'short_name': 'PwaDirectory',
'start_url': '/?utm_source=homescreen',
'display': 'fullscreen',
'icons': [{
'src': '/favicons/android-chrome-192x192.png',
'sizes': '192x192',
Expand All @@ -46,10 +48,11 @@ describe('TwaManifest', () => {
}],
};
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
const twaManifest = TwaManifest.fromWebManifestJson(manifestUrl, manifest);
const twaManifest = TwaManifest.fromWebManifestJson(manifestUrl, manifest as WebManifestJson);
expect(twaManifest.packageId).toBe('com.pwa_directory.twa');
expect(twaManifest.name).toBe('PWA Directory');
expect(twaManifest.launcherName).toBe('PwaDirectory');
expect(twaManifest.display).toBe('fullscreen');
expect(twaManifest.startUrl).toBe('/?utm_source=homescreen');
expect(twaManifest.iconUrl)
.toBe('https://pwa-directory.com/favicons/android-chrome-512x512.png');
Expand Down Expand Up @@ -89,6 +92,7 @@ describe('TwaManifest', () => {
expect(twaManifest.iconUrl).toBeUndefined();
expect(twaManifest.maskableIconUrl).toBeUndefined();
expect(twaManifest.monochromeIconUrl).toBeUndefined();
expect(twaManifest.display).toBe('standalone');
expect(twaManifest.themeColor.hex()).toBe('#FFFFFF');
expect(twaManifest.navigationColor.hex()).toBe('#000000');
expect(twaManifest.backgroundColor.hex()).toBe('#FFFFFF');
Expand Down Expand Up @@ -165,6 +169,14 @@ describe('TwaManifest', () => {
expect(twaManifest.maskableIconUrl).toBe('https://pwa-directory.com/favicons/maskable.png');
expect(twaManifest.monochromeIconUrl).toBe('https://pwa-directory.com/favicons/monochrome.png');
});

it('Replaces unsupported display modes with `standalone`', () => {
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
expect(TwaManifest.fromWebManifestJson(manifestUrl, {display: 'minimal-ui'}).display)
.toBe('standalone');
expect(TwaManifest.fromWebManifestJson(manifestUrl, {display: 'browser'}).display)
.toBe('standalone');
});
});

describe('#constructor', () => {
Expand All @@ -176,6 +188,7 @@ describe('TwaManifest', () => {
launcherName: 'PwaDirectory',
startUrl: '/',
iconUrl: 'https://pwa-directory.com/favicons/android-chrome-512x512.png',
display: 'fullscreen',
themeColor: '#00ff00',
navigationColor: '#000000',
backgroundColor: '#0000ff',
Expand All @@ -199,6 +212,7 @@ describe('TwaManifest', () => {
expect(twaManifest.launcherName).toEqual(twaManifest.launcherName);
expect(twaManifest.startUrl).toEqual(twaManifest.startUrl);
expect(twaManifest.iconUrl).toEqual(twaManifest.iconUrl);
expect(twaManifest.display).toEqual('fullscreen');
expect(twaManifest.themeColor).toEqual(new Color('#00ff00'));
expect(twaManifest.navigationColor).toEqual(new Color('#000000'));
expect(twaManifest.backgroundColor).toEqual(new Color('#0000ff'));
Expand Down Expand Up @@ -240,6 +254,7 @@ describe('TwaManifest', () => {
const twaManifest = new TwaManifest(twaManifestJson);
expect(twaManifest.webManifestUrl).toBeUndefined();
expect(twaManifest.fallbackType).toBe('customtabs');
expect(twaManifest.display).toBe('standalone');
});
});

Expand Down Expand Up @@ -272,4 +287,18 @@ describe('TwaManifest', () => {
expect(twaManifest.validate()).toBeNull();
});
});

describe('#asDisplayMode', () => {
it('Returns display mode if it is supported', () => {
expect(asDisplayMode('standalone')).toBe('standalone');
expect(asDisplayMode('fullscreen')).toBe('fullscreen');
});

it('Returns null for unsupported display modes', () => {
expect(asDisplayMode('browser')).toBeNull();
expect(asDisplayMode('minimal-ui')).toBeNull();
expect(asDisplayMode('bogus')).toBeNull();
expect(asDisplayMode('')).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
<meta-data android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
android:value="@string/fallbackType" />

<% if (display === 'fullscreen') { %>
<meta-data android:name="android.support.customtabs.trusted.DISPLAY_MODE"
android:value="immersive" />

<% }%>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down