1# PrintSpooler<a name="ZH-CN_TOPIC_0000001103330836"></a> 2 3- [简介](#section11660541593) 4 - [架构图](#section125101832114213) 5- [目录](#section161941989596) 6- [使用说明](#section123459000) 7- [相关仓](#section1371113476307) 8 9## 简介<a name="section11660541593"></a> 10 11PrintSpooler应用是OpenHarmony中预置的系统应用,为用户提供打印预览、发现和连接打印机、打印参数设置、下发打印任务以及打印任务状态的管理等功能。 12 13### 约束限制 14当前只支持对接支持ipp无驱动打印协议的打印机,需要单独安装驱动的打印机暂不支持对接。 15 16### 架构图<a name="section125101832114213"></a> 17 18 19 20## 目录<a name="section161941989596"></a> 21 22``` 23/applications/standard/print_spooler 24 ├── LICENSE # 许可文件 25 ├── common # 通用工具类目录 26 ├── entry # entry模块目录 27 ├── signature # 证书文件目录 28 ├── features # 子组件目录 29 │ ├── ippPrint # 局域网打印组件 30 31``` 32### 33 34## 基础开发说明 35### 资源引用 36#### 定义资源文件 37- 在 `src/main/resources/`目录下,根据不同的资源类型,定义资源文件。 38 39 ```json 40 { 41 "name": "default_background_color", 42 "value": "#F1F3F5" 43 }, 44 ``` 45#### 引用资源 46- 在有对应page的ets文件中,可直接通过`$r()`引用。 47 ```` JavaScript 48 @Provide backgroundColor: Resource = $r('app.color.default_background_color'); 49 ```` 50## 典型接口的使用 51打印框架启动打印界面: 52 53 ``` 54 std::string jobId = GetPrintJobId(); 55 auto printJob = std::make_shared<PrintJob>(); 56 if (printJob == nullptr) { 57 return E_PRINT_GENERIC_FAILURE; 58 } 59 printJob->SetFdList(fdList); 60 printJob->SetJobId(jobId); 61 printJob->SetJobState(PRINT_JOB_PREPARED); 62 AAFwk::Want want; 63 want.SetElementName(SPOOLER_BUNDLE_NAME, SPOOLER_ABILITY_NAME); 64 want.SetParam(LAUNCH_PARAMETER_JOB_ID, jobId); 65 want.SetParam(LAUNCH_PARAMETER_FILE_LIST, fileList); 66 BuildFDParam(fdList, want); 67 int32_t callerTokenId = static_cast<int32_t>(IPCSkeleton::GetCallingTokenID()); 68 std::string callerPkg = DelayedSingleton<PrintBMSHelper>::GetInstance()->QueryCallerBundleName(); 69 ingressPackage = callerPkg; 70 int32_t callerUid = IPCSkeleton::GetCallingUid(); 71 int32_t callerPid = IPCSkeleton::GetCallingPid(); 72 want.SetParam(AAFwk::Want::PARAM_RESV_CALLER_TOKEN, callerTokenId); 73 want.SetParam(AAFwk::Want::PARAM_RESV_CALLER_UID, callerUid); 74 want.SetParam(AAFwk::Want::PARAM_RESV_CALLER_PID, callerPid); 75 want.SetParam(CALLER_PKG_NAME, callerPkg); 76 if (!StartAbility(want)) { 77 PRINT_HILOGE("Failed to start spooler ability"); 78 return E_PRINT_SERVER_FAILURE; 79 } 80 ``` 81 82## 打印能力指南 83### 支持预览界面显示预览图并根据设置动态刷新 84- `entry/src/main/ets/Common/Utils/FileUtil.ts ` 85- 打开传入的图片uri,获得fd并生成imageSource 86 ``` 87 import Fileio from '@ohos.file.fs'; 88 import image from '@ohos.multimedia.image'; 89 90 file = Fileio.openSync(uri, Constants.READ_WRITE); 91 let imageSource = image.createImageSource(file.fd); 92 imageArray.push(new FileModel(<number> file.fd, <string> file.fd, <string> uri, 93 <number> imageInfo.size.width, <number> imageInfo.size.height, imageSource)); 94 ``` 95 96- `entry/src/main/ets/pages/component/PreviewComponent.ets` 97- 动态变量imageSorce数组更新时,触发handleImage,调用parseImageSize, 98- fdToPixelMap生成pixelMap更新this.currentPixelMap,Image组件显示图片 99- 彩色、无边距等设置项使用组件属性完成动态刷新; 100- 其它设置项变化事件中调用parseImageSize调整或重新加载图片。 101 ``` 102 @Link @Watch('handleImage') imageSources: Array<FileModel> 103 @State currentPixelMap: PixelMap = undefined; 104 105 Image(this.currentPixelMap).key('PreviewComponent_Image_currentPixelMap') 106 .width(this.canvasWidth).height(this.canvasHeight) 107 .backgroundColor($r('app.color.white')) 108 .objectFit(this.isBorderless?ImageFit.Cover:ImageFit.Contain) 109 .renderMode(this.colorMode === ColorMode.COLOR ? ImageRenderMode.Original : ImageRenderMode.Template) 110 111 handleImage(){ 112 Log.info(TAG,'handleImage'+this.imageSources.length) 113 this.checkCanvasWidth() 114 this.parseImageSize(false); 115 } 116 117 parseImageSize(isRendered: boolean) { 118 this.originalIndex = this.printRange[this.currentIndex - 1] 119 this.currentImage = this.imageSources[this.originalIndex - 1]; 120 if (CheckEmptyUtils.isEmpty(this.currentImage)){ 121 return; 122 } 123 if (!isRendered) { 124 this.fdToPixelMap(this.currentImage.fd); 125 } 126 let width = this.currentImage.width 127 let height = this.currentImage.height 128 if(width > height) { 129 this.imageOrientation = PageDirection.LANDSCAPE //图片横向 130 } else { 131 this.imageOrientation = PageDirection.VERTICAL //图片竖向 132 } 133 this.updateCanvasSize() 134 } 135 ``` 136### 支持打印任务管理,显示打印任务状态及错误信息 137-`entry/src/main/ets/pages/JobManagerPage.ets` 138-任务管理界面加载时,根据任务id创建本地任务,用@StorageLink动态监控打印任务队列变化,任务有更新后刷新界面显示; 139 140 ``` 141 @StorageLink('JobQueue') jobQueue: Array<PrintJob> = new Array(); 142 143 List() { 144 ForEach(this.jobQueue, (printJob:PrintJob)=>{ 145 ListItem(){ 146 printJobComponent({ mPrintJob: printJob}); 147 }.key(`JobManagerPage_ListItem_${printJob.jobId}`) 148 }, printJob=>printJob.jobId) 149 } 150 151 aboutToAppear() { 152 this.abilityContext = GlobalThisHelper.getValue<common.UIAbilityContext>(GlobalThisStorageKey.KEY_JOB_MANAGER_ABILITY_CONTEXT) 153 let data = { 154 wantJobId : Constants.STRING_NONE 155 } 156 this.abilityContext.eventHub.emit(Constants.EVENT_GET_ABILITY_DATA, data); 157 this.jobId = data.wantJobId; 158 this.adapter = PrintAdapter.getInstance(); 159 this.adapter.getPrintJobCtl().createPrintJob(this.jobId) 160 } 161 ``` 162 163- `entry/src/main/ets/Controller/PrintJobController.ets` 164- 初始化时将本地任务队列存入AppStorage,建立和界面@StorageLink的关联; 165- 通过print.on接口,传入回调监听任务状态更新事件; 166- 状态更新时通过更新本地任务队列,@StorageLink动态刷新界面 167 ``` 168 public init(): void { 169 AppStorageHelper.createValue<Array<PrintJob>>(this.getModel().mPrintJobs, AppStorageKeyName.JOB_QUEUE_NAME); 170 this.registerPrintJobCallback(); 171 } 172 173 private registerPrintJobCallback(): void { 174 print.on('jobStateChange', this.onJobStateChanged); 175 } 176 177 private onJobStateChanged = (state: print.PrintJobState, job: print.PrintJob): void => { 178 if (state === null || job === null) { 179 Log.error(TAG, 'device state changed null data'); 180 return; 181 } 182 this.deleteLocalSource(<number>state, <string>job.jobId); 183 switch (state) { 184 case PrintJobState.PRINT_JOB_PREPARED: 185 case PrintJobState.PRINT_JOB_QUEUED: 186 case PrintJobState.PRINT_JOB_RUNNING: 187 case PrintJobState.PRINT_JOB_BLOCKED: 188 case PrintJobState.PRINT_JOB_COMPLETED: 189 this.onPrintJobStateChange(job); 190 break; 191 default: 192 break; 193 } 194 }; 195 196 private onPrintJobStateChange(job: print.PrintJob): void { 197 if (job === null) { 198 return; 199 } 200 this.getModel().printJobStateChange(job.jobId, job.jobState, job.jobSubState); 201 } 202 ``` 203### 打印需支持mopria协议,支持p2p连接 204- `feature/ippPrint/src/main/ets/common/discovery/P2pDiscoveryChannel.ts` 205- 调用Wifi-P2p的接口发现周边的p2p打印机 206 ``` 207 import wifi from '@ohos.wifi'; 208 209 startDiscovery(callback: (found: boolean, peer: wifi.WifiP2pDevice) => void): void { 210 this.discoveryCallback = callback; 211 wifi.on('p2pPeerDeviceChange', this.updatePeerDevices); 212 this.startP2pDiscovery(); 213 this.registerWifiCommonEvent(); 214 215 216 this.discoverySleepTimer = setInterval(()=> { 217 Log.debug(TAG, 'native p2p service is sleep, start discovery'); 218 this.startP2pDiscovery(); 219 }, DISCOVERY_SLEEP); 220 } 221 222 private startP2pDiscovery(): void { 223 wifi.startDiscoverDevices(); 224 wifi.getP2pPeerDevices().then((peers: wifi.WifiP2pDevice[]) => { 225 this.updatePeerDevices(peers); 226 }); 227 } 228 ``` 229- `feature/ippPrint/src/main/ets/common/connect/P2pPrinterConnection.ts` 230- 发现p2p打印机之后,执行连接,获取对端打印机的ip信息 231 ``` 232 import wifi from '@ohos.wifi'; 233 234 private startConnect(printer: DiscoveredPrinter): void { 235 Log.debug(TAG, 'connect to ' + CommonUtils.getSecurityMac(printer.getDeviceAddress())); 236 let config: wifi.WifiP2PConfig = this.configForPeer(printer); 237 this.mWifiModel.registerWifiP2pEvent(WifiModel.p2pConnectionChange, this.p2pConnectionChangeReceive); 238 this.mWifiModel.registerWifiP2pEvent(WifiModel.p2pPeerDeviceChange, this.p2pPeersChangeReceive); 239 let connectionOperation: boolean = this.mWifiModel.connectToPrinter(config); 240 if (!connectionOperation) { 241 Log.error(TAG, 'connection operation failed'); 242 if (this.delayTimer !== undefined) { 243 clearTimeout(this.delayTimer); 244 } 245 this.mWifiModel.unregisterWifiP2pEvent(WifiModel.p2pConnectionChange, this.p2pConnectionChangeReceive); 246 this.mWifiModel.unregisterWifiP2pEvent(WifiModel.p2pPeerDeviceChange, this.p2pPeersChangeReceive); 247 this.mListener.onConnectionDelayed(); 248 return; 249 } 250 Log.error(TAG, 'connection operation success'); 251 } 252 ``` 253- `feature/ippPrint/src/main/ets/common/napi/NativeApi.ts` 254- p2p打印机连接成功之后调用print_print_fwk的接口获取打印机支持的ipp协议能力,并调用print_print_fwk接口向cupsd服务配置一台无驱动打印机(支持Mopria协议) 255 ``` 256 import { print } from '@kit.BasicServicesKit'; 257 258 public getCapabilities(uri: string, printerName: string, getCapsCallback: (result) => void): void { 259 Log.debug(TAG, 'getCapabilities enter'); 260 if (print === undefined) { 261 Log.error(TAG, 'print is undefined'); 262 getCapsCallback(ERROR); 263 return; 264 } 265 Log.debug(TAG, 'getCapabilities start'); 266 // 获取打印机的ipp打印能力 267 print.queryPrinterCapabilityByUri(uri).then((result) => { 268 Log.debug(TAG, 'nativeGetCapabilities result: ' + JSON.stringify(result)); 269 this.setCupsPrinter(uri, this.removeSpaces(printerName)); 270 getCapsCallback(result); 271 }).catch((error) => { 272 Log.error(TAG, 'nativeGetCapabilities error: ' + JSON.stringify(error)); 273 getCapsCallback(ERROR); 274 }); 275 Log.debug(TAG, 'getCapabilities end'); 276 } 277 278 public setCupsPrinter(uri: string, name: string): void { 279 Log.debug(TAG, 'setCupsPrinter enter'); 280 if (print === undefined) { 281 Log.error(TAG, 'print is undefined'); 282 return; 283 } 284 // 向cupsd服务配置无驱动打印机 285 print.addPrinterToCups(uri, name).then((result) => { 286 Log.debug(TAG, 'nativeSetCupsPrinter result: ' + JSON.stringify(result)); 287 }).catch((error) => { 288 Log.error(TAG, 'nativeSetCupsPrinter error: ' + JSON.stringify(error)); 289 }); 290 } 291 ``` 292- 打印框架代码详见:[print_print_fwk](https://gitee.com/openharmony/print_print_fwk) 293 294## 签名打包 295### 签名 296#### 签名文件的获取 2971. 拷贝OpenHarmony标准版 工程的 OpenHarmony\signcenter_tool 目录到操作目录 2982. 标准版的签名文件下载路径:https://gitee.com/openharmony/signcenter_tool?_from=gitee_search。 2993. PrintSpooler 工程的 signature\spooler.p7b 到该目录下 300#### 签名文件的配置 301打开项目工程,选择 File → Project Structure 302 303 304 305选择Project → Signing Configs,将对应的签名文件配置如下,完成后点击Apply,再点击OK。 306密码为生成签名文件时的密码,如果使用默认的签名文件,则使用默认密码123456。 307 308 309 310## 安装、运行、调试 311## 应用安装 312配置 hdc: 313进入SDK目录中的toolchains文件夹下,获取文件路径: 314 315 316 317> 注意,此处的hdc.exe如果版本较老,可能不能正常使用,需要获取新的hdc.exe文件 318> hdc命令介绍与下载详见:[hdc仓库地址](https://gitee.com/openharmony/developtools_hdc_standard) 319 320 321并将此路径配置到环境变量中: 322 323 324 325重启电脑使环境变量生效 326 327连接开发板,打开cmd命令窗口,执行hdc list targets,弹出窗口如下: 328 329 330 331等待一段时间后,窗口出现如下打印,可回到输入 hdc list targets 的命令窗口继续操作: 332 333 334 335再次输入hdc list targets,出现如下结果,说明hdc连接成功 336 337 338 339获取读写权限: 340 341``` 342hdc target mount 343``` 344将签名好的 hap 包放入设备的 `/system/app/com.ohos.spooler` 目录下,并修改hap包的权限 345 346``` 347hdc file send 本地路径 /system/app/com.ohos.spooler/hap包名称 348例如:hdc file send Spooler.hap /system/app/com.ohos.spooler/Spooler.hap 349``` 350## 应用运行 351Spooler属于系统应用,在将签名的 hap 包放入 `/system/app/com.ohos.spooler` 目录后,重启系统,应用会自动拉起。 352``` 353hdc shell 354reboot 355(不可以直接执行hdc reboot,命令是无效的) 356``` 357> 注意,如果设备之前安装过系统应用,则需要执行如下两条命令清除设备中存储的应用信息才能够在设备重启的时候将其装入设备的新 hap 包正常拉起。 358> ``` 359> hdc shell rm -rf /data/misc_de/0/mdds/0/default/bundle_manager_service 360> hdc shell rm -rf /data/accounts 361> ``` 362## 应用调试 363### log打印 364- 在程序中添加 log 365```JS 366import hilog from '@ohos.hilog'; 367hilog.info(0x0001, "Spooler", "%{public}s World %{private}d", "hello", 3); 368``` 369### log获取及过滤 370- log获取 371 372 373将log输出至文件 374``` 375hdc shell hilog > 输出文件名称 376``` 377 378例: 379在真实环境查看log,将全log输出到当前目录的hilog.log文件中 380``` 381hdc shell hilog > hilog.log 382``` 383 384- log过滤 385 386在命令行窗口中过滤log 387``` 388hilog │ grep 过滤信息 389``` 390 391例:过滤包含信息 Label 的 hilog 392``` 393hilog │ grep Label 394``` 395## 贡献代码 396### Fork 代码仓库 3971. 在码云上打开 PrintSpooler 代码仓库([仓库地址](https://gitee.com/openharmony/applications_print_spooler))。 398 3992. 点击仓库右上角的 Forked 按钮,在弹出的画面中,选择将仓库 fork 到哪里,点击确认。 400 4013. Fork 成功之后,会在自己的账号下看见 fork 的代码仓库。 402 403### 提交代码 4041. 访问开发者在码云账号上 fork 的代码仓库,点击“克隆/下载”按钮,选择 SSH/HTTPS,点击“复制”按钮。 405 4062. 在本地新建 PrintSpooler 目录,在 PrintSpooler 目录中执行如下命令 407 ``` 408 git clone 步骤1中复制的地址 409 ``` 410 4113. 修改代码。 412 413 > 将代码引入工程,以及编译工程等相关内容请参见 **3. 代码使用** 部分的相关内容。 4144. 提交代码到 fork 仓库。 415 > 修改后的代码,首先执行 `git add` 命令,然后执行 `git commit` 命令与 `git push` 命令,将代码 push 到开发者的 fork 仓中。 416 > 关于代码提交的这部分内容涉及 git 的使用,可以参照 [git官网](https://git-scm.com/) 的内容,在此不再赘述。 417 418### 发起 Pull Request (PR) 419在将代码提交到 fork 仓之后,开发者可以通过发起 Pull Request(PR)的方式来为 OpenHarmony 的相关项目贡献代码。 420 4211. 打开 fork 仓库。选择 `Pull Requests` → `新建 Pull Request` 422 4232. 在 `新建 Pull Request` 画面填入标题与说明,点击 `创建` 按钮。 424 4253. 创建 Pull Request 完成。 PR 创建完成后,会有专门的代码审查人员对代码进行评审,评审通过之后会合入相应的代码库。 426