【HarmonyOS之旅】基于ArkTS开发(一)
【HarmonyOS之旅】基于ArkTS开发(一)
1.1 -> 场景介绍
基于Data模板的Ability,有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data既可用于同设备不同应用的数据共享,也支持跨设备不同应用的数据共享。
Data提供方可以自定义数据的增、删、改、查,以及文件打开等功能,并对外提供这些接口。
1.2 -> 接口说明
接口名 | 描述 |
---|---|
onlnitialized | 在Ability初始化调用,通过此回调方法执行rdb等初始化操作。 |
update | 更新数据库中的数据。 |
query | 查询数据库中的数据。 |
delete | 删除一条或多条数据。 |
normalizeUri | 对uri进行规范化。一个规范化的uri可以支持跨设备使用、持久化、备份和还原等,当上下文改变时仍然可以引用到相同的数据项。 |
batchinsert | 向数据库中插入多条数据。 |
denormalizeUri | 将一个由normalizeUri生产的规范化uri转换成非规范化的uri。 |
insert | 向数据中插入一条数据。 |
openFile | 打开一个文件。 |
getFileTypes | 获取文件的MIME类型。 |
getType | 获取uri指定数据相匹配的MIME类型。 |
executeBatch | 批量操作数据库中的数据。 |
call | 自定义方法。 |
1. -> 开发步骤
1..1 -> 创建Data
1. 需要实现Data中Insert、Query、Update、Delete接口的业务内容。保证能够满足数据库存储业务的基本需求。BatchInsert与ExecuteBatch接口已经在系统中实现遍历逻辑,依赖Insert、Query、Update、Delete接口逻辑,来实现数据的批量处理。
创建Data的代码示例如下:
代码语言:javascript代码运行次数:0运行复制 import featureAbility from '@ohos.ability.featureAbility'
import dataAbility from '@ohos.data.dataAbility'
import dataRdb from '@ohos.data.rdb'
ct TABLE_AME = 'book'
ct STORE_COFIG = { name: 'book.db' }
ct SQL_CREATE_TABLE = 'CREATE TABLE IF OT EXISTS book(id ITEGER PRIMARY KEY AUTOICREMET, name TEXT OT ULL, introduction TEXT OT ULL)'
let rdbStore: dataRdb.RdbStore = undefined
export default {
onInitialized(abilityInfo) {
cole.info('DataAbility onInitialized, abilityInfo:' + abilityInfo.bundleame)
let context = featureAbility.getContext()
dataRdb.getRdbStore(context, STORE_COFIG, 1, (err, store) => {
cole.info('DataAbility getRdbStore callback')
(SQL_CREATE_TABLE, [])
rdbStore = store
});
},
insert(uri, valueBucket, callback) {
cole.info('DataAbility insert start')
rdbStore.insert(TABLE_AME, valueBucket, callback)
},
batchInsert(uri, valueBuckets, callback) {
cole.info('DataAbility batch insert start')
for (let i = 0;i < valueBuckets.length; i++) {
cole.info('DataAbility batch insert i=' + i)
if (i < valueBuckets.length - 1) {
rdbStore.insert(TABLE_AME, valueBuckets[i], (err: any, num: number) => {
cole.info('DataAbility batch insert ret=' + num)
})
} else {
rdbStore.insert(TABLE_AME, valueBuckets[i], callback)
}
}
},
query(uri, columns, predicates, callback) {
cole.info('DataAbility query start')
let rdbPredicates = (TABLE_AME, predicates)
rdbStore.query(rdbPredicates, columns, callback)
},
update(uri, valueBucket, predicates, callback) {
cole.info('DataAbilityupdate start')
let rdbPredicates = (TABLE_AME, predicates)
rdbStore.update(valueBucket, rdbPredicates, callback)
},
delete(uri, predicates, callback) {
cole.info('DataAbilitydelete start')
let rdbPredicates = (TABLE_AME, predicates)
rdbStore.delete(rdbPredicates, callback)
}
};
2. 子系统配置
Json重要字段 | 备注说明 |
---|---|
“name” | Ability名称,对应Ability派生的Data类名。 |
“type” | Ability类型,Data对应的Ability类型为”data“。 |
“uri” | 通信使用的URI。 |
“visible” | 对其他应用是否可见,设置为true时,Data才能与其他应用进行通信传输数据。 |
config.json配置样例
代码语言:javascript代码运行次数:0运行复制"abilities":[{
"srcPath": "DataAbility",
"name": ".DataAbility",
"icon": "$media:icon",
"srcLanguage": "ets",
"description": "$string:description_dataability",
"type": "data",
"visible": true,
"uri": "dataability://ohos.DataAbility"
}]
1..2 -> 访问Data
开发前准备
需导入基础依赖包,以及获取与Data子模块通信的Uri字符串。
其中,基础依赖包包括:
- @ohos.ability.featureAbility
- @ohos.data.dataability
- @ohos.data.rdb
DataAbility接口开发指导
1. 创建工具接口类对象。
代码语言:javascript代码运行次数:0运行复制// 作为参数传递的Uri,与config中定义的Uri的区别是多了一个"/",是因为作为参数传递的uri中,在第二个与第三个"/"中间,存在一个DeviceID的参数
import featureAbility from '@ohos.ability.featureAbility'
import ohos_data_ability from '@ohos.data.dataAbility'
import ohos_data_rdb from '@ohos.data.rdb'
var urivar = "dataability:///com.ix.DataAbility"
var DAHelper = featureAbility.acquireDataAbilityHelper(
urivar
);
2. 构建数据库相关的rdb数据。
代码语言:javascript代码运行次数:0运行复制var valuesBucket = {"name": "gaolu"}
var da = new ohos_data_ability.DataAbilityPredicates()
var valArray =new Array("value1");
var cars = new Array({"batchInsert1" : "value1",});
. 调用insert方法向指定的Data子模块插入数据。
代码语言:javascript代码运行次数:0运行复制// callback方式调用:
DAHelper.insert(
urivar,
valuesBucket,
(error, data) => {
cole.log("DAHelper insert result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var datainsert = await DAHelper.insert(
urivar,
valuesBucket
);
4. 调用delete方法删除Data子模块中指定的数据。
代码语言:javascript代码运行次数:0运行复制// callback方式调用:
DAHelper.delete(
urivar,
da,
(error, data) => {
cole.log("DAHelper delete result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var datadelete = await DAHelper.delete(
urivar,
da,
);
5. 调用update方法更新指定Data子模块中的数据。
代码语言:javascript代码运行次数:0运行复制// callback方式调用:
DAHelper.update(
urivar
valuesBucket,
da,
(error, data) => {
cole.log("DAHelper update result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var dataupdate = await DAHelper.update(
urivar,
valuesBucket,
da,
);
6. 调用query方法在指定的Data子模块中查数据。
代码语言:javascript代码运行次数:0运行复制// callback方式调用:
DAHelper.query(
urivar,
valArray,
da,
(error, data) => {
cole.log("DAHelper query result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var dataquery = await DAHelper.query(
urivar,
valArray,
da
);
7. 调用batchInsert方法向指定的数据子系统批量插入数据。
代码语言:javascript代码运行次数:0运行复制// callback方式调用:
DAHelper.batchInsert(
urivar,
cars,
(error, data) => {
cole.log("DAHelper batchInsert result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var databatchInsert = await DAHelper.batchInsert(
urivar,
cars
);
8. 调用executeBatch方法向指定的Data子模块进行数据的批量处理。
代码语言:javascript代码运行次数:0运行复制// callbacke方式调用:
(
urivar,
[
{
uri: urivar,
type: featureAbility.DataAbilityOperationType.TYPE_ISERT,
valuesBucket: {"executeBatch" : "value1",},
predicates: da,
expectedCount:0,
predicatesBackReferences: null,
interrupted:true,
}
],
(error, data) => {
cole.log("DAHelper executeBatch result: " + data)
}
);
代码语言:javascript代码运行次数:0运行复制// promise方式调用:
var dataexecuteBatch = await (
urivar,
[
{
uri: urivar,
type: featureAbility.DataAbilityOperationType.TYPE_ISERT,
valuesBucket:
{
"executeBatch" : "value1",
},
predicates: da,
expectedCount:0,
predicatesBackReferences: null,
interrupted:true,
}
]
);
2.1 -> 卡片概述
卡片是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达,减少体验层级的目的。
卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面,发送消息等基础的交互功能。卡片使用方负责显示卡片。
卡片的基本概念:
- 卡片提供方:提供卡片显示内容元服务,控制卡片的显示内容、控件布局以及控件点击事件。
- 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
- 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。
说明:
卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。
开发者仅需作为卡片提供方进行卡片内容的开发,卡片使用方和卡片管理服务由系统自动处理。
卡片提供方控制卡片实际显示的内容、控件布局以及点击事件。
2.2 -> 场景介绍
FA卡片开发,即基于FA模型的卡片提供方开发,主要涉及如下功能逻辑:
- 开发卡片生命周期回调函数LifecycleForm。
- 创建卡片数据FormBindingData对象。
- 通过FormProvider更新卡片。
- 开发卡片页面。
2. -> 接口说明
接口名 | 描述 |
---|---|
onCreate(want: Want): formBindingData.FormBindingData | 卡片提供方接收创建卡片的通知接口。 |
onCastToormal(formId: string): void | 卡片提供方接收临时卡片转常态卡片的通知接口。 |
onUpdate(formId: string): void | 卡片提供方接收更新卡片的通知接口。 |
onVisibilityChange(newStatus: { [key: string]: number }): void | 卡片提供方接收修改可见性的通知接口。 |
onEvent(formId: string, message: string): void | 卡片提供方接收处理卡片事件的通知接口。 |
onDestroy(formId: string): void | 卡片提供方接收销毁卡片的通知接口。 |
onAcquireFormState?(want: Want): formInfo.FormState | 卡片提供方接收查询卡片状态的通知接口。 |
接口名 | 描述 |
---|---|
setFormextRefreshTime(formId: string, minute: number, callback: AsyncCallback<void>): void; | 设置指定卡片的下一次更新时间。 |
setFormextRefreshTime(formId: string, minute: number): Promise<void>; | 设置指定卡片的下一次更新时间,以promise方式返回。 |
updateForm(formId: string, formBindingData: FormBindingData, callback: AsyncCallback<void>): void; | 更新指定的卡片。 |
updateForm(formId: string, formBindingData: FormBindingData): Promise<void>; | 更新指定的卡片,以promise方式返回。 |
2.4 -> 开发步骤
2.4.1 -> 创建LifecycleForm
创建FA模型的卡片,需实现LifecycleForm的生命周期接口。具体示例代码如下:
1. 导入相关模块
代码语言:javascript代码运行次数:0运行复制import formBindingData from '@ohos.application.formBindingData'
import formInfo from '@ohos.application.formInfo'
import formProvider from '@ohos.application.formProvider'
2. 实现LifecycleForm生命周期接口
代码语言:javascript代码运行次数:0运行复制export default {
onCreate(want) {
cole.log('FormAbility onCreate');
// 由开发人员自行实现,将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用
let obj = {
"title": "titleOnCreate",
"detail": "detailOnCreate"
};
let formData = (obj);
return formData;
},
onCastToormal(formId) {
// 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
cole.log('FormAbility onCastToormal');
},
onUpdate(formId) {
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要覆写该方法以支持数据更新
cole.log('FormAbility onUpdate');
let obj = {
"title": "titleOnUpdate",
"detail": "detailOnUpdate"
};
let formData = (obj);
formProvider.updateForm(formId, formData).catch((error) => {
cole.log('FormAbility updateForm, error:' + JSO.stringify(error));
});
},
onVisibilityChange(newStatus) {
// 使用方发起可见或者不可见通知触发,提供方需要做相应的处理
cole.log('FormAbility onVisibilityChange');
},
onEvent(formId, message) {
// 若卡片支持触发事件,则需要覆写该方法并实现对事件的触发
cole.log('FormAbility onEvent');
},
onDestroy(formId) {
// 删除卡片实例数据
cole.log('FormAbility onDestroy');
},
onAcquireFormState(want) {
cole.log('FormAbility onAcquireFormState');
return formInfo.FormState.READY;
},
}
2.4.2 -> 配置卡片配置文件
卡片需要在应用配置文件config.json中进行配置。
- js模块,用于对应卡片的js相关资源,内部字段结构说明:
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
name | 表示JS Component的名字。该标签不可缺省,默认值为default。 | 字符串 | 不可缺省 |
pages | 表示JS Component的页面用于列举JS Component中每个页面的路由信息[页面路径+页面名称]。该标签不可缺省,取值为数组,数组第一个元素代表JS FA首页。 | 数组 | 不可缺省 |
window | 用于定义与显示窗口相关的配置。 | 对象 | 可缺省 |
type | 表示JS应用的类型。取值范围如下: normal:标识该JS Component为应用实例。 form:标识该JS Component为卡片实例。 | 字符串 | 可缺省,缺省值为“normal” |
mode | 定义JS组件的开发模式。 | 对象 | 可缺省,缺省值为空 |
配置示例如下:
代码语言:javascript代码运行次数:0运行复制 "js": [{
"name": "widget",
"pages": ["pages/index/index"],
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
}]
- abilities模块,用于对应卡片的LifecycleForm,内部字段结构说明:
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
name | 表示卡片的类名。字符串最大长度为127字节。 | 字符串 | 不可缺省 |
description | 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 | 字符串 | 可缺省,缺省值为空 |
isDefault | 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。 true:默认卡片。 false:非默认卡片。 | 布尔值 | 不可缺省 |
type | 表示卡片的类型。取值范围如下: JS:JS卡片。 | 字符串 | 不可缺省 |
colorMode | 表示卡片的主题样式,取值范围如下: auto:自适应。 dark:深主题。 light:浅主题。 | 字符串 | 可缺省,缺省值为“auto” |
supportDimensi | 表示卡片支持的外观规格,取值范围: 1 * 2:表示1行2列的二宫格。 2 * 2:表示2行2列的四宫格。 2 * 4:表示2行4列的八宫格。 4 * 4:表示4行4列的十六宫格。 | 字符串数组 | 不可缺省 |
defaultDimension | 表示卡片的默认外观规格,取值必须在该卡片supportDimensi配置的列表中。 | 字符串 | 不可缺省 |
updateEnabled | 表示卡片是否支持周期性刷新,取值范围: true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。 false:表示不支持周期性刷新。 | 布尔值 | 不可缺省 |
scheduledUpdateTime | 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。 | 字符串 | 可缺省,缺省值为“0:0” |
updateDuration | 表示卡片定时刷新的更新周期,单位为0分钟,取值为自然数。 当取值为0时,表示该参数不生效。 当取值为正整数时,表示刷新周期为0*分钟。 | 数值 | 可缺省,缺省值为“0” |
formConfigAbility | 表示卡片的配置跳转链接,采用URI格式。 | 字符串 | 可缺省,缺省值为空 |
formVisibleotify | 标识是否允许卡片使用卡片可见性通知。 | 字符串 | 可缺省,缺省值为空 |
jsComponentame | 表示JS卡片的Component名称。字符串最大长度为127字节。 | 字符串 | 不可缺省 |
metaData | 表示卡片的自定义信息,包含customizeData数组标签。 | 对象 | 可缺省,缺省值为空 |
customizeData | 表示自定义的卡片信息。 | 对象数组 | 可缺省,缺省值为空 |
配置示例如下:
代码语言:javascript代码运行次数:0运行复制 "abilities": [{
"name": "FormAbility",
"description": "This is a FormAbility",
"formsEnabled": true,
"icon": "$media:icon",
"label": "$string:form_FormAbility_label",
"srcPath": "FormAbility",
"type": "service",
"srcLanguage": "ets",
"forms": [{
"colorMode": "auto",
"defaultDimension": "2*2",
"description": "This is a service widget.",
"formVisibleotify": true,
"isDefault": true,
"jsComponentame": "widget",
"name": "widget",
"scheduledUpdateTime": "10:0",
"supportDimensi": ["2*2"],
"type": "JS",
"updateDuration": 1,
"updateEnabled": true
}]
}]
2.4. -> 卡片信息的持久化
因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。
代码语言:javascript代码运行次数:0运行复制 onCreate(want) {
cole.log('FormAbility onCreate');
let formId = want.parameters["param.key.form_identity"];
let formame = want.parameters["param.key.form_name"];
let tempFlag = want.parameters["param.key.form_temporary"];
// 由开发人员自行实现,将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用
storeFormInfo(formId, formame, tempFlag, want);
let obj = {
"title": "titleOnCreate",
"detail": "detailOnCreate"
};
let formData = (obj);
return formData;
}
且需要适配onDestroy卡片删除通知接口,在其中实现卡片实例数据的删除。
需要注意的是,卡片使用方在请求卡片时传递给提供方应用的Want数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片:
常态卡片:卡片使用方会持久化的卡片;
临时卡片:卡片使用方不会持久化的卡片;
由于临时卡片的数据具有非持久化的特殊性,某些场景比如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。
2.4.4 -> 开发卡片页面
可以使用hml+css+json开发JS卡片页面:
说明
当前仅支持JS扩展的类Web开发范式来实现卡片的UI页面。
- hml:
<div class="container">
<stack>
<div class="container-img">
<image src="/common/widget.png" class="bg-img"></image>
</div>
<div class="container-inner">
<text class="title">{{title}}</text>
<text class="detail_text" onclick="routerEvent">{{detail}}</text>
</div>
</stack>
</div>
- css:
.container {
flex-direction: column;
justify-content: center;
align-items: center;
}
.bg-img {
flex-shrink: 0;
height: 100%;
}
.container-inner {
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
height: 100%;
width: 100%;
padding: 12px;
}
.title {
font-size: 19px;
font-weight: bold;
color: white;
text-overflow: ellipsis;
max-lines: 1;
}
.detail_text {
font-size: 16px;
color: white;
opacity: 0.66;
text-overflow: ellipsis;
max-lines: 1;
margin-top: 6px;
}
- json:
{
"data": {
"title": "TitleDefault",
"detail": "TextDefault"
},
"acti": {
"routerEvent": {
"action": "router",
"abilityame": "MyApplication.hmservice.FormAbility",
"params": {
"message": "add detail"
}
}
}
}
.1 -> 场景简介
WantAgent封装了一个行为意图信息,可以通过接口主动触发,也可以通过与通知绑定被动触发。
具体的行为包括:启动Ability和发布公共事件。
.2 -> 接口说明
接口名 | 接口描述 |
---|---|
getWantAgentInfo(info: WantAgentInfo, callback: AsyncCallback<WantAgent>) | 以AsyncCallback形式创建WantAgent对象 |
getWantAgent(info: WantAgentInfo): Promise<WantAgent> | 以Promise形式创建WantAgent对象 |
trigger(agent: WantAgent, triggerInfo: TriggerInfo, callback?: Callback<CompleteData>) | 触发WantAgent |
. -> 开发步骤
1. 导入WantAgent模块
代码语言:javascript代码运行次数:0运行复制import wantAgent from '@ohos.wantAgent';
2. 创建拉起Ability的WantAgentInfo信息
代码语言:javascript代码运行次数:0运行复制private wantAgentObj = null //用于保存创建成功的wantAgent对象,后续使用其完成触发的动作。
//wantAgentInfo
var wantAgentInfo = {
wants: [
{
deviceId: "",
bundleame: "test",
abilityame: "test.MainAbility",
action: "",
entities: [],
uri: "",
parameters: {}
}
],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags:[wantAgent.WantAgentFlags.COSTAT_FLAG]
}
. 创建发布公共事件的WantAgentInfo信息
代码语言:javascript代码运行次数:0运行复制private wantAgentObj = null //用于保存创建成功的WantAgent对象,后续使用其完成触发的动作。
//wantAgentInfo
var wantAgentInfo = {
wants: [
{
action: "event_name", // 设置事件名
parameters: {}
}
],
operationType: wantAgent.OperationType.SED_COMMO_EVET,
requestCode: 0,
wantAgentFlags:[wantAgent.WantAgentFlags.COSTAT_FLAG]
}
4. 创建WantAgent,保存返回的WantAgent对象wantAgentObj,用于执行后续触发操作。
代码语言:javascript代码运行次数:0运行复制//创建WantAgent
wantAgent.getWantAgent(wantAgentInfo, (err, wantAgentObj) => {
if () {
("[WantAgent]getWantAgent err=" + JSO.stringify(err))
} else {
cole.log("[WantAgent]getWantAgent success")
this.wantAgentObj = wantAgentObj
}
})
5. 触发WantAgent
代码语言:javascript代码运行次数:0运行复制//触发WantAgent
var triggerInfo = {
code:0
}
(wantAgentObj, triggerInfo, (completeData) => {
cole.log("[WantAgent]getWantAgent success, completeData: ", + JSO.stringify(completeData))
})
感谢各位大佬支持!!!
互三啦!!!
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-01-08,如有侵权请联系 cloudcommunity@tencent 删除harmonyos接口开发数据字符串#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
推荐阅读
留言与评论(共有 5 条评论) |
本站网友 阳高新闻 | 26分钟前 发表 |
Promise<WantAgent>以Promise形式创建WantAgent对象trigger(agent | |
本站网友 中山路租房 | 19分钟前 发表 |
19px; font-weight | |
本站网友 华亚 | 30分钟前 发表 |
字符串可缺省 | |
本站网友 穿越宇宙的少女 | 18分钟前 发表 |
"MyApplication.hmservice.FormAbility" |