Skip to content

Commit 9219637

Browse files
committed
Add file handling support
1 parent 70d7598 commit 9219637

File tree

5 files changed

+157
-0
lines changed

5 files changed

+157
-0
lines changed

packages/core/src/lib/TwaManifest.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {LocationDelegationConfig} from './features/LocationDelegationFeature';
2929
import {PlayBillingConfig} from './features/PlayBillingFeature';
3030
import {FirstRunFlagConfig} from './features/FirstRunFlagFeature';
3131
import {ArCoreConfig} from './features/ArCoreFeature';
32+
import {FileHandler, processFileHandlers} from './types/FileHandler';
3233

3334
// The minimum size needed for the app icon.
3435
const MIN_ICON_SIZE = 512;
@@ -171,6 +172,7 @@ export class TwaManifest {
171172
additionalTrustedOrigins: string[];
172173
retainedBundles: number[];
173174
protocolHandlers?: ProtocolHandler[];
175+
fileHandlers?: FileHandler[];
174176

175177
private static log = new ConsoleLog('twa-manifest');
176178

@@ -222,6 +224,7 @@ export class TwaManifest {
222224
this.additionalTrustedOrigins = data.additionalTrustedOrigins || [];
223225
this.retainedBundles = data.retainedBundles || [];
224226
this.protocolHandlers = data.protocolHandlers;
227+
this.fileHandlers = data.fileHandlers;
225228
}
226229

227230
/**
@@ -321,6 +324,12 @@ export class TwaManifest {
321324
fullScopeUrl,
322325
);
323326

327+
const fileHandlers = processFileHandlers(
328+
webManifest.file_handlers ?? [],
329+
fullStartUrl,
330+
fullScopeUrl,
331+
);
332+
324333
const twaManifest = new TwaManifest({
325334
packageId: generatePackageId(webManifestUrl.host) || '',
326335
host: webManifestUrl.host,
@@ -353,6 +362,7 @@ export class TwaManifest {
353362
orientation: asOrientation(webManifest.orientation) || DEFAULT_ORIENTATION,
354363
fullScopeUrl: fullScopeUrl.toString(),
355364
protocolHandlers: processedProtocolHandlers,
365+
fileHandlers,
356366
});
357367
return twaManifest;
358368
}
@@ -505,6 +515,18 @@ export class TwaManifest {
505515
const fullStartUrl: URL = new URL(webManifest['start_url'] || '/', webManifestUrl);
506516
const fullScopeUrl: URL = new URL(webManifest['scope'] || '.', webManifestUrl);
507517

518+
let fileHandlers = oldTwaManifestJson.fileHandlers;
519+
if (!(fieldsToIgnore.includes('file_handlers'))) {
520+
fileHandlers = processFileHandlers(
521+
webManifest.file_handlers ?? [],
522+
fullStartUrl,
523+
fullScopeUrl,
524+
);
525+
if (fileHandlers.length == 0) {
526+
fileHandlers = oldTwaManifestJson.fileHandlers;
527+
}
528+
}
529+
508530
const twaManifest = new TwaManifest({
509531
...oldTwaManifestJson,
510532
name: this.getNewFieldValue('name', fieldsToIgnore, oldTwaManifest.name,
@@ -527,6 +549,7 @@ export class TwaManifest {
527549
monochromeIconUrl: monochromeIconUrl || oldTwaManifestJson.monochromeIconUrl,
528550
shortcuts: shortcuts,
529551
protocolHandlers: protocolHandlers,
552+
fileHandlers,
530553
});
531554
return twaManifest;
532555
}
@@ -583,6 +606,7 @@ export interface TwaManifestJson {
583606
additionalTrustedOrigins?: string[];
584607
retainedBundles?: number[];
585608
protocolHandlers?: ProtocolHandler[];
609+
fileHandlers?: FileHandler[];
586610
}
587611

588612
export interface SigningKeyInfo {

packages/core/src/lib/features/FeatureManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {FirstRunFlagFeature} from './FirstRunFlagFeature';
2323
import {Log, ConsoleLog} from '../Log';
2424
import {ArCoreFeature} from './ArCoreFeature';
2525
import {ProtocolHandlersFeature} from './ProtocolHandlersFeature';
26+
import {FileHandlingFeature} from './FileHandlingFeature';
2627

2728
const ANDROID_BROWSER_HELPER_VERSIONS = {
2829
stable: 'com.google.androidbrowserhelper:androidbrowserhelper:2.6.0',
@@ -108,6 +109,10 @@ export class FeatureManager {
108109
if (twaManifest.protocolHandlers) {
109110
this.addFeature(new ProtocolHandlersFeature(twaManifest.protocolHandlers));
110111
}
112+
113+
if (twaManifest.fileHandlers) {
114+
this.addFeature(new FileHandlingFeature(twaManifest.fileHandlers));
115+
}
111116
}
112117

113118
private addFeature(feature: Feature): void {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {EmptyFeature} from './EmptyFeature';
18+
import {FileHandler} from '../types/FileHandler';
19+
20+
const activityAliasTemplate = (handler: FileHandler, index: number) => `
21+
<activity-alias
22+
android:name="FileHandlingActivity${index}"
23+
android:targetActivity="LauncherActivity"
24+
android:exported="true">
25+
<meta-data android:name="android.support.customtabs.trusted.FILE_HANDLING_ACTION_URL"
26+
android:value="${handler.actionUrl}" />
27+
<intent-filter>
28+
<action android:name="android.intent.action.VIEW"/>
29+
<category android:name="android.intent.category.DEFAULT" />
30+
<category android:name="android.intent.category.BROWSABLE"/>
31+
<data android:scheme="content" />
32+
${ handler.mimeTypes.map(
33+
(mimeType: string) => `<data android:mimeType="${mimeType}" />`
34+
).join('\n') }
35+
</intent-filter>
36+
</activity-alias>
37+
`;
38+
39+
export class FileHandlingFeature extends EmptyFeature {
40+
constructor(fileHandlers: FileHandler[]) {
41+
super('fileHandling');
42+
if (fileHandlers.length === 0) return;
43+
for (let i = 0; i < fileHandlers.length; i++) {
44+
this.androidManifest.components.push(activityAliasTemplate(fileHandlers[i], i));
45+
}
46+
}
47+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export interface FileHandlerJson {
18+
action?: string;
19+
accept?: {
20+
[mimeType: string]: Array<string>;
21+
}
22+
}
23+
24+
export interface FileHandler {
25+
actionUrl: string;
26+
mimeTypes: Array<string>;
27+
}
28+
29+
function normalizeUrl(url: string, startUrl: URL, scopeUrl: URL,): string | undefined {
30+
try {
31+
const absoluteUrl = new URL(url, startUrl);
32+
33+
if (absoluteUrl.protocol !== 'https:') {
34+
console.warn('Ignoring url with illegal scheme:', absoluteUrl.toString());
35+
return;
36+
}
37+
38+
if (absoluteUrl.origin != scopeUrl.origin) {
39+
console.warn('Ignoring url with invalid origin:', absoluteUrl.toString());
40+
return;
41+
}
42+
43+
if (!absoluteUrl.pathname.startsWith(scopeUrl.pathname)) {
44+
console.warn('Ignoring url not within manifest scope: ', absoluteUrl.toString());
45+
return;
46+
}
47+
48+
return absoluteUrl.toString();
49+
} catch (error) {
50+
console.warn('Ignoring invalid url:', url);
51+
}
52+
}
53+
54+
export function processFileHandlers(
55+
fileHandlers: FileHandlerJson[],
56+
startUrl: URL,
57+
scopeUrl: URL,
58+
): FileHandler[] {
59+
const processedFileHandlers: FileHandler[] = [];
60+
61+
for (const handler of fileHandlers) {
62+
if (!handler.action || !handler.accept) continue;
63+
64+
const actionUrl = normalizeUrl(handler.action, startUrl, scopeUrl);
65+
if (!actionUrl) continue;
66+
67+
const mimeTypes = Object.keys(handler.accept);
68+
if (mimeTypes.length == 0) continue;
69+
70+
const processedHandler: FileHandler = {
71+
actionUrl,
72+
mimeTypes,
73+
};
74+
75+
processedFileHandlers.push(processedHandler);
76+
}
77+
78+
return processedFileHandlers;
79+
}

packages/core/src/lib/types/WebManifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18+
import {FileHandlerJson} from './FileHandler';
1819
import {ProtocolHandler} from './ProtocolHandler';
1920

2021
export interface WebManifestIcon {
@@ -70,4 +71,5 @@ export interface WebManifestJson {
7071
share_target?: ShareTarget;
7172
orientation?: OrientationLock;
7273
protocol_handlers?: Array<ProtocolHandler>;
74+
file_handlers?: Array<FileHandlerJson>;
7375
}

0 commit comments

Comments
 (0)