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

Fix #615 Add Meta Quest support #704

Merged
9 changes: 6 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,13 @@ parse the Web manifest and generate default values for the Android project, wher
will prompt the user to confirm or input values where one could not be generated.

```
bubblewrap init --manifest="<web-manifest-url>" [--directory="<path-to-output-location>"] [--chromeosonly]
bubblewrap init --manifest="<web-manifest-url>" [--directory="<path-to-output-location>"] [--chromeosonly] [--metaquest]
```

Options:
- `--directory`: path where to generate the project. Defaults to the current directory.
- `--chromeosonly`: this flag specifies that the build will be used for Chrome OS only and prevents non-Chrome OS devices from installing the app.
- `--metaquest`: this flag specifies that the build will be compatible with Meta Quest devices.
- `--alphaDependencies`: enables features that depend on upcoming version of the Android library
for Trusted Web Activity or that are still unstable.

Expand Down Expand Up @@ -351,6 +352,7 @@ Fields:
|host|string|true|The origin that will be opened in the Trusted Web Activity.|
|iconUrl|string|true|Full URL to an the icon used for the application launcher and splash screen. Must be at least 512x512 px.|
|isChromeOSOnly|boolean|false|Generates an application that targets only ChromeOS devices. Defaults to `false`.|
|isMetaQuest|boolean|false|Generates an application that compatible with Meta Quest devices. Defaults to `false`.|
|launcherName|string|false|A short name for the Android application, displayed on the Android launcher|
|maskableIconUrl|string|false|Full URL to an the icon used for maskable icons, when supported by the device.|
|monochromeIconUrl|string|false|Full URL to a monochrome icon, used when displaying notifications.|
Expand All @@ -369,8 +371,9 @@ Fields:
|splashScreenFadeOutDuration|number|true|Duration for the splash screen fade out animation.|
|startUrl|string|true|The start path for the TWA. Must be relative to the domain.|
|themeColor|string|true|The color used for the status bar.|
|webManifestUrl|string|false|Full URL to the PWA Web Manifest. Required for the application to be compatible with Chrome OS devices.|

|webManifestUrl|string|false|Full URL to the PWA Web Manifest. Required for the application to be compatible with Chrome OS and Meta Quest devices.|
|fullScopeUrl|string|false|The navigation scope that the browser considers to be within the app. If the user navigates outside the scope, it reverts to a normal web page inside a browser tab or window. Must be a full URL. Required only for Meta Quest devices.|
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved

### Features

Developers can enable additional features in their Android application. Some features may include more dependencies into the application and increase the binary size.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/lib/cmds/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const HELP_MESSAGES = new Map<string, string>(
' directory',
'--chromeosonly ........ specifies that the build will be used for Chrome OS only and' +
' prevents non-Chrome OS devices from installing the app.',
'--metaquest ........... specifies that the build will be compatible with Meta Quest' +
' devices.',
'--alphaDependencies ... enables features that depend on upcoming version of the ' +
' Android library for Trusted Web Activity or that are still unstable.',
].join('\n')],
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/lib/cmds/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface InitArgs {
manifest?: string;
directory?: string;
chromeosonly?: boolean;
metaquest?: boolean;
alphaDependencies?: boolean;
}

Expand Down Expand Up @@ -245,6 +246,11 @@ export async function init(
twaManifest.isChromeOSOnly = true;
}

if (args.metaquest) {
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
twaManifest.isMetaQuest = true;
twaManifest.minSdkVersion = 23;
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
}

if (args.alphaDependencies) {
twaManifest.alphaDependencies = {
enabled: true,
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/lib/TwaManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const DEFAULT_NAVIGATION_DIVIDER_COLOR = '#00000000';
const DEFAULT_BACKGROUND_COLOR = '#FFFFFF';
const DEFAULT_APP_VERSION_CODE = 1;
const DEFAULT_APP_VERSION_NAME = DEFAULT_APP_VERSION_CODE.toString();
const DEFAULT_MIN_SDK_VERSION = 19;
const DEFAULT_SIGNING_KEY_PATH = './android.keystore';
const DEFAULT_SIGNING_KEY_ALIAS = 'android';
const DEFAULT_ENABLE_NOTIFICATIONS = true;
Expand Down Expand Up @@ -122,6 +123,7 @@ type alphaDependencies = {
* splashScreenFadeOutDuration: 300
* isChromeOSOnly: false, // Setting to true will enable a feature that prevents non-ChromeOS devices
* from installing the app.
* isMetaQuest: false, // Setting to true will generate the build compatible with Meta Quest devices.
* serviceAccountJsonFile: '<%= serviceAccountJsonFile %>', // The service account used to communicate with
* Google Play.
*
Expand Down Expand Up @@ -155,6 +157,9 @@ export class TwaManifest {
alphaDependencies: alphaDependencies;
enableSiteSettingsShortcut: boolean;
isChromeOSOnly: boolean;
isMetaQuest: boolean;
fullScopeUrl?: URL;
minSdkVersion: number;
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
shareTarget?: ShareTarget;
orientation: Orientation;
fingerprints: Fingerprint[];
Expand Down Expand Up @@ -201,6 +206,9 @@ export class TwaManifest {
this.enableSiteSettingsShortcut = data.enableSiteSettingsShortcut != undefined ?
data.enableSiteSettingsShortcut : true;
this.isChromeOSOnly = data.isChromeOSOnly != undefined ? data.isChromeOSOnly : false;
this.isMetaQuest = data.isMetaQuest != undefined ? data.isMetaQuest : false;
this.fullScopeUrl = data.fullScopeUrl ? new URL(data.fullScopeUrl) : undefined;
this.minSdkVersion = data.minSdkVersion || DEFAULT_MIN_SDK_VERSION;
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
this.shareTarget = data.shareTarget;
this.orientation = data.orientation || DEFAULT_ORIENTATION;
this.fingerprints = data.fingerprints || [];
Expand All @@ -224,6 +232,7 @@ export class TwaManifest {
backgroundColor: this.backgroundColor.hex(),
appVersion: this.appVersionName,
webManifestUrl: this.webManifestUrl ? this.webManifestUrl.toString() : undefined,
fullScopeUrl: this.fullScopeUrl ? this.fullScopeUrl.toString() : undefined,
});
}

Expand Down Expand Up @@ -291,6 +300,7 @@ export class TwaManifest {
findSuitableIcon(webManifest.icons, 'monochrome', MIN_NOTIFICATION_ICON_SIZE);

const fullStartUrl: URL = new URL(webManifest['start_url'] || '/', webManifestUrl);
const fullScopeUrl: URL = new URL(webManifest['scope'] || '.', webManifestUrl);
const shortcuts: ShortcutInfo[] = this.getShortcuts(webManifestUrl, webManifest);

function resolveIconUrl(icon: WebManifestIcon | null): string | undefined {
Expand Down Expand Up @@ -326,6 +336,7 @@ export class TwaManifest {
features: {},
shareTarget: TwaManifest.verifyShareTarget(webManifestUrl, webManifest.share_target),
orientation: asOrientation(webManifest.orientation) || DEFAULT_ORIENTATION,
fullScopeUrl: fullScopeUrl.toString(),
});
return twaManifest;
}
Expand Down Expand Up @@ -463,6 +474,7 @@ export class TwaManifest {
webManifestUrl);

const fullStartUrl: URL = new URL(webManifest['start_url'] || '/', webManifestUrl);
const fullScopeUrl: URL = new URL(webManifest['scope'] || '.', webManifestUrl);

const twaManifest = new TwaManifest({
...oldTwaManifestJson,
Expand All @@ -473,6 +485,8 @@ export class TwaManifest {
webManifest['name']?.substring(0, SHORT_NAME_MAX_SIZE)),
display: this.getNewFieldValue('display', fieldsToIgnore, oldTwaManifest.display,
asDisplayMode(webManifest['display']!)!),
fullScopeUrl: this.getNewFieldValue('fullScopeUrl', fieldsToIgnore,
oldTwaManifest.fullScopeUrl?.toString(), fullScopeUrl.toString()),
themeColor: this.getNewFieldValue('themeColor', fieldsToIgnore,
oldTwaManifest.themeColor.hex(), webManifest['theme_color']!),
backgroundColor: this.getNewFieldValue('backgroundColor', fieldsToIgnore,
Expand Down Expand Up @@ -528,6 +542,9 @@ export interface TwaManifestJson {
};
enableSiteSettingsShortcut?: boolean;
isChromeOSOnly?: boolean;
isMetaQuest?: boolean; // Older Manifests may not have this field.
fullScopeUrl?: string; // Older Manifests may not have this field.
minSdkVersion?: number; // Older Manifests may not have this field.
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
shareTarget?: ShareTarget;
orientation?: Orientation;
fingerprints?: Fingerprint[];
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/lib/types/WebManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface WebManifestJson {
name?: string;
short_name?: string;
start_url?: string;
scope?: string;
display?: WebManifestDisplayMode;
theme_color?: string;
background_color?: string;
Expand Down
19 changes: 11 additions & 8 deletions packages/core/src/spec/lib/TwaManifestSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ describe('TwaManifest', () => {
fallbackType: 'webview',
enableSiteSettingsShortcut: false,
isChromeOSOnly: false,
isMetaQuest: false,
serviceAccountJsonFile: '/home/service-account.json',
additionalTrustedOrigins: ['test.com'],
retainedBundles: [3, 4, 5],
Expand Down Expand Up @@ -255,6 +256,7 @@ describe('TwaManifest', () => {
expect(twaManifest.fallbackType).toBe('webview');
expect(twaManifest.enableSiteSettingsShortcut).toEqual(false);
expect(twaManifest.isChromeOSOnly).toEqual(false);
expect(twaManifest.isMetaQuest).toEqual(false);
expect(twaManifest.serviceAccountJsonFile).toEqual(twaManifestJson.serviceAccountJsonFile);
expect(twaManifest.additionalTrustedOrigins).toEqual(['test.com']);
expect(twaManifest.retainedBundles).toEqual([3, 4, 5]);
Expand Down Expand Up @@ -376,11 +378,13 @@ describe('TwaManifest', () => {
'appVersionCode': 1,
'shortcuts': [],
'generatorApp': 'bubblewrap-cli',
'webManifestUrl': 'https://name.github.io/',
'webManifestUrl': 'https://name.github.io/manifest.json',
'fallbackType': 'customtabs',
'features': {},
'enableSiteSettingsShortcut': true,
'isChromeOSOnly': false,
'isMetaQuest': false,
'fullScopeUrl': 'https://name.github.io/',
'appVersion': '1',
'serviceAccountJsonFile': '/home/service-account.json',
});
Expand All @@ -391,7 +395,7 @@ describe('TwaManifest', () => {
'display': 'fullscreen',
});
// A URL to insert as the webManifestUrl.
const url = new URL('https://name.github.io/');
const url = new URL('https://name.github.io/manifest.json');
expect(await TwaManifest.merge([], url, webManifest, twaManifest))
.toEqual(expectedTwaManifest);
});
Expand Down Expand Up @@ -434,21 +438,20 @@ describe('TwaManifest', () => {
'appVersionCode': 1,
'shortcuts': [],
'generatorApp': 'bubblewrap-cli',
'webManifestUrl': 'https://name.github.io/',
'webManifestUrl': 'https://name.github.io/manifest.json',
'fallbackType': 'customtabs',
'features': {},
'enableSiteSettingsShortcut': true,
'isChromeOSOnly': false,
'isMetaQuest': false,
'fullScopeUrl': 'https://name.github.io/',
'appVersion': '1',
'serviceAccountJsonFile': '/home/service-account.json',
});
// The versions shouldn't change because the update happens in `cli`.
const expectedTwaManifest = new TwaManifest({
...twaManifest.toJson(),
'webManifestUrl': 'https://other_url.github.io/',
});
const expectedTwaManifest = twaManifest;
// A URL to insert as the webManifestUrl.
const url = new URL('https://name.github.io/');
const url = new URL('https://name.github.io/manifest.json');
expect(await TwaManifest.merge(['short_name', 'display'], url, webManifest, twaManifest))
.toEqual(expectedTwaManifest);
});
Expand Down
11 changes: 8 additions & 3 deletions packages/core/template_project/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ android {
compileSdkVersion 31
defaultConfig {
applicationId "<%= packageId %>"
minSdkVersion 19
minSdkVersion <%= minSdkVersion %>
targetSdkVersion 31
versionCode <%= appVersionCode %>
versionName "<%= appVersionName %>"
Expand All @@ -70,11 +70,16 @@ android {

<% if (webManifestUrl) { %>
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
// The URL the Web Manifest for the Progressive Web App that the TWA points to. This
// is used by Chrome OS to open the Web version of the PWA instead of the TWA, as it
// will probably give a better user experience for non-mobile devices.
// is used by Chrome OS and Meta Quest to open the Web version of the PWA instead of
// the TWA, as it will probably give a better user experience for non-mobile devices.
resValue "string", "webManifestUrl", '<%= webManifestUrl %>'
<% } %>

<% if (fullScopeUrl) { %>
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
// This is used by Meta Quest.
resValue "string", "fullScopeUrl", '<%= fullScopeUrl %>'
<% } %>

<% if (shareTarget) { %>
// The data for the app to support web share target.
resValue "string", "shareTarget", '<%= escapeJsonString(escapeGradleString(JSON.stringify(shareTarget))) %>'
Expand Down
29 changes: 29 additions & 0 deletions packages/core/template_project/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,25 @@
<uses-permission android:name="<%= permission %>"/>
<% } %>

<% if (isMetaQuest) { %>
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
<% } %>

<% if (isChromeOSOnly) { %>
<uses-feature android:name="org.chromium.arc" android:required="true" />
<% } %>

<% if (isMetaQuest) { %>
<uses-feature
android:name="android.hardware.vr.headtracking"
android:required="false"
android:version="1" />

<uses-feature
android:name="oculus.software.handtracking"
android:required="false" />
<% } %>

<application
android:name="Application"
android:allowBackup="true"
Expand Down Expand Up @@ -61,6 +76,20 @@
android:value="<%= metadata.value %>" />
<% } %>

<% if (isMetaQuest) { %>
FluorescentHallucinogen marked this conversation as resolved.
Show resolved Hide resolved
<meta-data
android:name="com.oculus.pwa.NAME"
android:value="@string/appName" />

<meta-data
android:name="com.oculus.pwa.START_URL"
android:value="@string/launchUrl" />

<meta-data
android:name="com.oculus.pwa.SCOPE"
android:value="@string/fullScopeUrl" />
<% } %>

<% if (enableSiteSettingsShortcut) { %>
<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity">
<meta-data
Expand Down