【鸿蒙开发】第二十九章 Stage模型-应用间跳转

目录

1 应用场景

2 拉起指定应用

2.1 应用链接

2.2 运作机制

2.3 应用链接分类

 2.4 使用canOpenLink判断应用是否可访问

2.4.1 使用场景

2.4.2 约束限制

2.4.3 接口说明

2.4.4 操作步骤

2.4.4.1 调用方操作步骤

2.4.4.2 目标方操作步骤

2.5 使用Deep Linking实现应用间跳转

2.5.1 实现原理

2.5.2 目标应用操作指导

2.5.2.1 配置module.json5文件

2.5.2.2 获取并解析拉起方传入的应用链接

2.5.3 拉起方应用实现应用跳转

2.5.3.1 使用openLink实现应用跳转

2.5.3.2 使用startAbility实现应用跳转

2.5.3.3 使用Web组件实现应用跳转

2.6 使用App Linking实现应用间跳转

2.6.1 简介

2.6.2 适用场景

2.6.3 实现原理

2.6.4 开发指导概述

2.6.5 被拉起应用开发指导

2.6.5.1 在AGC控制台开通App Linking服务

2.6.5.2 在开发者网站上关联应用

2.6.5.3 配置网址域名

2.6.5.4 在DevEco Studio的具体操作如下。

 2.6.5.5 处理传入的链接

2.6.6 验证应用被拉起效果

2.6.7 拉起方实现跳转指导

2.7 显式Want跳转切换应用链接跳转适配指导 

2.7.1 启动其他应用的UIAbility

2.7.2 启动其他应用的UIAbility并获取返回结果

2.8 应用链接说明

2.8.1 uris标签说明

2.8.2 URL的基本格式

2.8.3 linkFeature标签说明

2.8.4 配置示例

3 拉起指定类型的应用

3.1 概述

3.2 通过startAbilityByType接口拉起垂类面板

3.2.1 实现机制

3.2.2 匹配规则

3.3 拉起导航类应用(startAbilityByType)

3.3.1 导航类应用扩展面板参数说明

3.3.2 拉起方开发步骤

3.3.3 目标方开发步骤

 3.4 拉起邮件类应用(startAbilityByType)

3.4.1 邮件类应用扩展面板参数说明

 3.4.2 拉起方开发步骤

3.4.3 目标方开发步骤

3.5 拉起邮件类应用(mailto方式)

3.5.1 使用场景

3.5.2 mailto协议格式

3.5.3 拉起方开发步骤 

3.5.3.1 从网页拉起

3.5.3.2 从应用拉起

3.5.4 目标方开发步骤

3.6 拉起金融类应用(startAbilityByType)

3.6.1 金融类应用扩展面板参数说明

3.6.2 拉起方开发步骤

3.6.3 目标方开发步骤

3.7 拉起图片编辑类应用(startAbilityByType)

3.7.1 使用场景

3.7.2 接口说明

3.7.3 图片编辑类应用实现图片编辑页面

3.7.4 调用方拉起图片编辑类应用编辑图片

3.8 拉起文件处理类应用(startAbility)

3.8.1 使用场景

3.8.2 接口关键参数说明

3.8.3 接入步骤

3.8.3.1 调用方接入步骤

3.8.3.2 目标方接入步骤

4 拉起系统应用

4.1 拉起系统应用的方式

4.2 支持跳转系统应用的能力清单

设置

应用市场

钱包

电话

日历

联系人

地图

相机

文件管理

图库(媒体库)


应用跳转是指从一个应用跳转至另外一个应用,传递相应的数据、执行特定的功能。通过应用跳转可以满足用户更为真实丰富的场景诉求、提升交互体验的便捷性和流畅性。

1 应用场景

应用间跳转在社交分享、推广营销等场景广泛使用。举例如下:

  • 社交分享 在社交软件中分享位置链接、美食推荐链接、购物链接、游戏链接等,可以通过该链接快速跳转到匹配的导航App、美食App、购物App、游戏App等应用。
  • 推广营销 在视频类App、社交类App、浏览器App等广告投放平台中,嵌入需要推广的应用链接(该链接可能以文本、卡片、视频等形式呈现),通过该链接信息可以跳转到目标应用的指定页面;也可以在推送短信、发送邮件时,在正文中携带需要推广的应用链接,通过该链接信息可以跳转到目标应用的指定页面。

2 拉起指定应用

说明

从API 12开始,已不再推荐三方应用使用指定Ability方式(即显式Want)拉起其他应用。关于如何从指定Ability方式切换到指定应用链接方式,详见显式Want跳转切换应用链接跳转适配指导

2.1 应用链接

应用链接是指可以将用户引导至应用内特定位置或相关网页的URL,常见的格式如下。更多关于应用链接格式与字段含义的说明,详见应用链接说明

scheme://host[:port]/path

2.2 运作机制

  1. 目标应用在配置文件中注册自己的URL,并对外提供URL。
  2. 拉起方应用在跳转接口中传入目标应用的URL等信息。
  3. 系统接收到URL等相关信息,会寻找对应匹配项,并跳转至目标应用。

2.3 应用链接分类

按照应用链接的scheme以及校验机制的不同,可以分为Deep LinkingApp Linking两种方式。

  • Deep Linking:是一种通过链接跳转至应用特定页面的技术,其特点是支持开发者定义任意形式的scheme。由于缺乏域名校验机制,容易被其他应用所仿冒。
  • App Linking:其限定了scheme必须为https,同时通过增加域名校验机制,可以从已匹配到的应用中筛选过滤出目标应用,消除应用查询和定位中产生的歧义,直达受信的目标应用。

相比Deep Linking,App Linking具有更高的安全性可靠性,用户体验更佳。推荐开发者将App Linking作为首选方案。

类型App Linking(推荐)Deep Linking
实现方案目标应用需要在module.json5中声明应用链接;同时需要向系统注册域名并通过域名认证。目标应用需要在module.json5中声明应用链接。
链接格式scheme必须为https。scheme可以自定义。通常不为https、http、file,否则会拉起默认的系统浏览器。
是否可用于分享或直接在网页中访问可以不可以,需在代码中调用。
是否可以直接拉起目标应用可以可以,但不推荐使用,存在被仿冒风险。

Deep Linking与App Linking均可以使用openLink接口实现,不同条件下的跳转效果如下。

说明

该接口中的appLinkingOnly字段表示是否必须以App Linking的方式启动UIAbility,默认为false。appLinkingOnly为true一般只用于浏览器等应用。

应用链接类型App Linking(推荐)Deep Linking
appLinkingOnly为false且目标应用已安装直接跳转打开目标应用。跳转目标应用(如果有多个符合条件的应用时,展示应用选择弹框)。
appLinkingOnly为false且目标应用未安装跳转默认浏览器打开网页。返回失败,系统不跳转,由应用自行处理;当前会展示“链接无法打开”弹框。
appLinkingOnly为true且目标应用已安装直接跳转打开目标应用。返回失败,系统不跳转,由应用自行处理。
appLinkingOnly为true且目标应用未安装返回失败,系统不跳转由应用自行处理。返回失败,系统不跳转,由应用自行处理。

 2.4 使用canOpenLink判断应用是否可访问

2.4.1 使用场景

在应用A想要拉起应用B的场景中,应用A可先调用canOpenLink接口判断应用B是否可访问,如果可访问,再拉起应用B。

2.4.2 约束限制

entry模块的module.json5文件中的querySchemes字段中,最多允许配置50个URL scheme

2.4.3 接口说明

canOpenLink是bundleManager提供的支持判断目标应用是否可访问的接口

匹配规则请参考显式Want与隐式Want匹配规则

2.4.4 操作步骤

2.4.4.1 调用方操作步骤

1. 在entry模块的module.json5文件中配置querySchemes属性,声明想要查询的URL scheme

{
  "module": {
    //...
    "querySchemes": [
      "app1Scheme"
    ]
  }
}

2. 导入ohos.bundle.bundleManager模块。

3. 调用canOpenLink接口。

import { bundleManager } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
try {
  let link = 'app1Scheme://test.example.com/home';
  let canOpen = bundleManager.canOpenLink(link);
  hilog.info(0x0000, 'testTag', 'canOpenLink successfully: %{public}s', JSON.stringify(canOpen));
} catch (err) {
  let message = (err as BusinessError).message;
  hilog.error(0x0000, 'testTag', 'canOpenLink failed: %{public}s', message);
}
2.4.4.2 目标方操作步骤

module.json5文件中配置uris属性。

{
  "module": {
    //...
    "abilities": [
      {
        //...
        "skills": [
          {
            "uris": [
              {
                "scheme": "app1Scheme",
                "host": "test.example.com",
                "pathStartWith": "home"
              }
            ]
          }
        ]
      }
    ]
  } 
}

2.5 使用Deep Linking实现应用间跳转

采用Deep Linking进行跳转时,系统会根据接口中传入的uri信息,在本地已安装的应用中寻找到符合条件的应用并进行拉起。当匹配到多个应用时,会拉起应用选择框。

2.5.1 实现原理

Deep Linking基于隐式Want匹配机制中的uri匹配来查询、拉起目标应用。隐式Want的uri匹配规则详见uri匹配规则

2.5.2 目标应用操作指导

2.5.2.1 配置module.json5文件

为了能够支持被其他应用访问,目标应用需要在module.json5配置文件中配置skills标签

说明

skills标签下默认包含一个skill对象,用于标识应用入口。应用跳转链接不能在该skill对象中配置,需要创建独立的skill对象。如果存在多个跳转场景,需要在skills标签下创建不同的skill对象,否则会导致配置无法生效。

Deep Linking中的scheme取值支持自定义,可以为任意不包含特殊字符、非ohos开头的字符串。通常不为https、http、file,否则会拉起默认的系统浏览器。

配置示例如下:

{
  "module": {
    // ...
    "abilities": [
      {
        // ...
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          },
          {
            "actions": [
              // actions不能为空,actions为空会造成目标方匹配失败。
              "ohos.want.action.viewData"
            ],
            "uris": [
              {
                // scheme必选,可以自定义,以link为例,需要替换为实际的scheme
                "scheme": "link",
                // host必选,配置待匹配的域名
                "host": "www.example.com"
              }
            ]
          } // 新增一个skill对象,用于跳转场景。如果存在多个跳转场景,需配置多个skill对象。
        ]
      }
    ]
  }
}
2.5.2.2 获取并解析拉起方传入的应用链接

在目标应用的UIAbility的onCreate()或者onNewWant()生命周期回调中,获取、解析拉起方传入的应用链接。

// 以EntryAbility.ets为例
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { url } from '@kit.ArkTS';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 从want中获取传入的链接信息。
    // 如传入的url为:link://www.example.com/programs?action=showall
    let uri = want?.uri;
    if (uri) {
      // 从链接中解析query参数,拿到参数后,开发者可根据自己的业务需求进行后续的处理。
      let urlObject = url.URL.parseURL(want?.uri);
      let action = urlObject.params.get('action');
      // 例如,当action为showall时,展示所有的节目。
      if (action === "showall") {
         // ...
      }
    }
  }
}

2.5.3 拉起方应用实现应用跳转

下面通过三个案例,分别介绍如何使用openLink()startAbility()接口实现应用跳转,以及如何在Web组件中实现应用跳转。

2.5.3.1 使用openLink实现应用跳转

openLink接口的link字段中传入目标应用的URL信息,并将options字段中的appLinkingOnly配置为false。

示例代码如下:

import { common, OpenLinkOptions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = '[UIAbilityComponentsOpenLink]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Index {
  build() {
    Button('start link', { type: ButtonType.Capsule, stateEffect: true })
      .width('87%')
      .height('5%')
      .margin({ bottom: '12vp' })
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
        let link: string = "link://www.example.com";
        let openLinkOptions: OpenLinkOptions = {
          appLinkingOnly: false
        };

        try {
          context.openLink(link, openLinkOptions)
            .then(() => {
              hilog.info(DOMAIN_NUMBER, TAG, 'open link success.');
            }).catch((err: BusinessError) => {
              hilog.error(DOMAIN_NUMBER, TAG, `open link failed. Code is ${err.code}, message is ${err.message}`);
            });
        } catch (paramError) {
          hilog.error(DOMAIN_NUMBER, TAG, `Failed to start link. Code is ${paramError.code}, message is ${paramError.message}`);
        }
      })
  }
}
2.5.3.2 使用startAbility实现应用跳转

startAbility接口是将应用链接放入want中,通过调用隐式want匹配的方法触发应用跳转。通过startAbility接口启动时,还需要调用方传入待匹配的action和entity。

示例代码如下:

import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = '[UIAbilityComponentsOpenLink]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Index {
  build() {
    Button('start ability', { type: ButtonType.Capsule, stateEffect: true })
      .width('87%')
      .height('5%')
      .margin({ bottom: '12vp' })
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
        let want: Want = {
            uri: "link://www.example.com"
        };

        try {
          context.startAbility(want).then(() => {
            hilog.info(DOMAIN_NUMBER, TAG, 'start ability success.');
          }).catch((err: BusinessError) => {
            hilog.error(DOMAIN_NUMBER, TAG, `start ability failed. Code is ${err.code}, message is ${err.message}`);
          });
        } catch (paramError) {
          hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability. Code is ${paramError.code}, message is ${paramError.message}`);
        }
      })
  }
}
2.5.3.3 使用Web组件实现应用跳转

Web组件需要跳转DeepLink链接应用时,可通过拦截回调onLoadIntercept中对定义的事件进行处理,实现应用跳转

示例代码如下:

// index.ets
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .onLoadIntercept((event) => {
          const url: string = event.data.getRequestUrl();
          if (url === 'link://www.example.com') {
            (getContext() as common.UIAbilityContext).openLink(url)
              .then(() => {
                console.log('openLink success');
              }).catch((err: BusinessError) => {
                console.error('openLink failed, err:' + JSON.stringify(err));
              });
            return true;
          }
          // 返回true表示阻止此次加载,否则允许此次加载
          return false;
        })
    }
  }
}

前端页面代码:

// index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
<h1>Hello World</h1>
<!--方式一、通过绑定事件window.open方法实现跳转-->
<button class="doOpenLink" onclick="doOpenLink()">跳转其他应用一</button>
<!--方式二、通过超链接实现跳转-->
<a href="link://www.example.com">跳转其他应用二</a>
</body>
</html>
<script>
    function doOpenLink() {
        window.open("link://www.example.com")
    }
</script>

2.6 使用App Linking实现应用间跳转

2.6.1 简介

使用App Linking进行跳转时,系统会根据接口传入的uri信息(HTTPS链接)将用户引导至目标应用中的特定内容,无论应用是否已安装,用户都可以访问到链接对应的内容,跳转体验相比Deep Linking方式更加顺畅。

例如:当开发者使用App Linking接入“扫码直达”服务后,用户可通过控制中心扫一扫等系统级扫码入口,扫描应用的二维码、条形码并跳转到开发者应用对应服务页,实现一步直达的体验。

2.6.2 适用场景

  • 适用于应用的扫码直达、社交分享、沉默唤醒、广告引流等场景。
  • 适用于对安全性要求较高的场景,避免出现被其它应用仿冒的问题。
  • 适用于对体验要求较高的应用,不管目标应用是否安装,用户点击该链接都可以正常访问。

2.6.3 实现原理

  • App LinkingDeep Linking基础上增加了域名校验环节,通过域名校验,可帮助用户消除歧义,识别合法归属于域名的应用,使链接更加安全可靠。
  • App Linking要求对于同一HTTPS网址,有应用和网页两种内容的呈现方式。当应用安装时则优先打开应用去呈现内容;当应用未安装时,则打开浏览器呈现Web版的内容。

2.6.4 开发指导概述

若要实现App Linking跳转体验,需被拉起方和拉起方的不同角色相互配合,共同完成。

各个角色的分工如下。

2.6.5 被拉起应用开发指导

2.6.5.1 在AGC控制台开通App Linking服务

请先参考“应用开发准备”完成基本准备工作,再继续进行以下开发活动。

  1. 登录AppGallery Connect,点击“我的项目”。
  2. 在项目列表中点击您的项目。
  3. 在左侧导航栏中选择“增长 > App Linking”,进入App Linking页面,点击“立即开通”。

  4. 如果您的项目此时未设置数据处理位置,请在提示框内启用数据处理位置和设置默认数据处理位置,点击“确定”。

  5. 进入“项目设置 > 常规”页面,选择创建的HarmonyOS应用,查看应用的APP ID,后续开发需要使用该ID。

2.6.5.2 在开发者网站上关联应用

在开发者的网站域名服务器上做如下配置。后续当您配置该网站域名时,系统会通过此文件确认哪些应用才是合法归属于此域名的,使链接更加安全可靠。

1. 创建域名配置文件applinking.json,内容如下:

{
 "applinking": {
   "apps": [
     {
       "appIdentifier": "1234567"
     }
   ]
 }
}

说明

  • appIdentifier填写创建应用时生成的APP ID
  • 同一个网站域名可以关联多个应用,只需要在"apps"列表里放置多个"appIdentifier"元素即可,其中每个"appIdentifier"元素对应每个应用。

2. 将配置文件放在域名服务器的固定目录下:

https://domain.name/.well-known/applinking.json

例如:开发者的服务器域名为www.example.com,则必须将applinking.json文件放在如下位置:

https://www.example.com/.well-known/applinking.json

2.6.5.3 配置网址域名

基于HarmonyOS应用链接能力,需要为HarmonyOS应用创建关联的网址域名。如果用户已安装HarmonyOS应用,则用户点击域名下网址链接后,系统会默认打开该HarmonyOS应用内的相关页面。

在AGC控制台的具体操作如下。

  1. 登录AppGallery Connect,点击“我的项目”。
  2. 在项目列表中点击您的项目。
  3. 在左侧导航栏中选择“增长 > App Linking”,选择“应用链接(API>=12适用)”页签,点击“创建”。

    说明

    • HarmonyOS原生应用开发者仅需关注“应用链接(API>=12适用)”页签,其他页签为元服务或其他系统适用的配置,无需关注。
    • 如果界面未展示“应用链接(API>=12适用)”页签,请在右侧的“自定义配置”中勾选。

  4. 填写HarmonyOS应用关联的网址域名,即创建域名配置文件的网址,例如:https://www.example.com。必须输入精确的域名,不可输入包含特殊字符的模糊网址。

    说明

    不可以在域名后面添加/,即不支持“https://www.example.com/”形式。

  5. 设置完成后点击“发布”,AGC会对该网站域名的配置文件所包含的应用与本项目内的应用列表进行交集校验。

    说明

    应用链接发布成功后,如果距离上次更新超过24小时,系统会去域名服务器上重新获取配置文件进行交集校验。

    例如:4月7日17:21创建了应用链接,系统会在4月8日17:30去域名服务器上重新获取配置文件,然后进行交集校验,更新发布状态。

    • 如果域名的配置文件中有应用存在本项目中,则发布成功,点击“查看”可显示该域名关联的应用信息。

    • 如果异步校验中,则状态为“发布中”。
    • 如果配置文件中没有任何应用在本项目中,则发布失败,点击“查看”可显示发布失败原因。

2.6.5.4 在DevEco Studio的具体操作如下。

在应用的module.json5文件中进行如下配置,以声明应用关联的域名地址,并开启域名校验开关。

  • "entities"列表中必须包含"entity.system.browsable"。
  • "actions"列表中必须包含"ohos.want.action.viewData"。
  • "uris"列表中必须包含"scheme"为"https"且"host"为域名地址的元素,可选属性包含"path"、"pathStartWith"和"pathRegex",具体请参见“uris标签说明”。
  • "domainVerify"设置为true,表示开启域名校验开关。

说明

skills标签下默认包含一个skill对象,用于标识应用入口。应用跳转链接不能在该skill对象中配置,需要创建独立的skill对象。

如果存在多个跳转场景,需要在skills标签下创建不同的skill对象,否则会导致配置无法生效。

例如,声明应用关联的域名是www.example.com,则需进行如下配置。

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ts",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        // 请将exported配置为true;如果exported为false,仅具有权限的系统应用能够拉起该应用,否则无法拉起应用
        "exported": true,
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          },
          {
            "entities": [
              // entities必须包含"entity.system.browsable"
              "entity.system.browsable"
            ],
            "actions": [
              // actions必须包含"ohos.want.action.viewData"
              "ohos.want.action.viewData"
            ],
            "uris": [
              {
                // scheme须配置为https
                "scheme": "https",
                // host须配置为关联的域名
                "host": "www.example.com",
                // path可选,表示域名服务器上的目录或文件路径,例如www.example.com/path1/中的path1
                // 如果应用只能处理部分特定的path,则此处应该配置应用所支持的path,避免出现应用不能处理的path链接也被引流到应用中的问题
                "path": "path1"
              }
            ],
            // domainVerify须设置为true
           "domainVerify": true
          }
          // 若有其他跳转能力,如推送消息跳转、NFC跳转,可新增一个skill对象,防止与App Linking业务冲突
        ]
      }
    ]
  }
}
 2.6.5.5 处理传入的链接

在应用的Ability(如EntryAbility)的onCreate()或者onNewWant()生命周期回调中添加如下代码,以处理传入的链接。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { url } from '@kit.ArkTS';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 从want中获取传入的链接信息。
    // 如传入的url为:https://www.example.com/programs?action=showall
    let uri = want?.uri 
    if (uri) {
      // 从链接中解析query参数,拿到参数后,开发者可根据自己的业务需求进行后续的处理。
      let urlObject = url.URL.parseURL(want?.uri);
      let action = urlObject.params.get('action')
      // 例如,当action为showall时,展示所有的节目。
      if (action === "showall"){
         //...
      }
      //...
    }
  }
}

2.6.6 验证应用被拉起效果

  1. 对应用进行手动签名

    说明

    不能使用DevEco Studio的自动签名功能,必须使用手动签名,否则无法拉起应用。

  2. 编译打包,并安装应用至调试设备。
  3. 在拉起方应用中通过App Linking拉起此应用,详细请参考“拉起方实现跳转指导”。
  4. 查看集成效果,以“扫码直达”服务的美团单车场景为例:

2.6.7 拉起方实现跳转指导

支持App Linking的应用可以通过如下方式被拉起:

  1. 通过openLink接口拉起。

    拉起方应用通过UIAbilityContext.openLink()接口,传入目标应用的链接,拉起目标应用。

    openLink接口提供了两种拉起目标应用的方式,开发者可根据业务需求进行选择。

    本文为了方便验证App Linking的配置是否正确,选择方式一,示例如下。

    • 方式一: 仅以App Linking的方式打开应用。

      将appLinkingOnly参数设为true,若有匹配的应用,则直接打开目标应用。若无App Linking匹配的应用,则抛异常给开发者进行处理。

    • 方式二: 以App Linking优先的方式打开应用。

      将appLinkingOnly参数设为false或者默认,则为App Linking优先的方式打开应用。若有App Linking匹配的应用,则直接打开目标应用。若无App Linking匹配的应用,则尝试以浏览器打开链接的方式打开应用。

import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  build() {
    Button('start link', { type: ButtonType.Capsule, stateEffect: true })
      .width('87%')
      .height('5%')
      .margin({ bottom: '12vp' })
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
        let link: string = "https://www.example.com/programs?action=showall";
        // 仅以App Linking的方式打开应用
        context.openLink(link, { appLinkingOnly: true })
          .then(() => {
            console.info('openlink success.');
          })
          .catch((error: BusinessError) => {
            console.error(`openlink failed. error:${JSON.stringify(error)}`);
          });
      })
  }
}
  1. 在拉起方应用中执行上述代码,如果拉起方应用成功拉起目标应用,则成功配置App Linking。

  2. 通过系统浏览器或ArkWeb拉起。ArkWeb深度集成了App Linking的能力,当用户在系统浏览器或者集成ArkWeb的应用的网页上点击某个链接时,若有链接匹配的应用,系统则会通过App Linking能力优先拉起目标应用,并在应用内展示相应的内容。此机制有如下限制:
    1. 如果用户当前浏览的网页的域名与点击的App Linking链接的域名完全一致,则系统会继续在系统浏览器或ArkWeb中打开该链接,以维持连贯的用户浏览体验。
    2. 如果域名不完全一致(例如example.com和app.example.com),则系统会通过App Linking能力优先拉起目标应用,并在应用内展示相应的内容。

2.7 显式Want跳转切换应用链接跳转适配指导 

2.7.1 启动其他应用的UIAbility

  1. 将待跳转的应用安装到设备,在其对应UIAbility的module.json5配置文件中配置skills标签的entities字段、actions字段和uri字段:

    • "actions"列表中包含"ohos.want.action.viewData"。
    • "entities"列表中包含"entity.system.browsable"。
    • "uris"列表中包含"scheme"为"https"且"domainVerify"为true的元素。uri的匹配规则参考uri匹配, domainVerify为true代表开启域名检查,通过applinking匹配该应用时需经过配置的域名校验后才能匹配到。applinking域名配置具体可参考AppLinking。
{
  "module": {
    // ...
    "abilities": [
      {
        // ...
        "skills": [
          {
            "entities": [
              "entity.system.browsable"
            ],
            "actions": [
              "ohos.want.action.viewData"
            ],
            "uris": [
              {
                "scheme": "https",
                "host": "www.example.com",
              }
            ],
          "domainVerify": true
          }
        ]
      }
    ]
  }
}

 2. 调用方通过openLink接口执行跳转,在接口入参需要传入转换后的link和配置options, 不再传入bundleName、moduleName和abilityName。系统会根据传入的link匹配到符合skill配置的应用。

  • 当options中的appLinkingOnly为true时,匹配到的应用会经过应用市场域名检查(需联网)返回域名校验检查的唯一匹配项或未匹配结果。
  • 当options中的appLinkingOnly为false时,会优先尝试以AppLinking的方式拉起,如果没有匹配的应用则改为使用DeepLinking的方式拉起目标应用。
import { common } from '@kit.AbilityKit';
import OpenLinkOptions from '@ohos.app.ability.OpenLinkOptions';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';

const TAG: string = '[UIAbilityComponentsOpenLink]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Index {
  build() {
    Button('start link', { type: ButtonType.Capsule, stateEffect: true })
      .width('87%')
      .height('5%')
      .margin({ bottom: '12vp' })
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
        // 通过startAbility接口显式启动其他UIAbility,推荐使用openLink接口。
        // let want: Want = {
        //   bundleName: "com.test.example",
        //   moduleName: "entry",
        //   abilityName: "EntryAbility"
        // };
        // try {
        //   context.startAbility(want)
        //     .then(() => {
        //       hilog.info(DOMAIN_NUMBER, TAG, 'startAbility success.');
        //     }).catch((err: BusinessError) => {
        //       hilog.error(DOMAIN_NUMBER, TAG, `startAbility failed. Code is ${err.code}, message is ${err.message}`);
        //     })
        // } catch (paramError) {
        //   hilog.error(DOMAIN_NUMBER, TAG, `Failed to startAbility. Code is ${paramError.code}, message is ${paramError.message}`);
        // }
        let link: string = "https://www.example.com";
        let openLinkOptions: OpenLinkOptions = {
          // 匹配的abilities选项是否需要通过AppLinking域名校验,匹配到唯一配置过的应用ability
          appLinkingOnly: true,
          // 同want中的parameter,用于传递的参数
          parameters: {demo_key: "demo_value"}
        };

        try {
          context.openLink(link, openLinkOptions)
            .then(() => {
              hilog.info(DOMAIN_NUMBER, TAG, 'open link success.');
            }).catch((err: BusinessError) => {
              hilog.error(DOMAIN_NUMBER, TAG, `open link failed. Code is ${err.code}, message is ${err.message}`);
            })
        } catch (paramError) {
          hilog.error(DOMAIN_NUMBER, TAG, `Failed to start link. Code is ${paramError.code}, message is ${paramError.message}`);
        }
      })
  }
}

2.7.2 启动其他应用的UIAbility并获取返回结果

  1. 将待跳转的应用安装到设备,在其对应UIAbility的module.json5配置文件中配置skills标签的entities字段、actions字段和uri字段:

    • "actions"列表中包含"ohos.want.action.viewData"。
    • "entities"列表中包含"entity.system.browsable"。
    • "uris"列表中包含"scheme"为"https"且"domainVerify"为true的元素。uri的匹配规则参考uri匹配, domainVerify为true代表开启域名检查,通过applinking匹配该应用时需经过配置的域名校验后才能匹配到。applinking域名配置具体可参考App Linking。
{
  "module": {
    // ...
    "abilities": [
      {
        // ...
        "skills": [
          {
            "entities": [
              "entity.system.browsable"
            ],
            "actions": [
              "ohos.want.action.viewData"
            ],
            "uris": [
              {
                "scheme": "https",
                "host": "www.example.com",
              }
            ],
          "domainVerify": true
          }
        ]
      }
    ]
  }
}

2. 调用方通过openLink接口执行跳转,在接口入参需要传入转换后的link和配置options, 不再传入bundleName、moduleName和abilityName。系统会根据传入的link匹配到符合skills配置的应用。AbilityResult回调结果返回通过入参传入回调函数,在启动ability停止自身后返回给调用方的信息。启动成功和失败结果仍通过Promise返回。

  • 当options中的appLinkingOnly为true时,匹配到的应用会经过应用市场域名检查(需联网)返回域名校验检查的唯一匹配项或未匹配结果。
  • 当options中的appLinkingOnly为false时,会优先尝试以AppLinking的方式拉起,如果没有匹配的应用则改为使用DeepLinking的方式拉起目标应用。
import { common } from '@kit.AbilityKit';
import OpenLinkOptions from '@ohos.app.ability.OpenLinkOptions';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';

const TAG: string = '[UIAbilityComponentsOpenLink]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Index {
  build() {
    Button('start link', { type: ButtonType.Capsule, stateEffect: true })
      .width('87%')
      .height('5%')
      .margin({ bottom: '12vp' })
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
        // 通过startAbility接口显式启动其他UIAbility,推荐使用openLink接口。
        // let want: Want = {
        //   bundleName: "com.test.example",
        //   moduleName: "entry",
        //   abilityName: "EntryAbility"
        // };
        // try {
        //   context.startAbilityForResult(want)
        //     .then((data) => {
        //       hilog.info(DOMAIN_NUMBER, TAG, 'startAbility success. data:' + JSON.stringify(data));
        //     }).catch((err: BusinessError) => {
        //       hilog.error(DOMAIN_NUMBER, TAG, `startAbility failed. Code is ${err.code}, message is ${err.message}`);
        //     })
        // } catch (paramError) {
        //   hilog.error(DOMAIN_NUMBER, TAG, `Failed to startAbility. Code is ${paramError.code}, message is ${paramError.message}`);
        // }
        let link: string = "https://www.example.com";
        let openLinkOptions: OpenLinkOptions = {
          // 匹配的abilities选项是否需要通过AppLinking域名校验,匹配到唯一配置过的应用ability
          appLinkingOnly: true,
          // 同want中的parameter,用于传递的参数
          parameters: {demo_key: "demo_value"}
        };

        try {
          context.openLink(link, openLinkOptions, (err, data) => {
            // AbilityResult callback回调,仅在被拉起ability死亡时触发
            hilog.info(DOMAIN_NUMBER, TAG, 'open link success. Callback result:' + JSON.stringify(data));
          }).then(() => {
            hilog.info(DOMAIN_NUMBER, TAG, 'open link success.');
          }).catch((err: BusinessError) => {
            hilog.error(DOMAIN_NUMBER, TAG, `open link failed. Code is ${err.code}, message is ${err.message}`);
          })
        } catch (paramError) {
          hilog.error(DOMAIN_NUMBER, TAG, `Failed to start link. Code is ${paramError.code}, message is ${paramError.message}`);
        }
      })
  }
}

2.8 应用链接说明

2.8.1 uris标签说明

当在module.json5文件的skills字段中声明uris时,主要包含如下字段。

说明

  • 通过浏览器拉起应用页面时,浏览器会将uri中scheme和host中的大写字母自动转化为小写字母,导致无法正确匹配应用。因此建议scheme和host中不要包含大写字母。
  • path、pathStartWith、pathRegex的取值前后均不需要加斜杠/。例如对于应用链接https://developer.huawei.com/consumer/cn/support,path字段应配置为consumer/cn/support。
  • scheme:协议名称。常见的取值有http、https、file、ftp等,也可以自定义。

  • host:域名或IP地址。例如域名developer.huawei.com或IP地址127.0.0.1。

  • port:端口号。例如developer.huawei.com:80后面的80即为端口号。

  • path:路径,表示域名服务器上的目录或文件路径,该字段在scheme存在时才有意义。path字段不支持通配符,如果需要使用通配符,可以采用pathRegex字段。

  • pathStartWith:路径前缀,该字段在scheme存在时才有意义,表示域名服务器上的目录或文件路径的前缀,用于前缀匹配。

  • pathRegex:路径正则,该字段在scheme存在时才有意义,表示域名服务器上的目录或文件路径的正则表达式,用于正则匹配。

  • linkFeature:应用的功能类型(如文件打开、分享、导航等)。取值为长度不超过127字节的字符串,不支持中文。

2.8.2 URL的基本格式

按照配置的字段不同,uris可以拼接为不同的URL表达式。其中,scheme为必选字段,其他字段仅当scheme存在时才有意义。

  • 只配置scheme:scheme://
  • 只配置scheme和host:scheme://host
  • 只配置scheme、host和port:scheme://host:port
  • 当配置了path、pathStartWith或pathRegex字段时,组成的表达式如下。

    三方应用组件配置的scheme不能与系统应用重复,否则会导致无法通过该uri拉起三方应用组件。

    • 全路径表达式:scheme://host:port/path
    • 路径前缀表达式:scheme://host:port/pathStartWith
    • 路径正则表达式:scheme://host:port/pathRegex

说明

  • 三方应用组件配置的scheme不能与系统应用重复,否则会导致无法通过该uri拉起三方应用组件。
  • 如果多个应用的URL配置相同,应用跳转时匹配到同多个应用,则会拉起应用选择框。为了更好的用户体验,开发者可以通过链接的path字段去区分同一域名下的不同应用,如链接https://www.example.com/path1拉起目标应用1,链接https://www.example.com/path2拉起目标应用2。

2.8.3 linkFeature标签说明

目标应用在linkFeature字段中申明功能类型,并通过应用市场上架审核后,可以提升应用跳转体验。主要用于以下两种场景:

  1. 支持系统识别同类应用:当调用方拉起垂类应用(例如导航类应用)时,系统会根据linkFeature字段识别到匹配的应用,并在应用面板中展现。

    说明
    AppStorageMgmt指示清理应用沙箱目录中缓存数据的功能
    FileOpen指示打开处理文件的功能
    Navigation指示导航功能
    RoutePlan指示路线规划功能
    PlaceSearch指示地点搜索功能
  2. 跳转一键返回能力:用户从A应用跳转至B应用的某个功能界面后,B应用调用一键返回能力,可以支持用户直接返回A应用,无问询弹窗。例如:A应用跳转至B应用的支付界面,若B应用已申请了支付的linkfeature,则用户在B应用内完成操作后,可一键返回A应用。

    说明
    Login指示登录、授权登录等功能
    Pay指示支付页面、收银台等功能
    Share指示分享功能

2.8.4 配置示例

下面以授权登录场景举例说明:

"uris": [
    {
        "scheme": "https",
        "host": "developer.huawei.com",
        "path": "consumer",
        "linkFeature": "Login"  
    }
]

3 拉起指定类型的应用

3.1 概述

拉起方应用如何通过指定应用类型、而非某个具体的应用,来实现应用跳转。通常有以下几种方式:

  • 通过startAbilityByType接口拉起垂类面板:调用startAbilityByType接口拉起对应的垂域面板(目前支持拉起导航、金融、邮件类应用面板),该面板将展示目标方接入的垂域应用,由用户选择打开指定应用以实现相应的垂类意图。
  • 通过mailto方式跳转电子邮件应用:通过mailto电子邮件协议,可以创建指向电子邮件地址的超链接,方便用户通过网页或应用中的超链接直接跳转电子邮件应用。
  • 通过startAbility接口打开文件:开发者可以通过调用startAbility接口,由系统从已安装的应用中寻找符合要求的应用,打开特定类型的文件。

3.2 通过startAbilityByType接口拉起垂类面板

3.2.1 实现机制

可通过特定的业务类型如导航、金融、邮件等,调用startAbilityByType接口拉起对应的垂域面板,该面板将展示目标方接入的垂域应用,由用户选择打开指定应用以实现相应的垂类意图。

垂域面板为调用方提供统一的安全、可信的目标方应用,同时降低调用方的接入成本。

3.2.2 匹配规则

UIAbilityContext.startAbilityByTypeUIExtensionContentSession.startAbilityByType接口支持基于业务类型拉起垂域面板。调用方通过指定业务类型即可拉起对应的垂域面板,在垂域面板上将展示目标方接入的垂域应用。

系统会根据调用方在startAbilityByType接口传入的type与wantParams.sceneType取值,按照如下映射关系,匹配到在module.json5配置文件中声明了对应linkFeature的目标应用。

支持的功能调用方(startAbilityByType接口入参)目标方(配置文件linkFeature取值)
路线规划功能

- type:navigation

- wantParams.sceneType:1

RoutePlan
导航功能

- type:navigation

- wantParams.sceneType:2

Navigation
位置搜索功能

- type:navigation

- wantParams.sceneType:3

PlaceSearch
转账汇款功能

- type:finance

- wantParams.sceneType:1

Transfer
信用卡还款功能

- type:finance

- wantParams.sceneType:2

CreditCardRepayment
撰写邮件功能

- type:mail

- wantParams.sceneType:1

ComposeMail

3.3 拉起导航类应用(startAbilityByType)

3.3.1 导航类应用扩展面板参数说明

startAbilityByType接口中type字段为navigation支持路线规划、导航、位置搜索三种意图场景,对应的wantParam参数如下:

说明

本文中的经纬度均采用GCJ-02坐标系统。

  • 路线规划场景

    参数名类型必填说明
    sceneTypenumber意图场景,表明本次请求对应的操作意图。默认为1,路线规划场景填1或不填
    originNamestring起点名称
    originLatitudenumber起点纬度
    originLongitudenumber起点经度
    originPoiIdsRecord<number, string>起点POI ID列表,当前仅支持传入花瓣地图和高德地图的POI ID
    destinationNamestring终点名称
    destinationLatitudenumber终点纬度
    destinationLongitudenumber终点经度
    destinationPoiIdsRecord<number, string>终点POI ID列表,当前仅支持传入花瓣地图和高德地图的POI ID
    vehicleTypenumber交通出行工具,取值:0-驾车,1-步行,2-骑行,3-公交;
  • 导航场景

    参数名类型必填说明
    sceneTypenumber意图场景,表明本次请求对应的操作意图。导航场景填2
    destinationNamestring终点名称
    destinationLatitudenumber终点纬度
    destinationLongitudenumber终点经度
    destinationPoiIdsRecord<number, string>终点POI ID列表,当前仅支持传入花瓣地图和高德地图的POI ID
  • 位置搜索场景

    参数名类型必填说明
    sceneTypenumber意图场景,表明本次请求对应的操作意图。位置搜索场景填3
    destinationNamestring地点名称

3.3.2 拉起方开发步骤

1. 导入相关模块。

import { common } from '@kit.AbilityKit';

2. 构造接口参数并调用startAbilityByType接口。

终点POI ID列表(destinationPoiIds)和起点POI ID列表(originPoiIds)需开发者自行从各地图系统中获取,并按照对应关系传参

let context = getContext(this) as common.UIAbilityContext;
let wantParam: Record<string, Object> = {
  'sceneType': 1,
  'destinationLatitude': 32.060844,
  'destinationLongitude': 118.78315,
  'destinationName': 'xx市xx路xx号',
  'destinationPoiIds': {
      1: '1111',  // key为1代表花瓣地图,value需为花瓣地图POI
      2: '2222'   // key为2代表高德地图,value需为高德地图POI
  } as Record<number, string>,
  'originName': 'xx市xx公园',
  'originLatitude': 31.060844,
  'originLongitude': 120.78315,
  'originPoiIds': {
      1: '3333',  // key为1代表花瓣地图,value需为花瓣地图POI
      2: '4444'   // key为2代表高德地图,value需为高德地图POI
  } as Record<number, string>,
  'vehicleType': 0
};
let abilityStartCallback: common.AbilityStartCallback = {
  onError: (code: number, name: string, message: string) => {
    console.log(`onError code ${code} name: ${name} message: ${message}`);
  },
  onResult: (result)=>{
    console.log(`onResult result: ${JSON.stringify(result)}`);
  }
}

context.startAbilityByType("navigation", wantParam, abilityStartCallback, 
    (err) => {
        if (err) {
            console.error(`startAbilityByType fail, err: ${JSON.stringify(err)}`);
        } else {
            console.log(`success`);
        }
});

 效果示例图:

3.3.3 目标方开发步骤

  1. 在module.json5中配置uris,步骤如下:

    1. 设置linkFeature属性以声明当前应用支持的特性功能,从而系统可以从设备已安装应用中找到当前支持该特性的应用,取值范围如下:
      取值含义
      Navigation声明应用支持导航功能
      RoutePlan声明应用支持路线规划功能
      PlaceSearch声明应用支持位置搜索功能
    2. 设置scheme、host、port、path/pathStartWith属性,与Want中URI相匹配,以便区分不同功能。
{
  "abilities": [
      {
      "skills": [
          {
          "uris": [
              {
              "scheme": "maps", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "navigation",
              "path": "",
              "linkFeature": "Navigation" // 声明应用支持导航功能
              },
              {
              "scheme": "maps", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "routePlan",
              "path": "",
              "linkFeature": "RoutePlan" // 声明应用支持路线规划功能
              },
              {
              "scheme": "maps", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "search",
              "path": "",
              "linkFeature": "PlaceSearch" // 声明应用支持位置搜索功能
              }
          ]
          }
      ]
      }
  ]
}

 2. 解析参数并做对应处理。

UIAbility.onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void

在参数want.uri中会携带目标方配置的linkFeature对应的uri。

在参数want.parameters中会携带Caller方传入的参数,不同场景参数如下所示。

  • 路线规划场景

    参数名类型必填说明
    originNamestring起点名称
    originLatitudenumber起点纬度
    originLongitudenumber起点经度
    originPoiIdstring起点POI ID,当前仅支持花瓣地图和高德地图获取此参数
    destinationNamestring终点名称
    destinationLatitudenumber终点纬度
    destinationLongitudenumber终点经度
    destinationPoiIdstring终点POI ID,当前仅支持花瓣地图和高德地图获取此参数
    vehicleTypenumber交通出行工具,取值:0-驾车,1-步行,2-骑行,3-公交;
  • 导航场景

    参数名类型必填说明
    destinationNamestring终点名称
    destinationLatitudenumber终点纬度
    destinationLongitudenumber终点经度
    destinationPoiIdstring终点POI ID,当前仅支持花瓣地图和高德地图获取此参数
  • 位置搜索场景

    参数名类型必填说明
    destinationNamestring地点名称

应用可根据linkFeature中定义的特性功能,比如路线规划、导航和位置搜索,结合接收到的uri和参数开发不同的样式页面。

完整示例:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const TAG = 'EntryAbility'

export default class EntryAbility extends UIAbility {
    windowStage: window.WindowStage | null = null;

    uri?: string;
    destinationLatitude?: number;
    destinationLongitude?: number;
    destinationName?: string;
    originName?: string;
    originLatitude?: number;
    originLongitude?: number;
    vehicleType?: number;
    destinationPoiId?: string;
    originPoiId?: string;

    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onCreate, want=${JSON.stringify(want)}`);
        super.onCreate(want, launchParam);
        this.parseWant(want);
    }

    onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onNewWant, want=${JSON.stringify(want)}`);
        super.onNewWant(want, launchParam);
        this.parseWant(want);
        if (!this.windowStage) {
            hilog.error(0x0000, TAG, 'windowStage is null');
            this.context.terminateSelf();
            return;
        }
        this.loadPage(this.windowStage);
    }

    private parseWant(want: Want): void {
        this.uri = want.uri as string | undefined;
        this.destinationLatitude = want.parameters?.destinationLatitude as number | undefined;
        this.destinationLongitude = want.parameters?.destinationLongitude as number | undefined;
        this.destinationName = want.parameters?.destinationName as string | undefined;
        this.originName = want.parameters?.originName as string | undefined;
        this.originLatitude = want.parameters?.originLatitude as number | undefined;
        this.originLongitude = want.parameters?.originLongitude as number | undefined;
        this.vehicleType = want.parameters?.vehicleType as number | undefined;
        this.destinationPoiId = want.parameters?.destinationPoiId as string | undefined;
        this.originPoiId = want.parameters?.originPoiId as string | undefined;
    }

    private loadPage(windowStage: window.WindowStage): void {
        hilog.info(0x0000, TAG, `loadPage, uri=${this.uri}`);
        if (this.uri === 'maps://navigation') {
            // 构建导航场景参数
            const storage: LocalStorage = new LocalStorage({
                "destinationLatitude": this.destinationLatitude,
                "destinationLongitude": this.destinationLongitude,
                "destinationPoiId": this.destinationPoiId
            } as Record<string, Object>);
            // 拉起导航页面
            windowStage.loadContent('pages/NavigationPage', storage)
        } else if (this.uri === 'maps://routePlan') {
            // 构建路径规划场景参数
            const storage: LocalStorage = new LocalStorage({
                "destinationLatitude": this.destinationLatitude,
                "destinationLongitude": this.destinationLongitude,
                "destinationName": this.destinationName,
                "originName": this.originName,
                "originLatitude": this.originLatitude,
                "originLongitude": this.originLongitude,
                "vehicleType": this.vehicleType,
                "destinationPoiId": this.destinationPoiId,
                "originPoiId": this.originPoiId
            } as Record<string, Object>);
            // 拉起路径规划页面
            windowStage.loadContent('pages/RoutePlanPage', storage)
        }  else if (this.uri === 'maps://search') {
            // 构建位置搜索场景参数
            const storage: LocalStorage = new LocalStorage({
                "destinationName": this.destinationName
            } as Record<string, Object>);
            // 拉起位置搜索页面
            windowStage.loadContent('pages/PlaceSearchPage', storage)
        } else {
            // 默认拉起首页
            windowStage.loadContent('pages/Index', (err) => {
                if (err.code) {
                    hilog.error(0x0000, TAG, 'Failed to load the content. Cause: %{public}s',
                        JSON.stringify(err) ?? '');
                    return;
                }
                hilog.info(0x0000, TAG, 'Succeeded in loading the content.');
            });
        }
    }

    onDestroy(): void {
        hilog.info(0x0000, TAG, `onDestroy`);
    }

    onWindowStageCreate(windowStage: window.WindowStage): void {
        hilog.info(0x0000, TAG, `onWindowStageCreate`);
        this.windowStage = windowStage;
        this.loadPage(this.windowStage);
    }

    onWindowStageDestroy(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onWindowStageDestroy');
    }

    onForeground(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onForeground');
    }

    onBackground(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onBackground');
    }
}

 3.4 拉起邮件类应用(startAbilityByType)

3.4.1 邮件类应用扩展面板参数说明

startAbilityByType接口中type字段为mail,对应的wantParam参数:

参数名类型必填说明
emailstring[ ]收件人邮箱地址(支持多个且以逗号分隔)
ccstring[ ]抄收人邮箱地址(支持多个且以逗号分隔)
bccstring[ ]密送人邮箱地址(支持多个且以逗号分隔)
subjectstring邮件主题
bodystring邮件内容
ability.params.streamstring[ ]邮件附件(附件的uri地址列表)
ability.want.params.uriPermissionFlagwantConstant.Flags给邮件附件赋予至少读权限。邮件附件参数存在时,该参数也必须要传
sceneTypenumber意图场景,表明本次请求对应的操作意图。1:发邮件。默认为1。

说明

  • 邮件类应用扩展面板中的类型为string的参数,都要经过encodeURI编码。

  • 邮件类应用扩展面板中的类型为string[]的参数,数组中的元素都要经过encodeURI编码。

 3.4.2 拉起方开发步骤

1. 导入相关模块。

import { common, wantConstant } from '@kit.AbilityKit';

2. 构造接口参数并调用startAbilityByType接口。

let context = getContext(this) as common.UIAbilityContext;
let wantParam: Record<string, Object> = {
  'sceneType': 1,
  'email': [encodeURI('xxx@example.com'),encodeURI('xxx@example.com')], // 收件人邮箱地址,多值以逗号分隔,对数组内容使用encodeURI()方法进行url编码
  'cc': [encodeURI('xxx@example.com'),encodeURI('xxx@example.com')], // 抄收人邮箱地址,多值以逗号分隔,对数组内容使用encodeURI()方法进行url编码
  'bcc': [encodeURI('xxx@example.com'),encodeURI('xxx@example.com')], // 密送人邮箱地址,多值以逗号分隔,对数组内容使用encodeURI()方法进行url编码
  'subject': encodeURI('邮件主题'), // 邮件主题,对内容使用encodeURI()方法进行url编码
  'body': encodeURI('邮件正文'), // 邮件正文,对内容使用encodeURI()方法进行url编码
  'ability.params.stream': [encodeURI('附件uri1'),encodeURI('附件uri2')], // 附件uri,多值以逗号分隔,对数组内容使用encodeURI()方法进行url编码
  'ability.want.params.uriPermissionFlag': wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
};
let abilityStartCallback: common.AbilityStartCallback = {
  onError: (code: number, name: string, message: string) => {
    console.log(`onError code ${code} name: ${name} message: ${message}`);
  },
  onResult: (result)=>{
    console.log(`onResult result: ${JSON.stringify(result)}`);
  }
}

context.startAbilityByType("mail", wantParam, abilityStartCallback, 
    (err) => {
        if (err) {
            console.error(`startAbilityByType fail, err: ${JSON.stringify(err)}`);
        } else {
            console.log(`success`);
        }
});

效果示例图:

3.4.3 目标方开发步骤

1. 在module.json5中新增linkFeature属性并设置声明当前应用支持的特性功能,从而系统可以从设备已安装应用中找到当前支持该特性的应用,取值范围如下:

取值含义
ComposeMail声明应用支持撰写邮件功能
{
  "abilities": [
      {
      "skills": [
          {
          "uris": [
              {
              "scheme": "mailto", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "",
              "path": "",
              "linkFeature": "ComposeMail" // 声明应用支持撰写邮件功能
              }
            ]
          }
      ]
      }
  ]
}

2. 解析面板传过来的参数并做对应处理。

UIAbility.onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void

在参数want.parameters中会携带Caller方传入的参数(与调用方传入的有些差异),如下表所示:

参数名类型必填说明
emailstring[ ]收件人邮箱地址(支持多个且以逗号分隔)
ccstring[ ]抄收人邮箱地址(支持多个且以逗号分隔)
bccstring[ ]密送人邮箱地址(支持多个且以逗号分隔)
subjectstring邮件主题
bodystring邮件内容
streamstring[ ]邮件附件列表(附件的uri地址列表)

说明

  • 目标方接收的类型为string的参数,都要经过decodeURI解码。

  • 目标方接收的类型为string[]的参数,数组中的元素都要经过decodeURI解码。

 完整示例:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const TAG = 'MailTarget1.EntryAbility'

export default class EntryAbility extends UIAbility {
    windowStage: window.WindowStage | null = null;

    email: string[] | undefined;
    cc: string[] | undefined;
    bcc: string[] | undefined;
    subject: string | undefined;
    body: string | undefined;
    stream: string[] | undefined;
    
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onCreate, want=${JSON.stringify(want)}`);
        super.onCreate(want, launchParam);
        this.parseWant(want);
    }

    onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onNewWant, want=${JSON.stringify(want)}`);
        super.onNewWant(want, launchParam);
        this.parseWant(want);
        if (!this.windowStage) {
            hilog.error(0x0000, TAG, 'windowStage is null');
            this.context.terminateSelf();
            return;
        }
        this.loadPage(this.windowStage);
    }

    private parseWant(want: Want): void {
        this.email = this.decodeStringArr(want.parameters?.email as string[]);
        this.cc = this.decodeStringArr(want.parameters?.cc as string[]);
        this.bcc = this.decodeStringArr(want.parameters?.bcc as string[]);
        this.subject = decodeURI(want.parameters?.subject as string);// 使用decodeURI()方法对邮件主题进行url解码,其他字段处理方法相同
        this.body = decodeURI(want.parameters?.body as string);// 使用decodeURI()方法对邮件内容进行url解码,其他字段处理方法相同
        this.stream = this.decodeStringArr(want.parameters?.stream as string[]);
    }

    // 使用decodeURI()方法对string数组内容进行解码
    private decodeStringArr(source: string[] | undefined): string[] {
        let target: string[] = [];
        source?.forEach(e => {
            target.push(decodeURI(e));
        })
        return target;
    }

    private loadPage(windowStage: window.WindowStage): void {
        const storage: LocalStorage = new LocalStorage({
            "email": this.email,
            "cc": this.cc,
            "bcc": this.bcc,
            "subject": this.subject,
            "body": this.body,
            "stream": this.stream
        } as Record<string, Object>);

        windowStage.loadContent('pages/ComposeMailPage', storage);

    }

    onDestroy(): void {
        hilog.info(0x0000, TAG, `onDestroy`);
    }

    onWindowStageCreate(windowStage: window.WindowStage): void {
        hilog.info(0x0000, TAG, `onWindowStageCreate`);
        this.windowStage = windowStage;
        this.loadPage(this.windowStage);
    }

    onWindowStageDestroy(): void {
        hilog.info(0x0000, TAG, `onWindowStageDestroy`);
    }

    onForeground(): void {
        hilog.info(0x0000, TAG, `onForeground`);
    }

    onBackground(): void {
        hilog.info(0x0000, TAG, `onBackground`);
    }
}

3.5 拉起邮件类应用(mailto方式)

3.5.1 使用场景

通过mailto电子邮件协议,可以创建指向电子邮件地址的超链接,方便用户通过网页或应用中的超链接直接跳转电子邮件应用。同时,支持在mailto:的相关字段中定义邮件的收件人、主题、正文内容等,节省用户编辑邮件的时间。

常见的应用场景举例如下:

  • 从网页拉起:
    • 用户在购物网站浏览产品页面时,看到“联系我们”按钮,点击后会拉起默认邮件客户端。收件人自动填写为客服邮箱,邮件主题设定为产品咨询。
    • 在招聘岗位页面,点击“申请职位”按钮,会拉起邮件客户端。收件人地址为招聘邮箱,邮件主题为“应聘某职位”,正文可以预先填入申请模板。
  • 从应用拉起:
    • 移动应用中,用户点击“反馈”按钮时,应用调用系统功能,拉起默认邮件客户端,预先填写反馈邮箱、问题描述等信息。
    • 移动应用中,当用户点击“通过邮件分享”按钮时,应用会通过 mailto 调用邮件客户端,预填邮件主题和正文。

3.5.2 mailto协议格式

mailto协议标准格式如下:

mailto:someone@example.com?key1=value1&key2=value2
  • mailto::mailto scheme,必填。

  • someone@example.com:收件人地址,选填。如果存在多个地址,用英文逗号分隔。

  • ?:邮件头声明开始符号。如果带邮件头参数,则必填。

  • key-value:邮件头参数,详细参数见下表。

    邮件头含义数据类型是否必填
    subject邮件主题string
    body邮件正文string
    cc抄送人,多个用逗号分隔string
    bcc密送人,多个用逗号分隔string

3.5.3 拉起方开发步骤 

3.5.3.1 从网页拉起

网页中的超链接需要满足mailto协议。示例如下:

实际开发时,需要将邮箱地址替换为真实的邮箱,邮件内容可以根据需要进行配置。

实现效果如下:

3.5.3.2 从应用拉起

保证mailto字符串传入uri参数即可,在应用中page页面可通过 getContext(this) 获取context,在ability中可通过this.context获取context。

@Entry
@Component
struct Index {

  build() {
    Column() {
      Button('反馈')
        .onClick(() => {
          let ctx = getContext(this) as common.UIAbilityContext;
          ctx.startAbility({
            action: 'ohos.want.action.sendToData',
            uri: 'mailto:feedback@example.com?subject=App Feedback&body=Please describe your feedback here...'
          })
        
        })
    }
  }
}

实现效果如下:

3.5.4 目标方开发步骤

1. 为了能够支持被其他应用通过mailto协议拉起,目标应用需要在module.json5配置文件中声明mailto。

{
  "module": {
    // ...
    "abilities": [
      {
        // ...
        "skills": [
          {
          "actions": [
              'ohos.want.action.sendToData'
            ],
            "uris": [
              {
                "scheme": "mailto",
                // linkFeature 用于适配垂类面板拉起
                "linkFeature": 'ComposeMail'
              }
            ]
          }
        ]
      }
    ]
  }
}

2. 目标应用在代码中取出uri参数进行解析。

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 
    // 应用冷启动生命周期回调,其他业务处理...
    parseMailto(want);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 应用热启动生命周期回调,其他业务处理...
    parseMailto(want);
  }

  public parseMailto(want: Want) {
    const uri = want?.uri;
    if (!uri || uri.length <= 0) {
      return;
    }
    // 开始解析 mailto...
  }
}

3.6 拉起金融类应用(startAbilityByType)

3.6.1 金融类应用扩展面板参数说明

startAbilityByType接口中type字段为finance,对应的wantParam参数:

参数名类型必填说明
sceneTypenumber意图场景,表明本次请求对应的操作意图。1:转账汇款 2:信用卡还款。默认为1
bankCardNostring银行卡卡号

3.6.2 拉起方开发步骤

1. 导入相关模块。

import { common } from '@kit.AbilityKit';

2. 构造接口参数并调用startAbilityByType接口。

let context = getContext(this) as common.UIAbilityContext;
let wantParam: Record<string, Object> = {
  'sceneType': 1,
  "bankCardNo": '123456789'
};
let abilityStartCallback: common.AbilityStartCallback = {
  onError: (code: number, name: string, message: string) => {
    console.log(`onError code ${code} name: ${name} message: ${message}`);
  },
  onResult: (result)=>{
    console.log(`onResult result: ${JSON.stringify(result)}`);
  }
}

context.startAbilityByType("finance", wantParam, abilityStartCallback, 
    (err) => {
        if (err) {
            console.error(`startAbilityByType fail, err: ${JSON.stringify(err)}`);
        } else {
            console.log(`success`);
        }
});

效果示例图:

3.6.3 目标方开发步骤

1. 在module.json5中配置uris,步骤如下:

  1. 设置linkFeature属性以声明当前应用支持的特性功能,从而系统可以从设备已安装应用中找到当前支持该特性的应用,取值范围如下:

    取值含义
    Transfer声明应用支持转账汇款功能
    CreditCardRepayment声明应用支持信用卡还款功能
  2. 设置scheme、host、port、path/pathStartWith属性,与Want中URI相匹配,以便区分不同功能。

{
  "abilities": [
      {
      "skills": [
          {
          "uris": [
              {
              "scheme": "finance", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "transfer",
              "path": "",
              "linkFeature": "Transfer" // 声明应用支持转账汇款功能
              },
              {
              "scheme": "finance", // 这里仅示意,应用需确保这里声明的的uri能被外部正常拉起
              "host": "credit_card_repayment",
              "path": "",
              "linkFeature": "CreditCardRepayment" // 声明应用支持信用卡还款功能
              }
          ]
          }
      ]
      }
  ]
}

 2. 解析面板传过来的参数并做对应处理。

UIAbility.onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void

在参数want.uri中会携带目标方配置的linkFeature对应的uri;

在参数want.parameters中会携带Caller方传入的参数,如下表所示:

参数名类型必填说明
bankCardNostring银行卡卡号

应用可根据linkFeature中定义的特性功能,比如转账汇款和信用卡还款,结合接收到的uri开发不同的样式页面。

完整示例:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const TAG = 'EntryAbility'

export default class EntryAbility extends UIAbility {
    windowStage: window.WindowStage | null = null;

    uri?: string;
    bankCardNo?: string;

    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onCreate, want=${JSON.stringify(want)}`);
        super.onCreate(want, launchParam);
        this.parseWant(want);
    }

    onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, TAG, `onNewWant, want=${JSON.stringify(want)}`);
        super.onNewWant(want, launchParam);
        this.parseWant(want);
        if (!this.windowStage) {
            hilog.error(0x0000, TAG, 'windowStage is null');
            this.context.terminateSelf();
            return;
        }
        this.loadPage(this.windowStage);
    }

    private parseWant(want: Want): void {
        this.uri = want.uri as string | undefined;
        this.bankCardNo = want.parameters?.bankCardNo as string | undefined;
    }

    private loadPage(windowStage: window.WindowStage): void {
        hilog.info(0x0000, TAG, `loadPage, uri=${this.uri}`);
        if (this.uri === 'finance://transfer') {
            // 构建转账场景参数
            const storage: LocalStorage = new LocalStorage({
                "bankCardNo": this.bankCardNo
            } as Record<string, Object>);
            // 拉起转账页面
            windowStage.loadContent('pages/TransferPage', storage)
        } else if (this.uri === 'finance://credit_card_repayment') {
            // 构建信用卡还款场景参数
            const storage: LocalStorage = new LocalStorage({
                "bankCardNo": this.bankCardNo
            } as Record<string, Object>);
            // 拉起信用卡还款页面
            windowStage.loadContent('pages/CreditCardRepaymentPage', storage)
        } else {
            // 默认拉起首页
            windowStage.loadContent('pages/Index', (err) => {
                if (err.code) {
                    hilog.error(0x0000, TAG, 'Failed to load the content. Cause: %{public}s',
                        JSON.stringify(err) ?? '');
                    return;
                }
                hilog.info(0x0000, TAG, 'Succeeded in loading the content.');
            });
        }
    }

    onDestroy(): void {
        hilog.info(0x0000, TAG, `onDestroy`);
    }

    onWindowStageCreate(windowStage: window.WindowStage): void {
        hilog.info(0x0000, TAG, `onWindowStageCreate`);
        this.windowStage = windowStage;
        this.loadPage(this.windowStage);
    }

    onWindowStageDestroy(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onWindowStageDestroy');
    }

    onForeground(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onForeground');
    }

    onBackground(): void {
        hilog.info(0x0000, TAG, '%{public}s', 'Ability onBackground');
    }
}

3.7 拉起图片编辑类应用(startAbilityByType)

3.7.1 使用场景

当应用自身不具备图片编辑能力、但存在图片编辑的场景时,可以通过startAbilityByType拉起图片编辑类应用扩展面板,由对应的应用完成图片编辑操作。图片编辑类应用可以通过PhotoEditorExtensionAbility实现图片编辑页面,并将该页面注册到图片编辑面板,从而将图片编辑能力开放给其他应用。

流程示意图如下:

例如:用户在图库App中选择编辑图片时,图库App可以通过startAbilityByType拉起图片编辑类应用扩展面板。用户可以从已实现PhotoEditorExtensionAbility应用中选择一款,并进行图片编辑。

3.7.2 接口说明

接口详情参见PhotoEditorExtensionAbilityPhotoEditorExtensionContext

接口名描述
onStartContentEditing(uri: string, want:Want, session: UIExtensionContentSession):void可以执行读取原始图片、加载页面等操作。
saveEditedContentWithImage(pixelMap: image.PixelMap, option: image.PackingOption): Promise<AbilityResult>传入编辑过的图片的PixelMap对象并保存。

3.7.3 图片编辑类应用实现图片编辑页面

1. 在DevEco Studio工程中手动新建一个PhotoEditorExtensionAbility。

  1. 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录,如PhotoEditorExtensionAbility。
  2. 在PhotoEditorExtensionAbility目录中,右键选择“New > File”,新建一个.ets文件,如ExamplePhotoEditorAbility.ets。

2. 在ExamplePhotoEditorAbility.ets中重写onCreate、onForeground、onBackground、onDestroy和onStartContentEditing的生命周期回调。

其中,需要在onStartContentEditing中加载入口页面文件pages/Index.ets,并将session、uri、实例对象等保存在LocalStorage中传递给页面。

import { PhotoEditorExtensionAbility,UIExtensionContentSession,Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[ExamplePhotoEditorAbility]';
export default class ExamplePhotoEditorAbility extends PhotoEditorExtensionAbility {
  onCreate() {
    hilog.info(0x0000, TAG, 'onCreate');
  }

  // 获取图片,加载页面并将需要的参数传递给页面
  onStartContentEditing(uri: string, want: Want, session: UIExtensionContentSession): void {
    hilog.info(0x0000, TAG, `onStartContentEditing want: ${JSON.stringify(want)}, uri: ${uri}`);

    const storage: LocalStorage = new LocalStorage({
      "session": session,
      "uri": uri
    } as Record<string, Object>);

    session.loadContent('pages/Index', storage);
  }

  onForeground() {
    hilog.info(0x0000, TAG, 'onForeground');
  }

  onBackground() {
    hilog.info(0x0000, TAG, 'onBackground');
  }

  onDestroy() {
    hilog.info(0x0000, TAG, 'onDestroy');
  }
}

3. 在page中实现图片编辑功能。

图片编辑完成后调用saveEditedContentWithImage保存图片,并将回调结果通过terminateSelfWithResult返回给调用方。

import { common } from '@kit.AbilityKit';
import { UIExtensionContentSession, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';

const storage = LocalStorage.getShared()
const TAG = '[ExamplePhotoEditorAbility]';

@Entry
@Component
struct Index {
  @State message: string = 'editImg';
  @State originalImage: PixelMap | null = null;
  @State editedImage: PixelMap | null = null;
  private newWant ?: Want;

  aboutToAppear(): void {
    let originalImageUri = storage?.get<string>("uri") ?? "";
    hilog.info(0x0000, TAG, `OriginalImageUri: ${originalImageUri}.`);

    this.readImageByUri(originalImageUri).then(imagePixMap => {
      this.originalImage = imagePixMap;
    })
  }

  // 根据uri读取图片内容
  async readImageByUri(uri: string): Promise < PixelMap | null > {
    hilog.info(0x0000, TAG, "uri: " + uri);
    let file: fileIo.File | undefined;
    try {
      file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
      hilog.info(0x0000, TAG, "Original image file id: " + file.fd);

      let imageSourceApi: image.ImageSource = image.createImageSource(file.fd);
      if(!imageSourceApi) {
        hilog.info(0x0000, TAG, "ImageSourceApi failed");
        return null;
      }
      let pixmap: image.PixelMap = await imageSourceApi.createPixelMap();
      if(!pixmap) {
        hilog.info(0x0000, TAG, "createPixelMap failed");
        return null;
      }
      this.originalImage = pixmap;
      return pixmap;
    } catch(e) {
      hilog.info(0x0000, TAG, `ReadImage failed:${e}`);
    } finally {
      fileIo.close(file);
    }
    return null;
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Button("RotateAndSaveImg").onClick(event => {
          hilog.info(0x0000, TAG, `Start to edit image and save.`);
          // 编辑图片功能实现
          this.originalImage?.rotate(90).then(() => {
            let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
            try {
              // 调用saveEditedContentWithImage保存图片
              (getContext(this) as common.PhotoEditorExtensionContext).saveEditedContentWithImage(this.originalImage as image.PixelMap,
                packOpts).then(data => {
                if (data.resultCode == 0) {
                  hilog.info(0x0000, TAG, `Save succeed.`);
                }
                hilog.info(0x0000, TAG,
                    `saveContentEditingWithImage result: ${JSON.stringify(data)}`);
                this.newWant = data.want;
                // data.want.uri存有编辑过图片的uri
                this.readImageByUri(this.newWant?.uri ?? "").then(imagePixMap => {
                  this.editedImage = imagePixMap;
                })
              })
            } catch (e) {
              hilog.error(0x0000, TAG, `saveContentEditingWithImage failed:${e}`);
              return;
            }
          })
        }).margin({ top: 10 })

        Button("terminateSelfWithResult").onClick((event => {
          hilog.info(0x0000, TAG, `Finish the current editing.`);

          let session = storage.get('session') as UIExtensionContentSession;
          // 关闭并回传修改结果给调用方
          session.terminateSelfWithResult({ resultCode: 0, want: this.newWant });

        })).margin({ top: 10 })

        Image(this.originalImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain)

        Image(this.editedImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain)
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Pink)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
  }
}

4. 在工程Module对应的module.json5配置文件中注册PhotoEditorExtensionAbility。

type标签需要配置为"photoEditor",srcEntry需要配置为PhotoEditorExtensionAbility组件所对应的代码路径。

{
  "module": {
    "extensionAbilities": [
      {
        "name": "ExamplePhotoEditorAbility",
        "icon": "$media:icon",
        "description": "ExamplePhotoEditorAbility",
        "type": "photoEditor",
        "exported": true,
        "srcEntry": "./ets/PhotoEditorExtensionAbility/ExamplePhotoEditorAbility.ets",
        "label": "$string:EntryAbility_label",
        "extensionProcessMode": "bundle"
      },
    ]
  }
}

3.7.4 调用方拉起图片编辑类应用编辑图片

在UIAbility或者UIExtensionAbility的页面中通过接口startAbilityByType拉起图片编辑类应用扩展面板,系统将自动查找并在面板上展示基于PhotoEditorExtensionAbility实现的图片编辑应用,由用户选择某个应用来完成图片编辑的功能,最终将编辑的结果返回给到调用方,具体步骤如下:

1. 导入模块。

import { common, wantConstant } from '@kit.AbilityKit';
import { fileUri, picker } from '@kit.CoreFileKit';

 2. (可选)实现从图库中选取图片。

async photoPickerGetUri(): Promise < string > {
  try {
    let PhotoSelectOptions = new picker.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 1;
    let photoPicker = new picker.PhotoViewPicker();
    let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
    return photoSelectResult.photoUris[0];
  } catch(error) {
    let err: BusinessError = error as BusinessError;
    hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err));
  }
  return "";
}

3. 将图片拷贝到本地沙箱路径。

 let context = getContext(this) as common.UIAbilityContext;
 let file: fileIo.File | undefined;
 try {
   file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
   hilog.info(0x0000, TAG, "file: " + file.fd);

   let timeStamp = Date.now();
   // 将用户图片拷贝到应用沙箱路径
   fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`);

   this.filePath = context.filesDir + `/original-${timeStamp}.jpg`;
   this.originalImage = fileUri.getUriFromPath(this.filePath);
 } catch (e) {
   hilog.info(0x0000, TAG, `readImage failed:${e}`);
 } finally {
   fileIo.close(file);
 }

4. 在startAbilityByType回调函数中,通过want.uri获取编辑后的图片uri,并做对应的处理。

  let context = getContext(this) as common.UIAbilityContext;
  let abilityStartCallback: common.AbilityStartCallback = {
    onError: (code, name, message) => {
      const tip: string = `code:` + code + ` name:` + name + ` message:` + message;
      hilog.error(0x0000, TAG, "startAbilityByType:", tip);
    },
    onResult: (result) => {
      // 获取到回调结果中编辑后的图片uri并做对应的处理
      let uri = result.want?.uri ?? "";
      hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result));
      this.readImage(uri).then(imagePixMap => {
        this.editedImage = imagePixMap;
      });
    }
  }

5. 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板。

 let uri = fileUri.getUriFromPath(this.filePath);
 context.startAbilityByType("photoEditor", {
   "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri
   "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板
 } as Record<string, Object>, abilityStartCallback, (err) => {
   let tip: string;
   if (err) {
     tip = `Start error: ${JSON.stringify(err)}`;
     hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`);
   } else {
     tip = `Start success`;
     hilog.info(0x0000, TAG, "startAbilityByType: ", `success`);
   }
 });

示例:

import { common, wantConstant } from '@kit.AbilityKit';
import { fileUri, picker } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { JSON } from '@kit.ArkTS';

const TAG = 'PhotoEditorCaller';

@Entry
@Component
struct Index {
  @State message: string = 'selectImg';
  @State originalImage: ResourceStr = "";
  @State editedImage: PixelMap | null = null;
  private filePath: string = "";

  // 根据uri读取图片内容
  async readImage(uri: string): Promise < PixelMap | null > {
    hilog.info(0x0000, TAG, "image uri: " + uri);
    let file: fileIo.File | undefined;
    try {
      file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
      hilog.info(0x0000, TAG, "file: " + file.fd);

      let imageSourceApi: image.ImageSource = image.createImageSource(file.fd);
      if(!imageSourceApi) {
        hilog.info(0x0000, TAG, "imageSourceApi failed");
        return null;
      }
      let pixmap: image.PixelMap = await imageSourceApi.createPixelMap();
      if(!pixmap) {
        hilog.info(0x0000, TAG, "createPixelMap failed");
        return null;
      }
      this.editedImage = pixmap;
      return pixmap;
    } catch(e) {
      hilog.info(0x0000, TAG, `readImage failed:${e}`);
    } finally {
      fileIo.close(file);
    }
    return null;
  }

  // 图库中选取图片
  async photoPickerGetUri(): Promise < string > {
    try {
      let PhotoSelectOptions = new picker.PhotoSelectOptions();
      PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      PhotoSelectOptions.maxSelectNumber = 1;
      let photoPicker = new picker.PhotoViewPicker();
      let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
      hilog.info(0x0000, TAG,
        'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult));
      return photoSelectResult.photoUris[0];
    } catch(error) {
      let err: BusinessError = error as BusinessError;
      hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err));
    }
    return "";
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Button("selectImg").onClick(event => {
          // 图库中选取图片
          this.photoPickerGetUri().then(uri => {
            hilog.info(0x0000, TAG, "uri: " + uri);

            let context = getContext(this) as common.UIAbilityContext;
            let file: fileIo.File | undefined;
            try {
              file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
              hilog.info(0x0000, TAG, "file: " + file.fd);

              let timeStamp = Date.now();
              // 将用户图片拷贝到应用沙箱路径
              fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`);

              this.filePath = context.filesDir + `/original-${timeStamp}.jpg`;
              this.originalImage = fileUri.getUriFromPath(this.filePath);
            } catch (e) {
              hilog.info(0x0000, TAG, `readImage failed:${e}`);
            } finally {
              fileIo.close(file);
            }
          })

        }).width('200').margin({ top: 20 })

        Button("editImg").onClick(event => {
          let context = getContext(this) as common.UIAbilityContext;
          let abilityStartCallback: common.AbilityStartCallback = {
            onError: (code, name, message) => {
              const tip: string = `code:` + code + ` name:` + name + ` message:` + message;
              hilog.error(0x0000, TAG, "startAbilityByType:", tip);
            },
            onResult: (result) => {
              // 获取到回调结果中编辑后的图片uri并做对应的处理
              let uri = result.want?.uri ?? "";
              hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result));
              this.readImage(uri).then(imagePixMap => {
                this.editedImage = imagePixMap;
              });
            }
          }
          // 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板
          let uri = fileUri.getUriFromPath(this.filePath);
          context.startAbilityByType("photoEditor", {
            "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri
            "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板
          } as Record<string, Object>, abilityStartCallback, (err) => {
            let tip: string;
            if (err) {
              tip = `Start error: ${JSON.stringify(err)}`;
              hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`);
            } else {
              tip = `Start success`;
              hilog.info(0x0000, TAG, "startAbilityByType: ", `success`);
            }
          });

        }).width('200').margin({ top: 20 })

        Image(this.originalImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain)

        Image(this.editedImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain)
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Orange)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
  }
}

3.8 拉起文件处理类应用(startAbility)

3.8.1 使用场景

通过调用startAbility接口,由系统从已安装的应用中寻找符合要求的应用,打开特定文件。

例如,浏览器下应用下载PDF文件,可以调用此接口选择文件处理应用打开此PDF文件。开发者需要在请求中设置待打开文件的URI路径(uri)、文件格式(type)等字段,以便系统能够识别,直接拉起文件打开应用或弹出一个选择框,让用户选择合适的应用来打开文件,效果示意如下图所示。

图1 效果示意图

3.8.2 接口关键参数说明

通过调用startAbility接口即可实现由已安装的垂域应用来打开文件。

表1 startAbility请求中want相关参数说明

参数名称类型是否必填说明
uristring

表示待打开文件的URI路径,一般配合type使用。

uri格式为:file://bundleName/path

- file:文件URI的标志。

- bundleName:该文件资源的属主。

- path:文件资源在应用沙箱中的路径。

typestring

表示打开文件的类型,推荐使用UTD类型,比如:'general.plain-text'、'general.image'。目前也可以兼容使用MIME type类型,如:'text/xml' 、 'image/*'等。

说明:

1. type为可选字段,如果不传type,系统会尝试根据uri后缀名判断文件类型进行匹配;如果传入type,必须确保与uri的文件类型一致,否则会导致无法匹配到合适的应用。文件后缀与文件类型的映射关系参见Uniform Type Descriptor(UTD)预置列表

2. 不支持传*/*。

parametersRecord<string, Object>表示由系统定义,由开发者按需赋值的自定义参数,文件打开场景请参考表2。
flagsnumber表示处理方式,文件打开场景请参考表3。

表2 parameters相关参数说明

参数名称类型说明
ability.params.streamstring指示携带的文件URI要授权给目标方,用于待打开的文件存在其他文件依赖的场景。例如打开本地html文件依赖本地其余资源文件的场景等。对应的value必须是string类型的文件URI数组。文件URI的获取参考表1中uri参数。
ohos.ability.params.showDefaultPickerboolean

表示是否强制展示文件打开方式的选择弹框,缺省为false。

- false:表示由系统策略或默认应用设置决定直接拉起文件打开应用还是展示弹框。

- true:表示始终展示弹框。

showCallerboolean

表示调用方本身是否作为目标方应用之一参与匹配,缺省为false。

- false:不参与匹配。

- true:参与匹配。

表3 flags相关参数说明

参数名称说明
FLAG_AUTH_READ_URI_PERMISSION0x00000001指对URI执行读取操作的授权。
FLAG_AUTH_WRITE_URI_PERMISSION0x00000002指对URI执行写入操作的授权。

3.8.3 接入步骤

3.8.3.1 调用方接入步骤

1. 导入相关模块。

// xxx.ets
import { fileUri } from '@kit.CoreFileKit';
import { UIAbility, Want, common, wantConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';

2. 获取应用文件路径

// xxx.ets
// 假设应用bundleName值为com.example.demo
export default class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: window.WindowStage) {
        // 获取文件沙箱路径
        let filePath = this.context.filesDir + '/test1.txt';
        // 将沙箱路径转换为uri
        let uri = fileUri.getUriFromPath(filePath);
        // 获取的uri为"file://com.example.demo/data/storage/el2/base/files/test.txt"
    }
    // ...
}

3. 构造请求数据。

// xxx.ets
export default class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: window.WindowStage) {
        // 获取文件沙箱路径
        let filePath = this.context.filesDir + '/test.txt';
        // 将沙箱路径转换为uri
        let uri = fileUri.getUriFromPath(filePath);
        // 构造请求数据
        let want: Want = {
        action: 'ohos.want.action.viewData', // 表示查看数据的操作,文件打开场景固定为此值
        uri: uri,
        type: 'general.plain-text', // 表示待打开文件的类型
        // 配置被分享文件的读写权限,例如对文件打开应用进行读写授权
        flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
        };
    }
    // ...
}

4. 调用接口启动。

// xxx.ets
export default class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: window.WindowStage) {
        // 获取文件沙箱路径
        let filePath = this.context.filesDir + '/test.txt';
        // 将沙箱路径转换为uri
        let uri = fileUri.getUriFromPath(filePath);
        // 构造请求数据
        let want: Want = {
        action: 'ohos.want.action.viewData', // 表示查看数据的操作,文件打开场景固定为此值
        uri: uri,
        type: 'general.plain-text', // 表示待打开文件的类型
        // 配置被分享文件的读写权限,例如对文件打开应用进行读写授权
        flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
        };
        // 调用接口启动
        this.context.startAbility(want)
        .then(() => {
            console.info('Succeed to invoke startAbility.');
        })
        .catch((err: BusinessError) => {
            console.error(`Failed to invoke startAbility, code: ${err.code}, message: ${err.message}`);
        });
    }
    // ...
}
3.8.3.2 目标方接入步骤

1. 声明文件打开能力。

支持打开文件的应用需要在module.json5配置文件中声明文件打开能力。其中uris字段表示接收URI的类型,其中scheme固定为file。type字段表示支持打开的文件类型(参见UTD类型(推荐)或MIME type类型),如下举例中类型为txt文件。

{
"module": {
    // ...
    "abilities": [
    {
        // ...
        "skills": [
        {
            "actions": [
            "ohos.want.action.viewData" // 必填,声明数据处理能力
            ],
            "uris": [
            {
                // 允许打开uri中以file://协议开头标识的本地文件
                "scheme": "file", // 必填,声明协议类型为文件
                "type": "general.plain-text", // 必填,表示支持打开的文件类型
                "linkFeature": "FileOpen" // 必填且大小写敏感,表示此URI的功能为文件打开
            }
            // ...
            ]
            // ...
        }
        ]
    }
    ]
}
}

 2. 应用处理待打开文件。

声明了文件打开的应用在被拉起后,获取传入的Want参数信息,从中获取待打开文件的URI,在打开文件并获取对应的file对象后,可对文件进行读写操作。

// xxx.ets
import fs from '@ohos.file.fs';
import { Want, AbilityConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
        // 从want信息中获取uri字段
        let uri = want.uri;
        if (uri == null || uri == undefined) {
            console.info('uri is invalid');
            return;
        }
        try {
            // 根据待打开文件的URI进行相应操作。例如同步读写的方式打开URI获取file对象
            let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
            console.info('Succeed to open file.');
        } catch (err) {
            let error: BusinessError = err as BusinessError;
            console.error(`Failed to open file openSync, code: ${error.code}, message: ${error.message}`);
        }
    }
}

4 拉起系统应用

4.1 拉起系统应用的方式

拉起系统应用除了采用使用前面章节介绍的方式(比如使用openlink拉起指定应用、使用startAbilitybyType指定类型的应用),还可以采用如下方式。

  • 使用系统Picker组件

    相机、文件管理、联系人等系统应用提供了系统Picker组件,支持开发者无需申请权限、即可使用系统应用的一些常用功能,比如访问用户的资源文件。

    应用拉起系统Picker组件(文件选择器、照片选择器、联系人选择器等)后,由用户在Picker上选择对应的文件、照片、联系人等资源,应用即可获取到Picker的返回结果。例如,一个音频播放器应用可以通过AudioViewPicker让用户选择音频文件,然后获取所选的音频文件路径进行播放。

    说明

    由于系统Picker已经获取了对应权限的预授权,开发者使用系统Picker时,无需再次申请权限也可临时受限访问对应的资源。例如,当应用需要读取用户图片时,可通过使用照片Picker,在用户选择所需要的图片资源后,直接返回该图片资源,而不需要授予应用读取图片文件的权限。

    系统Picker由系统独立进程实现。

  • 使用特定接口

    设置、电话、日历等应用提供了一些接口,通过这些接口可以直接跳转系统应用。

4.2 支持跳转系统应用的能力清单

设置

当前支持直接拉起设置应用中如下功能界面,未列出的暂不支持。

  • 权限设置: 当应用通过requestPermissionsFromUser()接口拉起权限申请弹框时,如果用户拒绝授权,将无法使用该接口再次拉起弹框,需要调用requestPermissionOnSetting接口拉起权限设置弹窗。

    二次向用户申请授权中以申请麦克风权限为例,介绍了如何拉起权限设置弹窗。该文档中的示例代码同样适用于应用权限组列表中的所有权限,只需将对应的权限名进行替换即可。以下为开发者经常用到的一些场景。

    • 拉起位置权限设置弹窗
    • 拉起相机权限设置弹窗
    • 拉起图片与视频权限设置弹窗
    • 拉起音乐和音频权限设置弹窗
    • 拉起通讯录权限设置弹窗
    • 拉起日历权限设置弹窗

应用市场

Store Kit提供了loadProduct()接口,支持直接跳转应用详情页;也可以通过startAbility()隐式拉起应用市场详情页。详见应用详情页展示

钱包

Payment Kit提供了requestPayment接口,可以实现单次支付、支付并签约。

电话

Telephony Kit提供makeCall()接口,支持跳转到拨号界面,并显示待拨出的号码。

日历

Calendar Kit提供addEvent接口,用于创建日程。

联系人

Contacts Kit提供联系人Picker(Contacts Picker),用于拉起联系人应用,读取联系人数据人。详见选择联系人

地图

Map Kit提供了地图Picker,支持地点详情展示地点选取区划选择

相机

  • 拍照录像Camera Kit提供了相机Picker,用于拍照、录像。
  • 扫码 :Scan Kit提供了扫码Picker,支持调用相机,实现默认界面扫码。
  • 卡证识别Vision Kit提供了卡证识别Picker,支持调用相机,识别各类证件并提取卡证信息。
  • 文档扫描 :Vision Kit提供了文档扫描Picker,支持调用相机,拍摄文档并转化为高清扫描件。

文件管理

Core File Kit提供了文件Picker和音频Picker。

图库(媒体库)

Media Library Kit提供了照片Picker(PhotoViewPicker),用于访问、保存公共目录的图片或视频文件。详见选择媒体库资源 、创建媒体资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值