掌握 HarmonyOS APP媒体 Intent 机制玩法

举报
Jack20 发表于 2026/06/21 11:48:35 2026/06/21
【摘要】 一、背景与动机你有没有想过,当你在相册里长按一张照片选择"分享"时,系统是怎么知道哪些 App 可以接收这张图片的?当你在微信里点开一个视频链接时,系统又是怎么决定用哪个播放器来打开的?这些看似"理所当然"的操作,背后靠的就是 Intent 机制。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/jpegvideo/mp4audio/mpeg
uri 数据 URI file:///photos/img.jpgcontent://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 类型匹配

  1. 精确匹配 vs 通配匹配image/jpeg 只匹配 JPEG,image/* 匹配所有图片类型。分享时建议用精确类型,接收时建议用通配类型
  2. 大小写敏感:MIME 类型是大小写不敏感的,但实际开发中建议统一用小写
  3. 未知类型处理:遇到未知 MIME 类型时,应该用 application/octet-stream 兜底

4.3 Intent 过滤器配置注意

  • action 必须匹配:如果过滤器声明了 action.system.send,那么只有 action 为 send 的 Intent 才能匹配
  • type 必须匹配:过滤器声明的 MIME 类型必须与 Intent 中的 type 一致(或通配匹配)
  • scheme 必须匹配:如果过滤器声明了 file scheme,那么只有 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

关键知识点回顾

  1. Intent 是应用间通信的桥梁:Want 对象描述"想做什么",系统负责找到合适的目标
  2. 分享用 SEND,查看用 VIEW:这两个是最常用的媒体 Intent Action
  3. URI 权限是第一坑:忘记加 FLAG_AUTH_READ_URI_PERMISSION 是最常见的错误
  4. MIME 类型要准确:分享时用精确类型,接收时用通配类型,这是最佳实践
  5. 过滤器配置要完整:action、type、scheme 三要素缺一不可
  6. 跨应用调用要健壮:异常处理、超时机制、数据验证一个都不能少

一句话总结:媒体 Intent 的本质是"让媒体数据在应用之间流动起来",理解 Want 的构造和过滤器的匹配规则,你就掌握了这条数据管道的控制权。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。