掌握 HarmonyOS APP媒体 Intent 机制玩法
一、背景与动机
你有没有想过,当你在相册里长按一张照片选择"分享"时,系统是怎么知道哪些 App 可以接收这张图片的?当你在微信里点开一个视频链接时,系统又是怎么决定用哪个播放器来打开的?
这些看似"理所当然"的操作,背后靠的就是 Intent 机制。Intent 就像一条"消息管道",它把"我想做某件事"这个意图从一个应用传递到另一个应用,让不同应用之间可以协作完成媒体相关的操作。
在媒体开发场景中,Intent 的使用频率极高——分享图片/视频给其他 App、用系统播放器打开媒体文件、把音频文件关联到你的音乐 App、接收其他 App 发来的媒体数据……这些都需要你深入理解媒体 Intent 的工作机制。
二、核心原理
2.1 Intent 机制总览
HarmonyOS 的 Intent 机制基于 Want 对象实现,Want 描述了"想要做什么"以及"需要什么数据"。在媒体场景下,Want 通常携带媒体 URI、MIME 类型等关键信息。
flowchart TB
classDef primary fill:#4A90D9,stroke:#2E6BA6,color:#fff,stroke-width:2px
classDef warning fill:#E6A23C,stroke:#CF8C12,color:#fff,stroke-width:2px
classDef error fill:#F56C6C,stroke:#DD3B3B,color:#fff,stroke-width:2px
classDef info fill:#67C23A,stroke:#4AA82A,color:#fff,stroke-width:2px
classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,stroke-width:2px
A[源应用]:::primary --> B[构造 Want]:::warning
B --> C{Intent 类型}:::info
C -->|显式| D[指定目标 Ability]:::purple
C -->|隐式| E[匹配 Intent 过滤器]:::error
E --> F[MIME 类型匹配]:::primary
E --> G[Action 匹配]:::primary
E --> H[URI Scheme 匹配]:::primary
F --> I[候选列表]:::warning
G --> I
H --> I
I --> J[用户选择/系统默认]:::info
J --> K[目标应用]:::purple
D --> K
2.2 核心 Want 属性(媒体相关)
| 属性 | 说明 | 媒体场景示例 |
|---|---|---|
| action | 要执行的操作 | Action.ACTION_SEND(分享)、Action.ACTION_VIEW(查看) |
| type | MIME 类型 | image/jpeg、video/mp4、audio/mpeg |
| uri | 数据 URI | file:///photos/img.jpg、content://media/video/123 |
| parameters | 附加参数 | 分享文本、媒体标题等 |
| bundleName | 目标应用包名 | 显式调用时使用 |
| abilityName | 目标 Ability 名 | 显式调用时使用 |
2.3 媒体 MIME 类型体系
flowchart LR
classDef primary fill:#4A90D9,stroke:#2E6BA6,color:#fff,stroke-width:2px
classDef warning fill:#E6A23C,stroke:#CF8C12,color:#fff,stroke-width:2px
classDef error fill:#F56C6C,stroke:#DD3B3B,color:#fff,stroke-width:2px
classDef info fill:#67C23A,stroke:#4AA82A,color:#fff,stroke-width:2px
classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,stroke:#2px
A[媒体 MIME]:::primary --> B[image/*]:::warning
A --> C[video/*]:::error
A --> D[audio/*]:::info
A --> E[application/*]:::purple
B --> B1[image/jpeg]
B --> B2[image/png]
B --> B3[image/gif]
B --> B4[image/webp]
C --> C1[video/mp4]
C --> C2[video/avi]
C --> C3[video/mkv]
D --> D1[audio/mpeg]
D --> D2[audio/wav]
D --> D3[audio/flac]
E --> E1[application/pdf]
E --> E2[application/x-subrip]
三、代码实战
3.1 媒体分享 Intent
最常见的媒体 Intent 场景——把图片或视频分享给其他应用。
// MediaShareIntent.ets
// 媒体分享 Intent 完整实现
import { common, Want, WantConstant } from '@kit.AbilityKit';
import { fileUri } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct MediaShareIntent {
// 待分享的媒体文件列表
@State mediaFiles: string[] = [];
// 分享状态
@State shareStatus: string = '就绪';
// 上下文
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 分享单张图片
async shareSingleImage(imageUri: string) {
try {
const want: Want = {
// 分享动作
action: WantConstant.Action.ACTION_SEND,
// 图片 MIME 类型
type: 'image/jpeg',
// 附加数据:图片 URI
parameters: {
'ability.params.stream': [imageUri]
},
// 不指定目标,让系统弹出选择器
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
// 启动分享
await this.context.startAbility(want);
this.shareStatus = '分享成功';
console.info(`[媒体Intent] 分享图片: ${imageUri}`);
} catch (error) {
const err = error as BusinessError;
this.shareStatus = `分享失败: ${err.message}`;
console.error(`[媒体Intent] 分享失败: ${err.code} - ${err.message}`);
}
}
// 分享多张图片
async shareMultipleImages(imageUris: string[]) {
if (imageUris.length === 0) {
console.warn('[媒体Intent] 没有可分享的图片');
return;
}
try {
const want: Want = {
// 多文件分享动作
action: WantConstant.Action.ACTION_SEND_MULTIPLE,
type: 'image/*',
parameters: {
'ability.params.stream': imageUris
},
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.shareStatus = `已分享 ${imageUris.length} 张图片`;
console.info(`[媒体Intent] 批量分享 ${imageUris.length} 张图片`);
} catch (error) {
const err = error as BusinessError;
this.shareStatus = `批量分享失败: ${err.message}`;
console.error(`[媒体Intent] 批量分享失败: ${err.code} - ${err.message}`);
}
}
// 分享视频文件
async shareVideo(videoUri: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_SEND,
type: 'video/mp4',
parameters: {
'ability.params.stream': [videoUri]
},
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.shareStatus = '视频分享成功';
console.info(`[媒体Intent] 分享视频: ${videoUri}`);
} catch (error) {
const err = error as BusinessError;
this.shareStatus = `视频分享失败: ${err.message}`;
console.error(`[媒体Intent] 视频分享失败: ${err.code} - ${err.message}`);
}
}
// 分享音频文件
async shareAudio(audioUri: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_SEND,
type: 'audio/mpeg',
parameters: {
'ability.params.stream': [audioUri]
},
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.shareStatus = '音频分享成功';
console.info(`[媒体Intent] 分享音频: ${audioUri}`);
} catch (error) {
const err = error as BusinessError;
this.shareStatus = `音频分享失败: ${err.message}`;
console.error(`[媒体Intent] 音频分享失败: ${err.code} - ${err.message}`);
}
}
// 分享带文本描述的媒体
async shareMediaWithText(mediaUri: string, mimeType: string, text: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_SEND,
type: mimeType,
parameters: {
'ability.params.stream': [mediaUri],
// 附加文本描述
'ability.params.text': text
},
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.shareStatus = '带文本分享成功';
console.info(`[媒体Intent] 带文本分享: ${mediaUri}`);
} catch (error) {
const err = error as BusinessError;
this.shareStatus = `分享失败: ${err.message}`;
}
}
build() {
Column() {
// 标题
Text('媒体分享')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 状态显示
Text(this.shareStatus)
.fontSize(14)
.fontColor('#4A90D9')
.margin({ bottom: 16 })
// 分享操作按钮
Column() {
// 分享图片
Button('分享单张图片')
.width('100%')
.height(44)
.backgroundColor('#4A90D9')
.fontColor(Color.White)
.margin({ bottom: 12 })
.onClick(() => {
this.shareSingleImage('file:///data/storage/el2/base/haps/entry/files/sample.jpg');
})
// 批量分享图片
Button('批量分享图片')
.width('100%')
.height(44)
.backgroundColor('#67C23A')
.fontColor(Color.White)
.margin({ bottom: 12 })
.onClick(() => {
this.shareMultipleImages([
'file:///data/storage/el2/base/haps/entry/files/img1.jpg',
'file:///data/storage/el2/base/haps/entry/files/img2.jpg',
'file:///data/storage/el2/base/haps/entry/files/img3.jpg'
]);
})
// 分享视频
Button('分享视频')
.width('100%')
.height(44)
.backgroundColor('#E6A23C')
.fontColor(Color.White)
.margin({ bottom: 12 })
.onClick(() => {
this.shareVideo('file:///data/storage/el2/base/haps/entry/files/demo.mp4');
})
// 分享音频
Button('分享音频')
.width('100%')
.height(44)
.backgroundColor('#9B59B6')
.fontColor(Color.White)
.margin({ bottom: 12 })
.onClick(() => {
this.shareAudio('file:///data/storage/el2/base/haps/entry/files/song.mp3');
})
// 带文本分享
Button('带文本描述分享')
.width('100%')
.height(44)
.backgroundColor('#F56C6C')
.fontColor(Color.White)
.onClick(() => {
this.shareMediaWithText(
'file:///data/storage/el2/base/haps/entry/files/photo.jpg',
'image/jpeg',
'这是一张美丽的风景照片 #旅行 #摄影'
);
})
}
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
}
}
3.2 打开媒体文件与类型关联
当你的 App 需要打开一个媒体文件时,可以通过 Intent 让系统选择合适的播放器或查看器。反过来,你也可以注册 Intent 过滤器,让你的 App 成为某种媒体类型的默认处理程序。
// MediaFileOpen.ets
// 打开媒体文件与类型关联
import { common, Want, WantConstant } from '@kit.AbilityKit';
import { fileUri } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct MediaFileOpen {
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 最近打开的文件
@State recentFiles: string[] = [];
// 打开状态
@State openStatus: string = '就绪';
// 通过 Intent 打开图片
async openImage(imageUri: string) {
try {
const want: Want = {
// 查看动作
action: WantConstant.Action.ACTION_VIEW,
// 图片 MIME 类型
type: 'image/jpeg',
// 图片 URI
uri: imageUri,
// 授权目标应用读取 URI 的权限
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.updateRecentFiles(imageUri);
this.openStatus = '图片已打开';
console.info(`[媒体Intent] 打开图片: ${imageUri}`);
} catch (error) {
const err = error as BusinessError;
this.openStatus = `打开失败: ${err.message}`;
console.error(`[媒体Intent] 打开图片失败: ${err.code} - ${err.message}`);
}
}
// 通过 Intent 打开视频
async openVideo(videoUri: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_VIEW,
type: 'video/mp4',
uri: videoUri,
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.updateRecentFiles(videoUri);
this.openStatus = '视频已打开';
console.info(`[媒体Intent] 打开视频: ${videoUri}`);
} catch (error) {
const err = error as BusinessError;
this.openStatus = `打开失败: ${err.message}`;
}
}
// 通过 Intent 打开音频
async openAudio(audioUri: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_VIEW,
type: 'audio/mpeg',
uri: audioUri,
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.updateRecentFiles(audioUri);
this.openStatus = '音频已打开';
console.info(`[媒体Intent] 打开音频: ${audioUri}`);
} catch (error) {
const err = error as BusinessError;
this.openStatus = `打开失败: ${err.message}`;
}
}
// 使用特定应用打开媒体文件(显式 Intent)
async openWithSpecificApp(mediaUri: string, mimeType: string, bundleName: string, abilityName: string) {
try {
const want: Want = {
action: WantConstant.Action.ACTION_VIEW,
type: mimeType,
uri: mediaUri,
// 显式指定目标应用
bundleName: bundleName,
abilityName: abilityName,
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.openStatus = `已使用 ${bundleName} 打开`;
console.info(`[媒体Intent] 显式打开: ${bundleName}/${abilityName}`);
} catch (error) {
const err = error as BusinessError;
this.openStatus = `显式打开失败: ${err.message}`;
}
}
// 根据文件扩展名推断 MIME 类型
private getMimeTypeFromExtension(filePath: string): string {
const extension = filePath.split('.').pop()?.toLowerCase() || '';
const mimeMap: Record<string, string> = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'bmp': 'image/bmp',
'mp4': 'video/mp4',
'avi': 'video/avi',
'mkv': 'video/x-matroska',
'mov': 'video/quicktime',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'flac': 'audio/flac',
'aac': 'audio/aac',
'ogg': 'audio/ogg'
};
return mimeMap[extension] || 'application/octet-stream';
}
// 通用打开方法:自动识别类型
async openMediaFile(filePath: string) {
const mimeType = this.getMimeTypeFromExtension(filePath);
try {
const want: Want = {
action: WantConstant.Action.ACTION_VIEW,
type: mimeType,
uri: filePath,
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
await this.context.startAbility(want);
this.updateRecentFiles(filePath);
this.openStatus = `已打开 (${mimeType})`;
} catch (error) {
const err = error as BusinessError;
this.openStatus = `打开失败: ${err.message}`;
}
}
// 更新最近文件列表
private updateRecentFiles(filePath: string) {
const fileName = filePath.split('/').pop() || filePath;
this.recentFiles = [fileName, ...this.recentFiles].slice(0, 10);
}
build() {
Column() {
Text('打开媒体文件')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Text(this.openStatus)
.fontSize(14)
.fontColor('#4A90D9')
.margin({ bottom: 16 })
// 快捷打开按钮
Row() {
Button('打开图片')
.onClick(() => this.openImage('file:///photos/landscape.jpg'))
.backgroundColor('#4A90D9')
.fontColor(Color.White)
.layoutWeight(1)
Button('打开视频')
.onClick(() => this.openVideo('file:///videos/demo.mp4'))
.backgroundColor('#E6A23C')
.fontColor(Color.White)
.layoutWeight(1)
.margin({ left: 8 })
Button('打开音频')
.onClick(() => this.openAudio('file:///music/song.mp3'))
.backgroundColor('#9B59B6')
.fontColor(Color.White)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
.margin({ bottom: 16 })
// 最近打开的文件
Text('最近打开')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
List() {
ForEach(this.recentFiles, (file: string) => {
ListItem() {
Row() {
Text('📄')
.fontSize(18)
Text(file)
.fontSize(14)
.margin({ left: 8 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.padding(10)
.borderRadius(6)
.backgroundColor('#F5F7FA')
}
.margin({ bottom: 4 })
}, (file: string, index?: number) => `${index}`)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(16)
}
}
3.3 Intent 过滤器与跨应用媒体调用
这一部分展示如何配置 Intent 过滤器让你的 App 能接收其他应用发来的媒体数据,以及如何实现跨应用的媒体调用。
// MediaIntentFilter.ets
// Intent 过滤器配置与跨应用媒体调用
import { common, Want, WantConstant, AbilityConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// ======== 第一部分:接收方 Ability 配置 ========
// 在 module.json5 中配置 Intent 过滤器
// 以下为配置示例(非代码,是配置文件内容):
//
// "abilities": [
// {
// "name": "MediaReceiverAbility",
// "skills": [
// {
// "entities": ["entity.system.default"],
// "actions": [
// "action.system.send", // 接收分享
// "action.system.view" // 接收查看
// ],
// "uris": [
// {
// "scheme": "file",
// "type": "image/jpeg"
// },
// {
// "scheme": "file",
// "type": "video/mp4"
// },
// {
// "scheme": "content",
// "type": "audio/mpeg"
// }
// ]
// }
// ]
// }
// ]
// ======== 第二部分:接收方 Ability 实现 ========
// MediaReceiverAbility.ets
// 接收来自其他应用的媒体数据
export default class MediaReceiverAbility extends common.UIAbility {
// 当其他应用通过 Intent 启动此 Ability 时触发
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
console.info('[媒体Intent] 收到外部 Intent');
this.handleIncomingMedia(want);
}
// 处理传入的媒体数据
private handleIncomingMedia(want: Want) {
const action = want.action;
const mimeType = want.type;
const uri = want.uri;
const parameters = want.parameters;
console.info(`[媒体Intent] Action: ${action}`);
console.info(`[媒体Intent] MIME: ${mimeType}`);
console.info(`[媒体Intent] URI: ${uri}`);
switch (action) {
case WantConstant.Action.ACTION_SEND:
this.handleSendAction(want);
break;
case WantConstant.Action.ACTION_VIEW:
this.handleViewAction(want);
break;
default:
console.warn(`[媒体Intent] 未知的 Action: ${action}`);
}
}
// 处理分享动作
private handleSendAction(want: Want) {
const mimeType = want.type || '';
const uri = want.uri || '';
const stream = want.parameters?.['ability.params.stream'] as string[];
const text = want.parameters?.['ability.params.text'] as string;
if (stream && stream.length > 0) {
// 接收到媒体文件流
console.info(`[媒体Intent] 收到 ${stream.length} 个媒体文件`);
for (const fileUri of stream) {
this.processMediaFile(fileUri, mimeType);
}
} else if (uri) {
// 接收到单个媒体 URI
this.processMediaFile(uri, mimeType);
}
if (text) {
console.info(`[媒体Intent] 附加文本: ${text}`);
}
}
// 处理查看动作
private handleViewAction(want: Want) {
const uri = want.uri || '';
const mimeType = want.type || '';
if (uri) {
console.info(`[媒体Intent] 打开媒体: ${uri} (${mimeType})`);
// 在 UI 中展示该媒体文件
// 可以通过 AppStorage 或 LocalStorage 将 URI 传递给 UI 页面
AppStorage.setOrCreate('incomingMediaUri', uri);
AppStorage.setOrCreate('incomingMediaType', mimeType);
}
}
// 处理媒体文件
private processMediaFile(fileUri: string, mimeType: string) {
// 根据媒体类型进行不同处理
if (mimeType.startsWith('image/')) {
console.info(`[媒体Intent] 处理图片: ${fileUri}`);
// 保存到相册、显示预览等
} else if (mimeType.startsWith('video/')) {
console.info(`[媒体Intent] 处理视频: ${fileUri}`);
// 添加到播放列表、开始播放等
} else if (mimeType.startsWith('audio/')) {
console.info(`[媒体Intent] 处理音频: ${fileUri}`);
// 添加到音乐库、开始播放等
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
// Ability 已在后台时,再次收到 Intent 的处理
this.handleIncomingMedia(want);
}
}
// ======== 第三部分:跨应用媒体调用封装 ========
// CrossAppMediaCaller.ets
// 跨应用媒体调用工具类
export class CrossAppMediaCaller {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// 调用系统相册选择图片
async pickImageFromGallery(): Promise<string | null> {
try {
const want: Want = {
action: 'action.system.pick',
type: 'image/*',
parameters: {
// 选择模式
'pick.mode': 'single',
// 最大选择数量
'pick.maxCount': 1
}
};
const result = await this.context.startAbilityForResult(want, {
windowMode: 0
});
if (result.resultCode === 0) {
const selectedUri = result.want?.uri || null;
console.info(`[跨应用调用] 选择图片: ${selectedUri}`);
return selectedUri;
}
return null;
} catch (error) {
const err = error as BusinessError;
console.error(`[跨应用调用] 选择图片失败: ${err.code} - ${err.message}`);
return null;
}
}
// 调用系统相机拍照
async capturePhoto(): Promise<string | null> {
try {
const want: Want = {
action: 'action.system.capture',
type: 'image/jpeg',
parameters: {
'capture.outputFormat': 'jpeg',
'capture.quality': 90
}
};
const result = await this.context.startAbilityForResult(want, {
windowMode: 0
});
if (result.resultCode === 0) {
const photoUri = result.want?.uri || null;
console.info(`[跨应用调用] 拍照结果: ${photoUri}`);
return photoUri;
}
return null;
} catch (error) {
const err = error as BusinessError;
console.error(`[跨应用调用] 拍照失败: ${err.code} - ${err.message}`);
return null;
}
}
// 调用系统录像
async recordVideo(): Promise<string | null> {
try {
const want: Want = {
action: 'action.system.record',
type: 'video/mp4',
parameters: {
'record.outputFormat': 'mp4',
'record.maxDuration': 60000, // 最长60秒
'record.quality': 'high'
}
};
const result = await this.context.startAbilityForResult(want, {
windowMode: 0
});
if (result.resultCode === 0) {
const videoUri = result.want?.uri || null;
console.info(`[跨应用调用] 录像结果: ${videoUri}`);
return videoUri;
}
return null;
} catch (error) {
const err = error as BusinessError;
console.error(`[跨应用调用] 录像失败: ${err.code} - ${err.message}`);
return null;
}
}
// 调用系统录音
async recordAudio(): Promise<string | null> {
try {
const want: Want = {
action: 'action.system.record',
type: 'audio/mpeg',
parameters: {
'record.outputFormat': 'mp3',
'record.maxDuration': 300000, // 最长5分钟
'record.sampleRate': 44100
}
};
const result = await this.context.startAbilityForResult(want, {
windowMode: 0
});
if (result.resultCode === 0) {
const audioUri = result.want?.uri || null;
console.info(`[跨应用调用] 录音结果: ${audioUri}`);
return audioUri;
}
return null;
} catch (error) {
const err = error as BusinessError;
console.error(`[跨应用调用] 录音失败: ${err.code} - ${err.message}`);
return null;
}
}
// 调用系统编辑器编辑图片
async editImage(imageUri: string): Promise<string | null> {
try {
const want: Want = {
action: 'action.system.edit',
type: 'image/jpeg',
uri: imageUri,
flags: WantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION |
WantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION
};
const result = await this.context.startAbilityForResult(want, {
windowMode: 0
});
if (result.resultCode === 0) {
const editedUri = result.want?.uri || null;
console.info(`[跨应用调用] 编辑结果: ${editedUri}`);
return editedUri;
}
return null;
} catch (error) {
const err = error as BusinessError;
console.error(`[跨应用调用] 编辑失败: ${err.code} - ${err.message}`);
return null;
}
}
}
// ======== 第四部分:UI 页面集成 ========
@Entry
@Component
struct CrossAppMediaPage {
private caller: CrossAppMediaCaller = new CrossAppMediaCaller(getContext(this) as common.UIAbilityContext);
// 操作结果
@State resultMessage: string = '等待操作';
// 获取到的媒体 URI
@State mediaUri: string = '';
build() {
Column() {
Text('跨应用媒体调用')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 结果展示
Column() {
Text(this.resultMessage)
.fontSize(14)
.fontColor('#333333')
if (this.mediaUri) {
Text(this.mediaUri)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#F5F7FA')
.margin({ bottom: 16 })
// 调用按钮
Button('选择图片')
.width('100%')
.height(44)
.backgroundColor('#4A90D9')
.fontColor(Color.White)
.margin({ bottom: 8 })
.onClick(async () => {
const uri = await this.caller.pickImageFromGallery();
if (uri) {
this.mediaUri = uri;
this.resultMessage = '已选择图片';
} else {
this.resultMessage = '未选择图片';
}
})
Button('拍照')
.width('100%')
.height(44)
.backgroundColor('#67C23A')
.fontColor(Color.White)
.margin({ bottom: 8 })
.onClick(async () => {
const uri = await this.caller.capturePhoto();
if (uri) {
this.mediaUri = uri;
this.resultMessage = '拍照成功';
} else {
this.resultMessage = '拍照取消';
}
})
Button('录像')
.width('100%')
.height(44)
.backgroundColor('#E6A23C')
.fontColor(Color.White)
.margin({ bottom: 8 })
.onClick(async () => {
const uri = await this.caller.recordVideo();
if (uri) {
this.mediaUri = uri;
this.resultMessage = '录像成功';
} else {
this.resultMessage = '录像取消';
}
})
Button('录音')
.width('100%')
.height(44)
.backgroundColor('#9B59B6')
.fontColor(Color.White)
.margin({ bottom: 8 })
.onClick(async () => {
const uri = await this.caller.recordAudio();
if (uri) {
this.mediaUri = uri;
this.resultMessage = '录音成功';
} else {
this.resultMessage = '录音取消';
}
})
}
.width('100%')
.height('100%')
.padding(16)
}
}
四、踩坑与注意事项
4.1 URI 权限问题
这是媒体 Intent 开发中最常见的坑。当你通过 Intent 传递文件 URI 给其他应用时,必须授予读取权限,否则对方应用会报"权限拒绝"。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 目标应用无法读取文件 | 未授予 URI 读取权限 | 添加 FLAG_AUTH_READ_URI_PERMISSION |
| 目标应用无法修改文件 | 未授予 URI 写入权限 | 添加 FLAG_AUTH_WRITE_URI_PERMISSION |
| 文件路径无效 | 使用了应用私有路径 | 使用 fileUri.getUriFromPath() 转换为可分享的 URI |
4.2 MIME 类型匹配
- 精确匹配 vs 通配匹配:
image/jpeg只匹配 JPEG,image/*匹配所有图片类型。分享时建议用精确类型,接收时建议用通配类型 - 大小写敏感:MIME 类型是大小写不敏感的,但实际开发中建议统一用小写
- 未知类型处理:遇到未知 MIME 类型时,应该用
application/octet-stream兜底
4.3 Intent 过滤器配置注意
- action 必须匹配:如果过滤器声明了
action.system.send,那么只有 action 为send的 Intent 才能匹配 - type 必须匹配:过滤器声明的 MIME 类型必须与 Intent 中的 type 一致(或通配匹配)
- scheme 必须匹配:如果过滤器声明了
filescheme,那么只有file://开头的 URI 才能匹配 - categories 可选匹配:Intent 中的所有 category 必须在过滤器中有对应的声明
4.4 跨应用调用注意事项
- 结果码判断:
startAbilityForResult返回的resultCode为 0 表示成功,-1 表示用户取消 - 超时处理:跨应用调用可能因为目标应用无响应而卡住,建议设置超时
- 异常兜底:目标应用可能未安装或已禁用,需要 try-catch 并给出友好提示
- 数据验证:接收到的 URI 和数据要做有效性验证,不要盲目信任
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| Want 参数传递 | parameters |
新增 structuredParameters 结构化参数 |
| 文件分享 | 基于 URI | 新增 ShareData 数据对象,支持流式传输 |
| Intent 过滤器 | 静态配置 | 支持动态注册/注销过滤器 |
| 跨应用调用 | startAbilityForResult |
新增 startAbilityForResult Promise 化 API |
5.2 迁移指南
// HarmonyOS 5 写法
const want: Want = {
action: WantConstant.Action.ACTION_SEND,
type: 'image/jpeg',
parameters: {
'ability.params.stream': [imageUri]
}
};
// HarmonyOS 6 写法(结构化参数)
const want: Want = {
action: WantConstant.Action.ACTION_SEND,
type: 'image/jpeg',
structuredParameters: {
mediaStreams: [{
uri: imageUri,
mimeType: 'image/jpeg',
size: 1024000,
name: 'photo.jpg'
}],
shareContext: {
sourceApp: 'com.example.app',
timestamp: Date.now()
}
}
};
5.3 新特性:动态 Intent 过滤器
HarmonyOS 6 支持在运行时动态注册和注销 Intent 过滤器,让应用可以根据当前状态决定是否接收特定类型的 Intent:
// HarmonyOS 6 新增:动态 Intent 过滤器
import { intentFilter } from '@kit.AbilityKit';
// 动态注册过滤器(当应用准备好接收媒体时)
intentFilter.register({
actions: ['action.system.send'],
mimeTypes: ['video/mp4'],
schemes: ['file', 'content']
});
// 动态注销过滤器(当应用不再接收媒体时)
intentFilter.unregister({
actions: ['action.system.send'],
mimeTypes: ['video/mp4']
});
六、总结
mindmap
root((媒体Intent))
分享Intent
ACTION_SEND 单文件
ACTION_SEND_MULTIPLE 多文件
FLAG_AUTH_READ_URI_PERMISSION
附加文本描述
打开媒体文件
ACTION_VIEW 查看动作
MIME 类型推断
显式/隐式调用
URI 权限授予
类型关联
module.json5 过滤器
action 匹配
type 匹配
scheme 匹配
Intent过滤器
静态配置
动态注册(HarmoneyOS 6)
精确/通配匹配
category 可选匹配
跨应用调用
startAbilityForResult
相册选择
相机拍照/录像
录音/编辑
HarmonyOS 6
结构化参数
ShareData 对象
动态过滤器
Promise 化 API
关键知识点回顾:
- Intent 是应用间通信的桥梁:Want 对象描述"想做什么",系统负责找到合适的目标
- 分享用 SEND,查看用 VIEW:这两个是最常用的媒体 Intent Action
- URI 权限是第一坑:忘记加
FLAG_AUTH_READ_URI_PERMISSION是最常见的错误 - MIME 类型要准确:分享时用精确类型,接收时用通配类型,这是最佳实践
- 过滤器配置要完整:action、type、scheme 三要素缺一不可
- 跨应用调用要健壮:异常处理、超时机制、数据验证一个都不能少
一句话总结:媒体 Intent 的本质是"让媒体数据在应用之间流动起来",理解 Want 的构造和过滤器的匹配规则,你就掌握了这条数据管道的控制权。
- 点赞
- 收藏
- 关注作者
评论(0)