mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: hand files with a generic type to other apps by the type their name implies
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
filePreviewKind, inAppPreviewKind, exceedsPreviewLimit, FILE_PREVIEW_MAX_BYTES,
|
||||
needsNativeImageDecoder, needsWebImageViewer,
|
||||
needsNativeImageDecoder, needsWebImageViewer, handoffMimeType,
|
||||
} from '../file-preview';
|
||||
|
||||
const IOS = { platform: 'ios' } as const;
|
||||
@@ -145,3 +145,24 @@ describe('preview size limits', () => {
|
||||
expect(inAppPreviewKind({ name: 'film.mp4', type: 'video/mp4', size: 300 * 1024 * 1024 }, ANDROID)).toBe('video');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handoffMimeType', () => {
|
||||
it('keeps a real declared type', () => {
|
||||
expect(handoffMimeType('a.pdf', 'application/pdf')).toBe('application/pdf');
|
||||
expect(handoffMimeType('scan', 'image/png')).toBe('image/png');
|
||||
expect(handoffMimeType('a.zip', 'application/x-zip-compressed')).toBe('application/zip');
|
||||
});
|
||||
|
||||
it('names the format when the declared type does not, so Android finds a viewer', () => {
|
||||
expect(handoffMimeType('contract.docx', 'application/octet-stream'))
|
||||
.toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
expect(handoffMimeType('invoice.PDF', '')).toBe('application/pdf');
|
||||
expect(handoffMimeType('invoice.pdf', 'application/x-as400attachment')).toBe('application/pdf');
|
||||
expect(handoffMimeType('photo.jpg', 'application/x-unknown')).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('falls back to the declared type or octet-stream for unknown names', () => {
|
||||
expect(handoffMimeType('data.bin', '')).toBe('application/octet-stream');
|
||||
expect(handoffMimeType('data.xyz', 'application/x-foo')).toBe('application/x-foo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './download-filename';
|
||||
import { getClientCertAlias, secureFetch } from './client-cert';
|
||||
import { sniffImageMime } from './email-html';
|
||||
import { handoffMimeType } from './file-preview';
|
||||
|
||||
const RFC822 = 'message/rfc822';
|
||||
const FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
|
||||
@@ -156,7 +157,7 @@ async function openWithViewer(file: File, mimeType: string): Promise<boolean> {
|
||||
// intent. Deliberately no FLAG_ACTIVITY_NEW_TASK: expo-intent-launcher
|
||||
// uses startActivityForResult, and a new task cancels that result.
|
||||
data: file.contentUri,
|
||||
type: mimeType,
|
||||
type: handoffMimeType(file.name, mimeType),
|
||||
flags: FLAG_GRANT_READ_URI_PERMISSION,
|
||||
});
|
||||
return true;
|
||||
@@ -259,7 +260,7 @@ export async function shareAttachment(
|
||||
}
|
||||
try {
|
||||
await Sharing.shareAsync(downloaded.uri, {
|
||||
mimeType,
|
||||
mimeType: handoffMimeType(filename, mimeType),
|
||||
dialogTitle: filename,
|
||||
});
|
||||
} finally {
|
||||
@@ -296,7 +297,7 @@ async function offerSavedFile(file: File, filename: string, mimeType: string): P
|
||||
throw new Error(t('files.share_unavailable', 'Sharing is not available on this device'));
|
||||
}
|
||||
await Sharing.shareAsync(file.uri, {
|
||||
mimeType,
|
||||
mimeType: handoffMimeType(filename, mimeType),
|
||||
dialogTitle: filename,
|
||||
});
|
||||
}
|
||||
@@ -422,7 +423,7 @@ export async function shareLocalFile(
|
||||
throw new Error(t('files.share_unavailable', 'Sharing is not available on this device'));
|
||||
}
|
||||
try {
|
||||
await Sharing.shareAsync(file.uri, { mimeType, dialogTitle: dialogTitle ?? file.name });
|
||||
await Sharing.shareAsync(file.uri, { mimeType: handoffMimeType(file.name, mimeType), dialogTitle: dialogTitle ?? file.name });
|
||||
} finally {
|
||||
if (!opts.keep) scheduleTempCleanup(file);
|
||||
}
|
||||
|
||||
@@ -179,3 +179,81 @@ export function needsWebImageViewer(file: Pick<PreviewableFile, 'name' | 'type'>
|
||||
const mime = baseMime(file.type);
|
||||
return /^image\/(svg\+xml|x-icon|vnd\.microsoft\.icon|ico)$/.test(mime) || /\.(svg|ico)$/i.test(file.name);
|
||||
}
|
||||
|
||||
// Types for handing a file to another app when the sender declared none, or
|
||||
// a catch-all: Android picks the apps for "Open with" by type, and finds
|
||||
// none for application/octet-stream.
|
||||
const EXT_TO_MIME: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
odt: 'application/vnd.oasis.opendocument.text',
|
||||
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
odp: 'application/vnd.oasis.opendocument.presentation',
|
||||
rtf: 'application/rtf',
|
||||
epub: 'application/epub+zip',
|
||||
zip: 'application/zip',
|
||||
'7z': 'application/x-7z-compressed',
|
||||
rar: 'application/vnd.rar',
|
||||
gz: 'application/gzip',
|
||||
tar: 'application/x-tar',
|
||||
apk: 'application/vnd.android.package-archive',
|
||||
txt: 'text/plain',
|
||||
csv: 'text/csv',
|
||||
html: 'text/html',
|
||||
htm: 'text/html',
|
||||
md: 'text/markdown',
|
||||
json: 'application/json',
|
||||
xml: 'application/xml',
|
||||
ics: 'text/calendar',
|
||||
vcf: 'text/vcard',
|
||||
eml: 'message/rfc822',
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
heic: 'image/heic',
|
||||
heif: 'image/heif',
|
||||
avif: 'image/avif',
|
||||
bmp: 'image/bmp',
|
||||
tif: 'image/tiff',
|
||||
tiff: 'image/tiff',
|
||||
svg: 'image/svg+xml',
|
||||
ico: 'image/x-icon',
|
||||
mp3: 'audio/mpeg',
|
||||
m4a: 'audio/mp4',
|
||||
aac: 'audio/aac',
|
||||
wav: 'audio/wav',
|
||||
ogg: 'audio/ogg',
|
||||
opus: 'audio/ogg',
|
||||
flac: 'audio/flac',
|
||||
amr: 'audio/amr',
|
||||
mp4: 'video/mp4',
|
||||
m4v: 'video/mp4',
|
||||
mov: 'video/quicktime',
|
||||
webm: 'video/webm',
|
||||
mkv: 'video/x-matroska',
|
||||
avi: 'video/x-msvideo',
|
||||
wmv: 'video/x-ms-wmv',
|
||||
'3gp': 'video/3gpp',
|
||||
};
|
||||
|
||||
/**
|
||||
* The type to hand a file to another app with: the declared one, unless it
|
||||
* is generic or a catch-all that names no format, in which case the one the
|
||||
* file name implies.
|
||||
*/
|
||||
export function handoffMimeType(name: string | undefined, type: string | null | undefined): string {
|
||||
const declared = baseMime(type);
|
||||
const fromName = EXT_TO_MIME[extension(name || '')];
|
||||
if (!fromName) return type || 'application/octet-stream';
|
||||
if (GENERIC_MIME.has(declared)) return fromName;
|
||||
// An "x-" application type the name contradicts, such as x-as400attachment.
|
||||
if (/^application\/x-/.test(declared) && declared !== fromName) return fromName;
|
||||
return type || fromName;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user