<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Huawei Developers - Medium]]></title>
        <description><![CDATA[As Huawei Developers, our Medium publication where we share information about the use of Huawei Ecosystem - Medium]]></description>
        <link>https://medium.com/huawei-developers?source=rss----25443cf6c73---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Huawei Developers - Medium</title>
            <link>https://medium.com/huawei-developers?source=rss----25443cf6c73---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 22:46:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/huawei-developers" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[How to Use the Continuous Task in Background Tasks Kit to Obtain Location Data While the App Runs…]]></title>
            <link>https://medium.com/huawei-developers/how-to-use-the-continuous-task-in-background-tasks-kit-to-obtain-location-data-while-the-app-runs-40ee687662d1?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/40ee687662d1</guid>
            <category><![CDATA[huawei-watch]]></category>
            <category><![CDATA[harmony-os]]></category>
            <category><![CDATA[ark-ts]]></category>
            <category><![CDATA[background-task]]></category>
            <category><![CDATA[location-kit]]></category>
            <dc:creator><![CDATA[Egemen AYHAN]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 13:43:34 GMT</pubDate>
            <atom:updated>2026-11-06T13:43:33.740Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to Use the Continuous Task in Background Tasks Kit to Obtain Location Data While the App Runs in the Background?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j7h--i1-frW6tdpd" /><figcaption>Photo by <a href="https://unsplash.com/@fabioha?utm_source=medium&amp;utm_medium=referral">fabio</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Problem Description</h3><p>A continuous task keeps your process from being suspended while the app sits in the background. It doesn’t wake you up on a schedule. It only keeps you alive, so whatever loop or subscription you started keeps running. The example below uses background location tracking, which is the most common case.</p><p>Worth knowing before the code: the type you declare has to match the work you actually do. The system checks this, and if you declare location but never take a fix, it cancels the task within a couple of minutes. So pick the type that describes your real work, not the one that’s easiest to get.</p><h3>Background Knowledge</h3><p>A continuous task is requested through backgroundTaskManager.startBackgroundRunning, which takes a UIAbilityContext, an array of mode strings, and a WantAgent. Only a UIAbility can make this request, and it runs in the same process and on the same main thread as the rest of your page. No new process is created, so anything you start before backgrounding stays exactly where it was.</p><p>The mode strings come from the set of continuous task types the system defines: location, audioPlayback, audioRecording, dataTransfer, bluetoothInteraction, and others. Each one corresponds to a specific kind of user-perceivable work, and the system performs a consistency check against it. A task requested for one type while a different type of work is being done gets restricted.</p><p>Once the task starts, a persistent entry appears in the notification panel. The WantAgent you passed defines what happens when the user taps it. Deleting that notification cancels the task, which is why on(&#39;continuousTaskCancel&#39;) exists as a way to be told when this happens.</p><p>Location itself comes from geoLocationManager in @kit.LocationKit, subscribed through on(&#39;locationChange&#39;, request, callback) and released with the matching off.</p><h3>Solution</h3><p><strong>1. Declare the background mode</strong></p><p>In module.json5, add backgroundModes to the ability that will request the task. This has to go on the UIAbility itself, not on an extension, since only a UIAbility can request a continuous task.</p><pre>&quot;abilities&quot;: [<br>  {<br>    &quot;name&quot;: &quot;EntryAbility&quot;,<br>    &quot;srcEntry&quot;: &quot;./ets/entryability/EntryAbility.ets&quot;,<br>    &quot;description&quot;: &quot;$string:EntryAbility_desc&quot;,<br>    &quot;icon&quot;: &quot;$media:layered_image&quot;,<br>    &quot;label&quot;: &quot;$string:EntryAbility_label&quot;,<br>    &quot;backgroundModes&quot;: [&quot;location&quot;],<br>    &quot;startWindowIcon&quot;: &quot;$media:startIcon&quot;,<br>    &quot;startWindowBackground&quot;: &quot;$color:start_window_background&quot;,<br>    &quot;exported&quot;: true,<br>    &quot;skills&quot;: [<br>      {<br>        &quot;entities&quot;: [&quot;entity.system.home&quot;],<br>        &quot;actions&quot;: [&quot;ohos.want.action.home&quot;]<br>      }<br>    ]<br>  }<br>]</pre><p>The value in backgroundModes is specific to your task type. For location tracking it’s “location”, for background audio it’s “audioPlayback”, and so on. Declaring the wrong one, or leaving it out, makes startBackgroundRunning fail.</p><p><strong>2. Request the permissions</strong></p><p>Still in module.json5, at the module level, not inside the abilities array. Putting it in the wrong place compiles fine but silently registers nothing.</p><pre>&quot;requestPermissions&quot;: [<br>  { &quot;name&quot;: &quot;ohos.permission.KEEP_BACKGROUND_RUNNING&quot; },<br>  { &quot;name&quot;: &quot;ohos.permission.INTERNET&quot; },<br>  {<br>    &quot;name&quot;: &quot;ohos.permission.APPROXIMATELY_LOCATION&quot;,<br>    &quot;reason&quot;: &quot;$string:loc_reason&quot;,<br>    &quot;usedScene&quot;: { &quot;abilities&quot;: [&quot;EntryAbility&quot;], &quot;when&quot;: &quot;always&quot; }<br>  },<br>  {<br>    &quot;name&quot;: &quot;ohos.permission.LOCATION&quot;,<br>    &quot;reason&quot;: &quot;$string:loc_reason&quot;,<br>    &quot;usedScene&quot;: { &quot;abilities&quot;: [&quot;EntryAbility&quot;], &quot;when&quot;: &quot;always&quot; }<br>  },<br>  {<br>    &quot;name&quot;: &quot;ohos.permission.LOCATION_IN_BACKGROUND&quot;,<br>    &quot;reason&quot;: &quot;$string:loc_reason&quot;,<br>    &quot;usedScene&quot;: { &quot;abilities&quot;: [&quot;EntryAbility&quot;], &quot;when&quot;: &quot;always&quot; }<br>  },<br>  { &quot;name&quot;: &quot;ohos.permission.GET_NETWORK_INFO&quot; }<br>]</pre><p>KEEP_BACKGROUND_RUNNING is the one that gates the continuous task itself. Without it the request is rejected. The three location permissions are for the tracking work, and INTERNET plus GET_NETWORK_INFO are there because we’re sending data to a server.</p><p>Make sure loc_reason actually exists in resources/base/element/string.json. If the string is missing, the permission entry can be dropped without any visible error:</p><pre>{ &quot;name&quot;: &quot;loc_reason&quot;, &quot;value&quot;: &quot;Required for background location tracking&quot; }3. Ask the user at runtime</pre><p>Declaring a permission isn’t the same as having it. Location permissions are user-granted, so you have to prompt.</p><p>Request the foreground permissions first, then the background one in a separate call. Putting all three in one call tends to fail as a group. You get back authResults full of 2, which means the request was invalid rather than denied, and no dialog ever appears.</p><pre>private async askPermissions(): Promise&lt;boolean&gt; {<br>  const mgr = abilityAccessCtrl.createAtManager();<br><br>  const fg: Permissions[] = [<br>    &#39;ohos.permission.APPROXIMATELY_LOCATION&#39;,<br>    &#39;ohos.permission.LOCATION&#39;<br>  ];<br>  const r1 = await mgr.requestPermissionsFromUser(this.ctx, fg);<br>  hilog.info(DOMAIN, TAG, `fg perms=${JSON.stringify(r1.authResults)}`);<br><br>  if (!r1.authResults.every((v: number) =&gt; v === 0)) {<br>    this.status = `foreground permission denied: ${JSON.stringify(r1.authResults)}`;<br>    return false;<br>  }<br><br>  try {<br>    const r2 = await mgr.requestPermissionsFromUser(<br>      this.ctx, [&#39;ohos.permission.LOCATION_IN_BACKGROUND&#39;]);<br>    hilog.info(DOMAIN, TAG, `bg perm=${JSON.stringify(r2.authResults)}`);<br>  } catch (e) {<br>    hilog.warn(DOMAIN, TAG, `bg perm unavailable ${(e as BusinessError).code}`);<br>  }<br><br>  return true;<br>}</pre><p>In authResults, 0 means granted, -1 means denied, and 2 means the request itself was invalid. The background call is wrapped in try/catch because it isn’t available on every device type, wearables in particular. Failing that one isn’t fatal, so we log it and continue.</p><p><strong>4. Start the continuous task</strong></p><p>This is the core of it. You build a WantAgent, then hand it to startBackgroundRunning along with your context and the list of modes.</p><pre>private async startTask(): Promise&lt;boolean&gt; {<br>  const info: wantAgent.WantAgentInfo = {<br>    wants: [{ bundleName: BUNDLE, abilityName: &#39;EntryAbility&#39; }],<br>    actionType: wantAgent.OperationType.START_ABILITY,<br>    requestCode: 0,<br>    actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]<br>  };<br>  try {<br>    const agent: WantAgent = await wantAgent.getWantAgent(info);<br>    const modes: string[] = [&#39;location&#39;];<br>    const res: backgroundTaskManager.ContinuousTaskNotification =<br>      await backgroundTaskManager.startBackgroundRunning(this.ctx, modes, agent);<br>    hilog.info(DOMAIN, TAG, `task started, notificationId=${res.notificationId}`);<br>    this.status = &#39;continuous task active&#39;;<br>    return true;<br>  } catch (e) {<br>    const err = e as BusinessError;<br>    hilog.error(DOMAIN, TAG, `start failed ${err.code} ${err.message}`);<br>    this.status = `start failed ${err.code}: ${err.message}`;<br>    return false;<br>  }<br>}</pre><p>Three details worth calling out.</p><p>The WantAgent describes what happens when the user taps the notification your task creates. Here it opens EntryAbility. That notification is not optional: it appears automatically once the task starts, and if the user swipes it away, the system cancels the task.</p><p>The context has to be a UIAbilityContext, which is what getUIContext().getHostContext() returns. Pass the same context to stopBackgroundRunning later.</p><p>The modes array has to match what you put in backgroundModes in step 1. A mismatch here is a common reason for the call to fail.</p><p>The return value carries the notification id. You don’t have to capture it, and a plain await backgroundTaskManager.startBackgroundRunning(ctx, modes, agent) works just as well. It&#39;s useful for two things: confirming in the log that the task really started, and updating the notification content later through Notification Kit.</p><p>Stopping is a single call:</p><pre>async stop(): Promise&lt;void&gt; {<br>    this.stopTimerAndLocation();<br>    try {<br>      await backgroundTaskManager.stopBackgroundRunning(this.ctx);<br>      hilog.info(DOMAIN, TAG, &#39;TASK STOPPED&#39;);<br>      this.status = &#39;durduruldu&#39;;<br>    } catch (e) {<br>      hilog.error(DOMAIN, TAG, `stop fail ${(e as BusinessError).code}`);<br>    }<br>    this.running = false;<br>  }</pre><p><strong>5. Start the actual work</strong></p><p>The continuous task keeps you alive but does nothing on its own. You still have to start the work, and it has to be the work you declared.</p><pre>private startLocation(): void {<br>  if (this.listening) {<br>    return;<br>  }<br>  const req: geoLocationManager.ContinuousLocationRequest = {<br>    interval: LOCATION_INTERVAL_SEC,<br>    locationScenario: geoLocationManager.UserActivityScenario.NAVIGATION<br>  };<br>  try {<br>    geoLocationManager.on(&#39;locationChange&#39;, req, this.onLocation);<br>    this.listening = true;<br>    hilog.info(DOMAIN, TAG, &#39;location subscribed&#39;);<br>  } catch (e) {<br>    const err = e as BusinessError;<br>    hilog.error(DOMAIN, TAG, `subscribe failed ${err.code} ${err.message}`);<br>    this.status = `location error ${err.code}`;<br>  }<br>}<br><br>private onLocation = (loc: geoLocationManager.Location): void =&gt; {<br>  const t: string = new Date().toTimeString().substring(0, 8);<br>  hilog.info(DOMAIN, TAG,<br>    `fix ${t} lat=${loc.latitude.toFixed(5)} lng=${loc.longitude.toFixed(5)}`);<br>  this.lastFix = `${t}  ${loc.latitude.toFixed(4)}, ${loc.longitude.toFixed(4)}`;<br>  // send the sample to your backend here<br>};</pre><p><strong>THE FULL PAGE</strong></p><pre>import { backgroundTaskManager } from &#39;@kit.BackgroundTasksKit&#39;;<br>import { wantAgent, WantAgent, abilityAccessCtrl, common, Permissions } from &#39;@kit.AbilityKit&#39;;<br>import { geoLocationManager } from &#39;@kit.LocationKit&#39;;<br>import { BusinessError } from &#39;@kit.BasicServicesKit&#39;;<br>import { hilog } from &#39;@kit.PerformanceAnalysisKit&#39;;<br><br>const TAG: string = &#39;CT_DEMO&#39;;<br>const DOMAIN: number = 0xF00D;<br>const BUNDLE: string = &#39;com.example.myapp&#39;;<br>const LOCATION_INTERVAL_SEC: number = 15;<br><br>@Entry<br>@Component<br>struct Index {<br>  @State running: boolean = false;<br>  @State status: string = &#39;idle&#39;;<br>  @State lastFix: string = &#39;-&#39;;<br><br>  private ctx: common.UIAbilityContext =<br>    this.getUIContext().getHostContext() as common.UIAbilityContext;<br>  private listening: boolean = false;<br><br>  build() {<br>    Column({ space: 12 }) {<br>      Text(this.running ? &#39;TRACKING&#39; : &#39;STOPPED&#39;)<br>        .fontSize(18)<br>        .fontColor(this.running ? &#39;#27ae60&#39; : &#39;#888&#39;)<br><br>      Button(this.running ? &#39;Stop&#39; : &#39;Start&#39;)<br>        .width(&#39;85%&#39;)<br>        .onClick(() =&gt; {<br>          if (this.running) {<br>            this.stop();<br>          } else {<br>            this.start();<br>          }<br>        })<br><br>      Text(this.lastFix).fontSize(13).fontColor(&#39;#666&#39;)<br>      Text(this.status).fontSize(11).fontColor(&#39;#999&#39;)<br>    }<br>    .width(&#39;100%&#39;).height(&#39;100%&#39;).padding(20)<br>  }<br><br>  aboutToAppear(): void {<br>    try {<br>      backgroundTaskManager.on(&#39;continuousTaskCancel&#39;, this.onCancel);<br>    } catch (e) {<br>      hilog.error(DOMAIN, TAG, `on cancel failed ${(e as BusinessError).code}`);<br>    }<br>  }<br><br>  aboutToDisappear(): void {<br>    try {<br>      backgroundTaskManager.off(&#39;continuousTaskCancel&#39;, this.onCancel);<br>    } catch (e) { /* ignore */ }<br>    this.stop();<br>  }<br><br>  private onCancel = (info: backgroundTaskManager.ContinuousTaskCancelInfo): void =&gt; {<br>    hilog.warn(DOMAIN, TAG, `task cancelled id=${info.id} reason=${info.reason}`);<br>    this.status = `cancelled by system, reason=${info.reason}`;<br>    this.stopLocation();<br>    this.running = false;<br>  };<br><br>  async start(): Promise&lt;void&gt; {<br>    if (!geoLocationManager.isLocationEnabled()) {<br>      this.status = &#39;location is off in device settings&#39;;<br>      return;<br>    }<br><br>    const granted: boolean = await this.askPermissions();<br>    if (!granted) {<br>      return;<br>    }<br><br>    const started: boolean = await this.startTask();<br>    if (!started) {<br>      return;<br>    }<br><br>    this.startLocation();<br>    this.running = true;<br>  }<br><br>  private async askPermissions(): Promise&lt;boolean&gt; {<br>    const mgr = abilityAccessCtrl.createAtManager();<br><br>    const fg: Permissions[] = [<br>      &#39;ohos.permission.APPROXIMATELY_LOCATION&#39;,<br>      &#39;ohos.permission.LOCATION&#39;<br>    ];<br>    const r1 = await mgr.requestPermissionsFromUser(this.ctx, fg);<br>    hilog.info(DOMAIN, TAG, `fg perms=${JSON.stringify(r1.authResults)}`);<br><br>    if (!r1.authResults.every((v: number) =&gt; v === 0)) {<br>      this.status = `foreground permission denied: ${JSON.stringify(r1.authResults)}`;<br>      return false;<br>    }<br><br>    try {<br>      const r2 = await mgr.requestPermissionsFromUser(<br>        this.ctx, [&#39;ohos.permission.LOCATION_IN_BACKGROUND&#39;]);<br>      hilog.info(DOMAIN, TAG, `bg perm=${JSON.stringify(r2.authResults)}`);<br>    } catch (e) {<br>      hilog.warn(DOMAIN, TAG, `bg perm unavailable ${(e as BusinessError).code}`);<br>    }<br><br>    return true;<br>  }<br><br>  private async startTask(): Promise&lt;boolean&gt; {<br>    const info: wantAgent.WantAgentInfo = {<br>      wants: [{ bundleName: BUNDLE, abilityName: &#39;EntryAbility&#39; }],<br>      actionType: wantAgent.OperationType.START_ABILITY,<br>      requestCode: 0,<br>      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]<br>    };<br>    try {<br>      const agent: WantAgent = await wantAgent.getWantAgent(info);<br>      const modes: string[] = [&#39;location&#39;];<br>      const res: backgroundTaskManager.ContinuousTaskNotification =<br>        await backgroundTaskManager.startBackgroundRunning(this.ctx, modes, agent);<br>      hilog.info(DOMAIN, TAG, `task started, notificationId=${res.notificationId}`);<br>      this.status = &#39;continuous task active&#39;;<br>      return true;<br>    } catch (e) {<br>      const err = e as BusinessError;<br>      hilog.error(DOMAIN, TAG, `start failed ${err.code} ${err.message}`);<br>      this.status = `start failed ${err.code}: ${err.message}`;<br>      return false;<br>    }<br>  }<br><br>  private startLocation(): void {<br>    if (this.listening) {<br>      return;<br>    }<br>    const req: geoLocationManager.ContinuousLocationRequest = {<br>      interval: LOCATION_INTERVAL_SEC,<br>      locationScenario: geoLocationManager.UserActivityScenario.NAVIGATION<br>    };<br>    try {<br>      geoLocationManager.on(&#39;locationChange&#39;, req, this.onLocation);<br>      this.listening = true;<br>      hilog.info(DOMAIN, TAG, &#39;location subscribed&#39;);<br>    } catch (e) {<br>      const err = e as BusinessError;<br>      hilog.error(DOMAIN, TAG, `subscribe failed ${err.code} ${err.message}`);<br>      this.status = `location error ${err.code}`;<br>    }<br>  }<br><br>  private onLocation = (loc: geoLocationManager.Location): void =&gt; {<br>    const t: string = new Date().toTimeString().substring(0, 8);<br>    hilog.info(DOMAIN, TAG,<br>      `fix ${t} lat=${loc.latitude.toFixed(5)} lng=${loc.longitude.toFixed(5)}`);<br>    this.lastFix = `${t}  ${loc.latitude.toFixed(4)}, ${loc.longitude.toFixed(4)}`;<br>    // send the sample to your backend here<br>  };<br><br>  private stopLocation(): void {<br>    if (!this.listening) {<br>      return;<br>    }<br>    try {<br>      geoLocationManager.off(&#39;locationChange&#39;, this.onLocation);<br>      hilog.info(DOMAIN, TAG, &#39;location unsubscribed&#39;);<br>    } catch (e) {<br>      hilog.error(DOMAIN, TAG, `unsubscribe failed ${(e as BusinessError).code}`);<br>    }<br>    this.listening = false;<br>  }<br><br>  async stop(): Promise&lt;void&gt; {<br>    this.stopLocation();<br>    try {<br>      await backgroundTaskManager.stopBackgroundRunning(this.ctx);<br>      hilog.info(DOMAIN, TAG, &#39;task stopped&#39;);<br>      this.status = &#39;stopped&#39;;<br>    } catch (e) {<br>      hilog.error(DOMAIN, TAG, `stop failed ${(e as BusinessError).code}`);<br>    }<br>    this.running = false;<br>  }<br>}</pre><h3>Verification Result</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*6h90soFCMZBOAQdncqjqZQ.png" /><figcaption>Demo app</figcaption></figure><h3>References</h3><p><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/continuous-task">Document</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=40ee687662d1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-to-use-the-continuous-task-in-background-tasks-kit-to-obtain-location-data-while-the-app-runs-40ee687662d1">How to Use the Continuous Task in Background Tasks Kit to Obtain Location Data While the App Runs…</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How can I read Health Service data using Postman?]]></title>
            <link>https://medium.com/huawei-developers/how-can-i-read-health-service-data-using-postman-027068ae23ea?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/027068ae23ea</guid>
            <category><![CDATA[huawei-health-kit]]></category>
            <category><![CDATA[health-service-kit]]></category>
            <category><![CDATA[api-request]]></category>
            <category><![CDATA[harmony]]></category>
            <category><![CDATA[huawei-health-service]]></category>
            <dc:creator><![CDATA[Aycanur Ucar]]></dc:creator>
            <pubDate>Tue, 27 Oct 2026 12:08:07 GMT</pubDate>
            <atom:updated>2026-10-27T12:08:05.732Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nN-IvI6pxU2aISk4zskIWw.png" /><figcaption>Health Service Kit API Request</figcaption></figure><h3>Introduction</h3><p>This guide explains how to access Huawei Health Service data using Postman without integrating the Health Service SDK into an application. It covers the OAuth 2.0 authorization process, obtaining an access token, and using the token to call Health Service REST APIs. This approach is useful for testing API requests, verifying data access permissions, and troubleshooting integration issues.</p><h3>Implementation</h3><p><strong>Step 1. Generate the authorization URL</strong></p><p>Replace the <strong>client_id</strong>, <strong>redirect_uri</strong>, and <strong>scope</strong> values in the following URL with your own application information.</p><pre>https://oauth-login.cloud.huawei.com/oauth2/v3/authorize?response_type=code&amp;state=state_parameter_passthrough_value&amp;client_id=123456789&amp;redirect_uri=https%3A%2F%2Fwww.example.com&amp;scope=openid+https%3A%2F%2Fwww.huawei.com%2Fhealthkit%2Fheightweight.read+https%3A%2F%2Fwww.huawei.com%2Fhealthkit%2Fcalories.read&amp;access_type=offline&amp;display=touchCopy codeCopy code</pre><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/583/0*lcyx0SCeIqDUnX58.png" /><figcaption>Login Screen</figcaption></figure><ul><li>Replace client_id with your application’s Client ID.</li><li>Replace redirect_uri with your callback URL (URL-encoded).</li><li>Update the scope parameter with the permissions corresponding to the data types you want to read.</li></ul><p>For example:</p><ul><li>client_id: 123456789</li><li>redirect_uri: https%3A%2F%2Fwww.example.com</li><li>scope: openid <a href="https://www.huawei.com/healthkit/heightweight.read">https://www.huawei.com/healthkit/heightweight.read</a> <a href="https://www.huawei.com/healthkit/calories.read">https://www.huawei.com/healthkit/calories.read</a></li></ul><p>After signing in and granting the requested permissions, Huawei will redirect to your callback URL with an <strong>authorization code</strong>.</p><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/583/0*_ZK00VOCoCHHYl8F.png" /><figcaption>Token Request</figcaption></figure><p><strong>Step 2. Exchange the authorization code for an access token</strong></p><p>Use the authorization code returned in Step 1 to request an access token by calling the OAuth token API with the required request body.</p><p>The response will contain an <strong>access_token</strong>. Save this token, as it will be used for all subsequent Health Service REST API requests.</p><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/583/0*O9Mpw5CI4iPul9y9.png" /><figcaption>Token Response</figcaption></figure><p>For detailed authentication steps, refer to:</p><p><a href="https://developer.huawei.com/consumer/en/doc/HMSCore-Guides/auth-example-0000001054581058">https://developer.huawei.com/consumer/en/doc/HMSCore-Guides/auth-example-0000001054581058</a></p><p><strong>Step 3. Read Health Service data</strong></p><p>In Postman:</p><ul><li>Set <strong>Authorization</strong> to <strong>Bearer Token</strong>.</li><li>Paste the <strong>access_token</strong> obtained in Step 2.</li></ul><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/583/0*tYzsaQpux5d6IHW7.png" /><figcaption>Reading Data</figcaption></figure><p>Then call the corresponding Health Service REST API.</p><p>Specify the <strong>startTime</strong>, <strong>endTime</strong>, and the appropriate <strong>data type</strong> depending on the data you want to read.</p><p><strong>Important:</strong> The requests must be performed using the same Huawei account that has granted the required Health Service read permissions. Otherwise, the API may return an empty result even though the authentication is successful.</p><h3>Conclusion</h3><p>By following these steps, developers can authenticate with a Huawei Account, obtain an access token, and retrieve Health Service data directly through Postman. Successful data retrieval requires the appropriate read permissions, correct API parameters, and the same Huawei Account that granted the required permissions. This method provides a convenient way to validate Health Service API access and troubleshoot issues before implementing the integration in an application.</p><h3>References</h3><p><a href="https://developer.huawei.com/consumer/en/doc/HMSCore-Guides/menstrual-cycle-0000001253636829?source=post_page-----94311c9820fb-----------------------------------------">Document</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=027068ae23ea" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-can-i-read-health-service-data-using-postman-027068ae23ea">How can I read Health Service data using Postman?</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ArkWeb Error Handling in HarmonyOS NEXT Wearable Apps with ArkTS]]></title>
            <link>https://medium.com/huawei-developers/arkweb-error-handling-in-harmonyos-next-wearable-apps-with-arkts-baaea5b83ef5?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/baaea5b83ef5</guid>
            <category><![CDATA[ark-ts]]></category>
            <category><![CDATA[wearables]]></category>
            <category><![CDATA[error-handling]]></category>
            <category><![CDATA[harmony-os]]></category>
            <dc:creator><![CDATA[Atakan Yıldız]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 11:39:59 GMT</pubDate>
            <atom:updated>2026-10-21T11:39:57.957Z</atom:updated>
            <content:encoded><![CDATA[<h4>A practical, emulator-tested guide to ArkWeb error handling on HarmonyOS NEXT wearables.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g-sA9XfA5Dw8tPyEKl66Aw.jpeg" /><figcaption>ArkWeb error handling on a HarmonyOS NEXT wearable</figcaption></figure><h3>Introduction</h3><p>ArkWeb error handling becomes unusually visible on a wearable. Network failures, HTTP responses, certificate warnings, and authentication challenges all need a deterministic resolution, but a round display leaves little room for a browser-sized interface.</p><p>This article builds a wearable-compatible ArkWeb error lab in ArkTS. A 1 x 1 Web component remains mounted as the callback source, while an ArcList-based interface lets the developer trigger scenarios, resolve pending handlers, and inspect a compact event history.</p><p>The implementation was compiled with HarmonyOS 6.1.1 API 24 and exercised on an API 24 wearable emulator. It covers resource errors, HTTP status errors, SSL decisions, HTTP authentication, client-certificate requests, handler lifecycle, and verifiable results.</p><h3>Problem and Requirements</h3><p>The goal is not to display a full website. It is to observe and resolve ArkWeb failures correctly from a small wearable interface.</p><ul><li>Trigger resource-loading, HTTP, SSL, HTTP authentication, and client-certificate scenarios.</li><li>Keep the Web component alive without allowing it to dominate the circular UI.</li><li>Record lifecycle and error callbacks in the order they are received.</li><li>Resolve every handler exactly once.</li><li>Cancel pending handlers during navigation or page teardown.</li><li>Use current HarmonyOS NEXT ArkTS APIs without third-party dependencies.</li></ul><blockquote><em>A reliable error UI must preserve callback semantics. A transport failure, an HTTP 404 response, an SSL decision, and an authentication challenge are different events and require different handling.</em></blockquote><h3>Background and Technical Overview</h3><h4>Web component and controller lifecycle</h4><p>The Web component owns the ArkWeb instance and produces callbacks. WebviewController controls that instance. Construction alone does not attach the controller, so loadUrl must run only after onControllerAttached. Calling it too early can raise error 17100001.</p><h4>Transport failures and HTTP responses are different</h4><p>onErrorReceive reports loading failures such as DNS resolution, connection problems, and TLS failures. onHttpErrorReceive reports a valid HTTP response whose status indicates an error, such as 404 or 500. A 404 is still a server response and should not be modeled as a transport failure.</p><h4>Main-frame requests and subresources</h4><p>One page can request HTML, scripts, images, fonts, and other resources. Error callbacks can therefore run more than once. Use request.isMainFrame() when visible page state should react only to the top-level request, while still recording subresource failures for diagnostics.</p><h4>Handler-based callbacks</h4><p>SSL, HTTP authentication, and client-certificate callbacks provide decision handlers. The application must resolve each handler exactly once. The sample stores the active handler, presents a compact decision panel, and clears the reference after confirm, cancel, or ignore.</p><p>HttpAuthHandler.confirm() returning true only means that the handler accepted the invocation; it does not prove that the server accepted the credentials. Authentication success must be inferred from the callbacks that follow.</p><h4>API compatibility</h4><ul><li><strong>Web lifecycle, resource-error, and HTTP-error events:</strong> API 8; typed event callbacks used here are API 12.</li><li><strong>HTTP auth, SSL, and client-auth callbacks and handlers:</strong> API 9; typed event callbacks used here are API 12.</li><li><strong>WebviewController.loadUrl:</strong> API 9.</li><li><strong>onControllerAttached:</strong> API 10.</li><li><strong>ArcList and ArcListItem:</strong> API 18.</li><li><strong>SslErrorHandler.handleCancel(abortLoading):</strong> API 20.</li></ul><blockquote><em>The exact sample calls handleCancel(true), so API 20 is its effective minimum. It was built and tested at API 24.</em></blockquote><h3>Implementation</h3><h4>1. Define deterministic error scenarios</h4><p>Each scenario has a title, test URL, and retry flag. The invalid domain targets a resource error, httpbin returns HTTP 404 and HTTP Basic authentication challenges, and badssl provides controlled SSL and client-certificate endpoints.</p><pre>const SCENARIOS: Array&lt;ErrorScenario&gt; = [<br>  new ErrorScenario(&#39;Resource Error&#39;,<br>    &#39;add your invalid host address&#39;, true),<br>  new ErrorScenario(&#39;HTTP Error&#39;,<br>    &#39;add your 404 address&#39;, true),<br>  new ErrorScenario(&#39;SSL Error&#39;,<br>    &#39;add your bad SSL address&#39;, false),<br>  new ErrorScenario(&#39;HTTP Auth&#39;,<br>    &#39;add your HTTP auth address&#39;, false),<br>  new ErrorScenario(&#39;Client Auth&#39;,<br>    &#39;add your client certificate address&#39;, false)<br>];</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*0I9wmO18X1OrUlw5Xv15Cw.png" /><figcaption>The wearable scenario list with the hidden Web component active.</figcaption></figure><h4>2. Keep a minimal Web component mounted</h4><p>Create one WebviewController, attach it to a 1 x 1 Web component, disable JavaScript and DOM storage, and wait for onControllerAttached before navigating. Keeping a non-zero size and leaving the component mounted preserves the callback source.</p><pre>private readonly controller: webview.WebviewController =<br>  new webview.WebviewController();<br>private controllerAttached: boolean = false;<br> <br>Web({ src: this.currentUrl, controller: this.controller })<br>  .width(1)<br>  .height(1)<br>  .opacity(0.01)<br>  .javaScriptAccess(false)<br>  .domStorageAccess(false)<br>  .onControllerAttached(() =&gt; this.controllerAttached = true)</pre><h4>3. Capture the page lifecycle</h4><p>Track onPageBegin, onPageEnd, and onProgressChange. These callbacks describe navigation activity, not an independent success verdict. An error and onPageEnd may both appear during one navigation, which is why the UI keeps an ordered event history.</p><pre>.onPageBegin((event: OnPageBeginEvent) =&gt; {<br>  this.loading = true;<br>  this.status = &#39;Page loading started&#39;;<br>  this.addEvent(&#39;PAGE_BEGIN&#39;, event.url);<br>})<br>.onPageEnd((event: OnPageEndEvent) =&gt; {<br>  this.loading = false;<br>  this.progress = 100;<br>  this.status = &#39;Page loading finished&#39;;<br>  this.addEvent(&#39;PAGE_END&#39;, event.url);<br>})<br>.onProgressChange((event: OnProgressChangeEvent) =&gt; {<br>  this.progress = event.newProgress;<br>})</pre><h4>4. Handle resource-loading failures</h4><p>onErrorReceive exposes WebResourceRequest and WebResourceError. Record the code, message, and URL. Only clear the top-level loading state when request.isMainFrame() is true.</p><pre>.onErrorReceive((event: OnErrorReceiveEvent) =&gt; {<br>  if (event.request.isMainFrame()) {<br>    this.loading = false;<br>    this.status = &#39;Resource error received&#39;;<br>  }<br> <br>  this.addEvent(<br>    &#39;RESOURCE_ERROR&#39;,<br>    `${event.error.getErrorCode()} · ` +<br>      `${event.error.getErrorInfo()} · ` +<br>      event.request.getRequestUrl()<br>  );<br>})</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*3I2WnQcBvssS6gwAlz-UYg.png" /><figcaption>Resource error callback result.</figcaption></figure><h4>5. Handle HTTP error responses</h4><p>onHttpErrorReceive exposes the response status and reason message. Record the URL and status code while preserving the distinction between main-frame and subresource requests.</p><pre>.onHttpErrorReceive((event: OnHttpErrorReceiveEvent) =&gt; {<br>  if (event.request.isMainFrame()) {<br>    this.loading = false;<br>    this.status = &#39;HTTP error received&#39;;<br>  }<br> <br>  this.addEvent(<br>    &#39;HTTP_ERROR&#39;,<br>    `${event.response.getResponseCode()} ` +<br>      `${event.response.getReasonMessage()} · ` +<br>      event.request.getRequestUrl()<br>  );<br>})</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*NhnHy1fuEiwx_enHR23_xA.png" /><figcaption>HTTP 404 callback result.</figcaption></figure><h4>6. Resolve SSL certificate errors</h4><p>Store SslErrorHandler when onSslErrorEventReceive runs. Proceed calls handleConfirm(); Cancel calls handleCancel(true). Continuing past a certificate error is included only to demonstrate the callback branch.</p><pre>.onSslErrorEventReceive((event: OnSslErrorEventReceiveEvent) =&gt; {<br>  this.loading = false;<br>  this.sslHandler = event.handler;<br>  this.addEvent(&#39;SSL_ERROR&#39;, `Certificate error ${event.error}`);<br>  this.showPrompt(<br>    PromptType.SSL,<br>    &#39;SSL Warning&#39;,<br>    `Certificate error ${event.error}`<br>  );<br>})</pre><blockquote><em>Production applications should normally cancel certificate errors unless they implement a separately reviewed trust policy.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*dZhePpmEq1J1xuxiCTwWWw.png" /><figcaption>The SSL decision prompt on the wearable emulator.</figcaption></figure><h4>7. Resolve HTTP authentication</h4><p>Return true from onHttpAuthRequest to indicate that the application will handle the request. The decision panel supplies test credentials to HttpAuthHandler.confirm() or cancels the challenge.</p><pre>.onHttpAuthRequest((event: OnHttpAuthRequestEvent): boolean =&gt; {<br>  this.loading = false;<br>  this.httpAuthHandler = event.handler;<br>  this.addEvent(&#39;HTTP_AUTH&#39;, `${event.host} · ${event.realm}`);<br>  this.showPrompt(<br>    PromptType.HTTP_AUTH,<br>    &#39;Authentication Required&#39;,<br>    `${event.host} · ${event.realm}`<br>  );<br>  return true;<br>})</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*6YePF6eRQJ6Rjjv_PSuPuw.png" /><figcaption>HTTP authentication prompt.</figcaption></figure><h4>8. Resolve client-certificate requests</h4><p>Store ClientAuthenticationHandler and display the host, port, key types, and issuer count. This sample intentionally uses ignore() or cancel(); it does not package private keys or certificate chains.</p><pre>.onClientAuthenticationRequest(<br>  (event: OnClientAuthenticationEvent) =&gt; {<br>    this.loading = false;<br>    this.clientAuthHandler = event.handler;<br>    this.addEvent(<br>      &#39;CLIENT_AUTH&#39;,<br>      `${event.host}:${event.port} · ${event.keyTypes.join(&#39;, &#39;)}`<br>    );<br>    this.showPrompt(<br>      PromptType.CLIENT_AUTH,<br>      &#39;Client Certificate&#39;,<br>      `${event.host}:${event.port} · issuers ${event.issuers.length}`<br>    );<br>  }<br>)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*yr4BoCUWnL-Pr0Cz0eChFg.png" /><figcaption>Client-certificate prompt.</figcaption></figure><h4>9. Resolve every handler exactly once</h4><p>Starting another scenario or leaving the page cancels any unresolved handler. After a decision, the reference is cleared so no handler can be invoked twice.</p><pre>private cancelPendingHandler(): void {<br>  if (this.sslHandler !== undefined) {<br>    this.sslHandler.handleCancel(true);<br>    this.sslHandler = undefined;<br>  }<br>  if (this.httpAuthHandler !== undefined) {<br>    this.httpAuthHandler.cancel();<br>    this.httpAuthHandler = undefined;<br>  }<br>  if (this.clientAuthHandler !== undefined) {<br>    this.clientAuthHandler.cancel();<br>    this.clientAuthHandler = undefined;<br>  }<br>  this.clearPrompt();<br>}</pre><h4>10. Keep a bounded event history</h4><p>Prepend each event to a new array and keep the latest eight entries. Replacing the array ensures ArkUI observes the state update, while the limit keeps the wearable list compact.</p><pre>private addEvent(type: string, detail: string): void {<br>  const now: Date = new Date();<br>  const time: string =<br>    `${now.getHours().toString().padStart(2, &#39;0&#39;)}:` +<br>    `${now.getMinutes().toString().padStart(2, &#39;0&#39;)}:` +<br>    `${now.getSeconds().toString().padStart(2, &#39;0&#39;)}`;<br> <br>  const event: TraceEvent =<br>    new TraceEvent(this.nextEventId++, type, detail, time);<br>  const updated: Array&lt;TraceEvent&gt; = [event];<br> <br>  for (const existing of this.events) {<br>    if (updated.length === 8) {<br>      break;<br>    }<br>    updated.push(existing);<br>  }<br> <br>  this.events = updated;<br>}</pre><h3>Complete Runnable Example</h3><p>The focused snippets above explain one decision at a time. The files below are the complete implementation used by the tested sample, so the application can be reproduced without inventing missing imports, state, or helper methods.</p><p>Create an ArkTS <strong>Empty Ability</strong> project with these settings:</p><ul><li>Device type: wearable</li><li>Compatible SDK: HarmonyOS 6.1.1 API 24</li><li>Target SDK: HarmonyOS 6.1.1 API 24</li><li>Main page: pages/Index</li></ul><h4>entry/src/main/module.json5</h4><pre>{<br>  &quot;module&quot;: {<br>    &quot;name&quot;: &quot;entry&quot;,<br>    &quot;type&quot;: &quot;entry&quot;,<br>    &quot;description&quot;: &quot;$string:module_desc&quot;,<br>    &quot;mainElement&quot;: &quot;EntryAbility&quot;,<br>    &quot;deviceTypes&quot;: [<br>      &quot;wearable&quot;<br>    ],<br>    &quot;deliveryWithInstall&quot;: true,<br>    &quot;installationFree&quot;: false,<br>    &quot;pages&quot;: &quot;$profile:main_pages&quot;,<br>    &quot;requestPermissions&quot;: [<br>      {<br>        &quot;name&quot;: &quot;ohos.permission.INTERNET&quot;<br>      }<br>    ],<br>    &quot;abilities&quot;: [<br>      {<br>        &quot;name&quot;: &quot;EntryAbility&quot;,<br>        &quot;srcEntry&quot;: &quot;./ets/entryability/EntryAbility.ets&quot;,<br>        &quot;description&quot;: &quot;$string:EntryAbility_desc&quot;,<br>        &quot;icon&quot;: &quot;$media:layered_image&quot;,<br>        &quot;label&quot;: &quot;$string:EntryAbility_label&quot;,<br>        &quot;startWindowIcon&quot;: &quot;$media:startIcon&quot;,<br>        &quot;startWindowBackground&quot;: &quot;$color:start_window_background&quot;,<br>        &quot;exported&quot;: true,<br>        &quot;skills&quot;: [<br>          {<br>            &quot;entities&quot;: [<br>              &quot;entity.system.home&quot;<br>            ],<br>            &quot;actions&quot;: [<br>              &quot;ohos.want.action.home&quot;<br>            ]<br>          }<br>        ]<br>      }<br>    ],<br>    &quot;extensionAbilities&quot;: [<br>      {<br>        &quot;name&quot;: &quot;EntryBackupAbility&quot;,<br>        &quot;srcEntry&quot;: &quot;./ets/entrybackupability/EntryBackupAbility.ets&quot;,<br>        &quot;type&quot;: &quot;backup&quot;,<br>        &quot;exported&quot;: false,<br>        &quot;metadata&quot;: [<br>          {<br>            &quot;name&quot;: &quot;ohos.extension.backup&quot;,<br>            &quot;resource&quot;: &quot;$profile:backup_config&quot;<br>          }<br>        ],<br>      }<br>    ]<br>  }<br>}</pre><h4>entry/src/main/resources/base/profile/main_pages.json</h4><pre>{<br>  &quot;src&quot;: [<br>    &quot;pages/Index&quot;<br>  ]<br>}</pre><h4>entry/src/main/ets/entryability/EntryAbility.ets</h4><pre>import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from &#39;@kit.AbilityKit&#39;;<br>import { hilog } from &#39;@kit.PerformanceAnalysisKit&#39;;<br>import { window } from &#39;@kit.ArkUI&#39;;<br> <br>const DOMAIN = 0x0000;<br> <br>export default class EntryAbility extends UIAbility {<br>  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {<br>    try {<br>      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);<br>    } catch (err) {<br>      hilog.error(DOMAIN, &#39;testTag&#39;, &#39;Failed to set colorMode. Cause: %{public}s&#39;, JSON.stringify(err));<br>    }<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onCreate&#39;);<br>  }<br> <br>  onDestroy(): void {<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onDestroy&#39;);<br>  }<br> <br>  onWindowStageCreate(windowStage: window.WindowStage): void {<br>    // Main window is created, set main page for this ability<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onWindowStageCreate&#39;);<br> <br>    windowStage.loadContent(&#39;pages/Index&#39;, (err) =&gt; {<br>      if (err.code) {<br>        hilog.error(DOMAIN, &#39;testTag&#39;, &#39;Failed to load the content. Cause: %{public}s&#39;, JSON.stringify(err));<br>        return;<br>      }<br>      hilog.info(DOMAIN, &#39;testTag&#39;, &#39;Succeeded in loading the content.&#39;);<br>    });<br>  }<br> <br>  onWindowStageDestroy(): void {<br>    // Main window is destroyed, release UI related resources<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onWindowStageDestroy&#39;);<br>  }<br> <br>  onForeground(): void {<br>    // Ability has brought to foreground<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onForeground&#39;);<br>  }<br> <br>  onBackground(): void {<br>    // Ability has back to background<br>    hilog.info(DOMAIN, &#39;testTag&#39;, &#39;%{public}s&#39;, &#39;Ability onBackground&#39;);<br>  }<br>}</pre><h4>entry/src/main/ets/pages/Index.ets</h4><pre>import { webview } from &#39;@kit.ArkWeb&#39;;<br>import { ArcList, ArcListAttribute, ArcListItem, ArcListItemAttribute } from &#39;@kit.ArkUI&#39;;<br> <br>enum PromptType {<br>  NONE = &#39;NONE&#39;,<br>  SSL = &#39;SSL&#39;,<br>  HTTP_AUTH = &#39;HTTP_AUTH&#39;,<br>  CLIENT_AUTH = &#39;CLIENT_AUTH&#39;<br>}<br> <br>class ErrorScenario {<br>  readonly title: string;<br>  readonly url: string;<br>  readonly retryable: boolean;<br> <br>  constructor(<br>    title: string,<br>    url: string,<br>    retryable: boolean<br>  ) {<br>    this.title = title;<br>    this.url = url;<br>    this.retryable = retryable;<br>  }<br>}<br> <br>class TraceEvent {<br>  readonly id: number;<br>  readonly type: string;<br>  readonly detail: string;<br>  readonly time: string;<br> <br>  constructor(<br>    id: number,<br>    type: string,<br>    detail: string,<br>    time: string<br>  ) {<br>    this.id = id;<br>    this.type = type;<br>    this.detail = detail;<br>    this.time = time;<br>  }<br>}<br> <br>const SCENARIOS: Array&lt;ErrorScenario&gt; = [<br>  new ErrorScenario(&#39;Resource Error&#39;, &#39;add your invalid host address&#39;, true),<br>  new ErrorScenario(&#39;HTTP Error&#39;, &#39;add your 404 address&#39;, true),<br>  new ErrorScenario(&#39;SSL Error&#39;, &#39;add your bad SSL address&#39;, false),<br>  new ErrorScenario(&#39;HTTP Auth&#39;, &#39;add your HTTP auth address&#39;, false),<br>  new ErrorScenario(&#39;Client Auth&#39;, &#39;add your client certificate address&#39;, false)<br>];<br> <br>@Entry<br>@Component<br>struct Index {<br>  @State private currentUrl: string = &#39;about:blank&#39;;<br>  @State private status: string = &#39;Choose a scenario&#39;;<br>  @State private progress: number = 0;<br>  @State private loading: boolean = false;<br>  @State private events: Array&lt;TraceEvent&gt; = [];<br>  @State private promptType: PromptType = PromptType.NONE;<br>  @State private promptTitle: string = &#39;&#39;;<br>  @State private promptMessage: string = &#39;&#39;;<br>  @State private username: string = &#39;demo&#39;;<br>  @State private password: string = &#39;password&#39;;<br> <br>  private readonly controller: webview.WebviewController = new webview.WebviewController();<br>  private controllerAttached: boolean = false;<br>  private nextEventId: number = 1;<br>  private lastScenario: ErrorScenario | undefined = undefined;<br>  private sslHandler: SslErrorHandler | undefined = undefined;<br>  private httpAuthHandler: HttpAuthHandler | undefined = undefined;<br>  private clientAuthHandler: ClientAuthenticationHandler | undefined = undefined;<br> <br>  aboutToDisappear(): void {<br>    this.cancelPendingHandler();<br>  }<br> <br>  private runScenario(scenario: ErrorScenario): void {<br>    this.cancelPendingHandler();<br>    this.lastScenario = scenario;<br>    this.currentUrl = scenario.url;<br>    this.status = `Loading ${scenario.title}`;<br>    this.loading = true;<br>    this.progress = 0;<br>    if (this.controllerAttached) {<br>      try {<br>        this.controller.loadUrl(scenario.url);<br>      } catch (error) {<br>        this.loading = false;<br>        this.addEvent(&#39;LOAD_ERROR&#39;, JSON.stringify(error));<br>      }<br>    }<br>  }<br> <br>  private retry(): void {<br>    if (this.lastScenario === undefined || !this.lastScenario.retryable) {<br>      this.status = &#39;Retry is not available&#39;;<br>      return;<br>    }<br>    this.runScenario(this.lastScenario);<br>  }<br> <br>  private addEvent(type: string, detail: string): void {<br>    const now: Date = new Date();<br>    const time: string = `${now.getHours().toString().padStart(2, &#39;0&#39;)}:` +<br>      `${now.getMinutes().toString().padStart(2, &#39;0&#39;)}:` +<br>      `${now.getSeconds().toString().padStart(2, &#39;0&#39;)}`;<br>    const event: TraceEvent = new TraceEvent(this.nextEventId++, type, detail, time);<br>    const updated: Array&lt;TraceEvent&gt; = [event];<br>    for (const existing of this.events) {<br>      if (updated.length === 8) {<br>        break;<br>      }<br>      updated.push(existing);<br>    }<br>    this.events = updated;<br>  }<br> <br>  private showPrompt(type: PromptType, title: string, message: string): void {<br>    this.promptType = type;<br>    this.promptTitle = title;<br>    this.promptMessage = message;<br>    this.status = title;<br>  }<br> <br>  private confirmPrompt(): void {<br>    if (this.promptType === PromptType.SSL &amp;&amp; this.sslHandler !== undefined) {<br>      this.sslHandler.handleConfirm();<br>      this.sslHandler = undefined;<br>      this.status = &#39;SSL request continued&#39;;<br>    } else if (this.promptType === PromptType.HTTP_AUTH &amp;&amp; this.httpAuthHandler !== undefined) {<br>      const accepted: boolean = this.httpAuthHandler.confirm(this.username, this.password);<br>      this.httpAuthHandler = undefined;<br>      this.status = accepted ? &#39;Credentials accepted&#39; : &#39;Credentials rejected&#39;;<br>    }<br>    this.clearPrompt();<br>  }<br> <br>  private cancelPrompt(): void {<br>    this.cancelPendingHandler();<br>    this.status = &#39;Request cancelled&#39;;<br>  }<br> <br>  private ignoreClientAuth(): void {<br>    if (this.clientAuthHandler !== undefined) {<br>      this.clientAuthHandler.ignore();<br>      this.clientAuthHandler = undefined;<br>      this.status = &#39;Client certificate request ignored&#39;;<br>    }<br>    this.clearPrompt();<br>  }<br> <br>  private cancelPendingHandler(): void {<br>    if (this.sslHandler !== undefined) {<br>      this.sslHandler.handleCancel(true);<br>      this.sslHandler = undefined;<br>    }<br>    if (this.httpAuthHandler !== undefined) {<br>      this.httpAuthHandler.cancel();<br>      this.httpAuthHandler = undefined;<br>    }<br>    if (this.clientAuthHandler !== undefined) {<br>      this.clientAuthHandler.cancel();<br>      this.clientAuthHandler = undefined;<br>    }<br>    this.clearPrompt();<br>  }<br> <br>  private clearPrompt(): void {<br>    this.promptType = PromptType.NONE;<br>    this.promptTitle = &#39;&#39;;<br>    this.promptMessage = &#39;&#39;;<br>  }<br> <br>  @Builder<br>  private promptPanel() {<br>    Stack({ alignContent: Alignment.Center }) {<br>      Column({ space: 7 }) {<br>        Text(this.promptTitle)<br>          .fontSize(15)<br>          .fontWeight(FontWeight.Bold)<br>          .fontColor(&#39;#F8FAFC&#39;)<br>          .margin({ top: 20 })<br>        Text(this.promptMessage)<br>          .fontSize(9)<br>          .fontColor(&#39;#A9B8CC&#39;)<br>          .maxLines(2)<br>          .width(&#39;74%&#39;)<br>          .textAlign(TextAlign.Center)<br>        if (this.promptType === PromptType.HTTP_AUTH) {<br>          TextInput({ text: this.username, placeholder: &#39;Username&#39; })<br>            .width(&#39;70%&#39;)<br>            .height(34)<br>            .fontSize(11)<br>            .onChange((value: string) =&gt; this.username = value)<br>          TextInput({ text: this.password, placeholder: &#39;Password&#39; })<br>            .width(&#39;70%&#39;)<br>            .height(34)<br>            .fontSize(11)<br>            .type(InputType.Password)<br>            .onChange((value: string) =&gt; this.password = value)<br>        }<br>        Row({ space: 6 }) {<br>          if (this.promptType !== PromptType.CLIENT_AUTH) {<br>            Button(this.promptType === PromptType.SSL ? &#39;Proceed&#39; : &#39;Confirm&#39;)<br>              .height(34)<br>              .fontSize(10)<br>              .onClick(() =&gt; this.confirmPrompt())<br>          } else {<br>            Button(&#39;Ignore&#39;)<br>              .height(34)<br>              .fontSize(10)<br>              .onClick(() =&gt; this.ignoreClientAuth())<br>          }<br>          Button(&#39;Cancel&#39;)<br>            .height(34)<br>            .fontSize(10)<br>            .onClick(() =&gt; this.cancelPrompt())<br>        }<br>      }<br>      .width(&#39;86%&#39;)<br>      .padding({ left: 12, right: 12, bottom: 18 })<br>      .borderRadius(28)<br>      .backgroundColor(&#39;#172A46&#39;)<br>      .alignItems(HorizontalAlign.Center)<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .backgroundColor(&#39;#D907111F&#39;)<br>  }<br> <br>  build() {<br>    Stack({ alignContent: Alignment.Center }) {<br>      ArcList() {<br>        ArcListItem() {<br>          Column({ space: 3 }) {<br>            Text(&#39;WEB ERROR LAB&#39;)<br>              .fontSize(15)<br>              .fontWeight(FontWeight.Bold)<br>              .fontColor(&#39;#EAF2FF&#39;)<br>            Text(this.status)<br>              .fontSize(9)<br>              .fontColor(&#39;#79C8FF&#39;)<br>              .maxLines(1)<br>            if (this.loading) {<br>              Progress({ value: this.progress, total: 100, type: ProgressType.Linear })<br>                .width(&#39;64%&#39;)<br>                .height(3)<br>                .color(&#39;#38BDF8&#39;)<br>            }<br>          }<br>          .width(&#39;100%&#39;)<br>          .alignItems(HorizontalAlign.Center)<br>          .margin({ top: 20, bottom: 4 })<br>        }<br>        .height(62)<br> <br>        ForEach(SCENARIOS, (scenario: ErrorScenario) =&gt; {<br>          ArcListItem() {<br>            Button(scenario.title)<br>              .width(&#39;88%&#39;)<br>              .height(38)<br>              .fontSize(12)<br>              .fontColor(&#39;#EAF2FF&#39;)<br>              .backgroundColor(&#39;#17345A&#39;)<br>              .onClick(() =&gt; this.runScenario(scenario))<br>          }<br>          .height(42)<br>          .autoScale(true)<br>        }, (scenario: ErrorScenario) =&gt; scenario.title)<br> <br>        ForEach(this.events, (event: TraceEvent) =&gt; {<br>          ArcListItem() {<br>            Column({ space: 1 }) {<br>              Row() {<br>                Text(event.type)<br>                  .fontSize(9)<br>                  .fontWeight(FontWeight.Bold)<br>                  .fontColor(&#39;#7DD3FC&#39;)<br>                  .layoutWeight(1)<br>                Text(event.time).fontSize(8).fontColor(&#39;#718096&#39;)<br>              }<br>              .width(&#39;100%&#39;)<br>              Text(event.detail)<br>                .fontSize(8)<br>                .fontColor(&#39;#CBD5E1&#39;)<br>                .maxLines(2)<br>                .width(&#39;100%&#39;)<br>            }<br>            .width(&#39;88%&#39;)<br>            .padding({ left: 10, right: 10, top: 6, bottom: 6 })<br>            .borderRadius(14)<br>            .backgroundColor(&#39;#102033&#39;)<br>          }<br>          .height(55)<br>          .autoScale(true)<br>        }, (event: TraceEvent) =&gt; event.id.toString())<br> <br>        ArcListItem() {<br>          Row({ space: 6 }) {<br>            Button(&#39;Retry&#39;).height(34).fontSize(10).onClick(() =&gt; this.retry())<br>            Button(&#39;Clear&#39;).height(34).fontSize(10).onClick(() =&gt; this.events = [])<br>          }<br>          .width(&#39;100%&#39;)<br>          .justifyContent(FlexAlign.Center)<br>          .margin({ bottom: 30 })<br>        }<br>        .height(68)<br>      }<br>      .width(&#39;100%&#39;)<br>      .height(&#39;100%&#39;)<br>      .scrollBar(BarState.Off)<br>      .backgroundColor(&#39;#07111F&#39;)<br> <br>      Web({ src: this.currentUrl, controller: this.controller })<br>        .width(1)<br>        .height(1)<br>        .opacity(0.01)<br>        .javaScriptAccess(false)<br>        .domStorageAccess(false)<br>        .onControllerAttached(() =&gt; this.controllerAttached = true)<br>        .onPageBegin((event: OnPageBeginEvent) =&gt; {<br>          this.loading = true;<br>          this.status = &#39;Page loading started&#39;;<br>          this.addEvent(&#39;PAGE_BEGIN&#39;, event.url);<br>        })<br>        .onPageEnd((event: OnPageEndEvent) =&gt; {<br>          this.loading = false;<br>          this.progress = 100;<br>          this.status = &#39;Page loading finished&#39;;<br>          this.addEvent(&#39;PAGE_END&#39;, event.url);<br>        })<br>        .onProgressChange((event: OnProgressChangeEvent) =&gt; this.progress = event.newProgress)<br>        .onErrorReceive((event: OnErrorReceiveEvent) =&gt; {<br>          if (event.request.isMainFrame()) {<br>            this.loading = false;<br>            this.status = &#39;Resource error received&#39;;<br>          }<br>          this.addEvent(<br>            &#39;RESOURCE_ERROR&#39;,<br>            `${event.error.getErrorCode()} · ${event.error.getErrorInfo()} · ${event.request.getRequestUrl()}`<br>          );<br>        })<br>        .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) =&gt; {<br>          if (event.request.isMainFrame()) {<br>            this.loading = false;<br>            this.status = &#39;HTTP error received&#39;;<br>          }<br>          this.addEvent(<br>            &#39;HTTP_ERROR&#39;,<br>            `${event.response.getResponseCode()} ${event.response.getReasonMessage()} · ` +<br>              event.request.getRequestUrl()<br>          );<br>        })<br>        .onSslErrorEventReceive((event: OnSslErrorEventReceiveEvent) =&gt; {<br>          this.loading = false;<br>          this.sslHandler = event.handler;<br>          this.addEvent(&#39;SSL_ERROR&#39;, `Certificate error ${event.error}`);<br>          this.showPrompt(PromptType.SSL, &#39;SSL Warning&#39;, `Certificate error ${event.error}`);<br>        })<br>        .onHttpAuthRequest((event: OnHttpAuthRequestEvent): boolean =&gt; {<br>          this.loading = false;<br>          this.httpAuthHandler = event.handler;<br>          this.addEvent(&#39;HTTP_AUTH&#39;, `${event.host} · ${event.realm}`);<br>          this.showPrompt(PromptType.HTTP_AUTH, &#39;Authentication Required&#39;, `${event.host} · ${event.realm}`);<br>          return true;<br>        })<br>        .onClientAuthenticationRequest((event: OnClientAuthenticationEvent) =&gt; {<br>          this.loading = false;<br>          this.clientAuthHandler = event.handler;<br>          this.addEvent(&#39;CLIENT_AUTH&#39;, `${event.host}:${event.port} · ${event.keyTypes.join(&#39;, &#39;)}`);<br>          this.showPrompt(<br>            PromptType.CLIENT_AUTH,<br>            &#39;Client Certificate&#39;,<br>            `${event.host}:${event.port} · issuers ${event.issuers.length}`<br>          );<br>        })<br> <br>      if (this.promptType !== PromptType.NONE) {<br>        this.promptPanel();<br>      }<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .backgroundColor(&#39;#07111F&#39;)<br>  }<br>}</pre><h3>Run the Sample</h3><p>Build the project from its root directory:</p><pre>hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon</pre><p>A signing profile is required only for installation on physical hardware. Configure it in DevEco Studio instead of copying another developer’s machine-specific signing values.</p><p>On the wearable emulator, run each scenario and verify the visible callback history and each handler decision panel.</p><h3>Testing and Results</h3><p>The implementation was validated as an application, not only as isolated code. It was clean-built with HarmonyOS 6.1.1 API 24, installed on a wearable emulator, and operated through the real interface.</p><ul><li><strong>Build:</strong> BUILD SUCCESSFUL with no ArkTS compiler warning.</li><li><strong>Environment:</strong> HarmonyOS 6.1.0.125 API 24 wearable emulator, 466 x 466 display.</li><li><strong>Application start:</strong> EntryAbility reached the foreground and ArkWeb started without a crash.</li><li><strong>Resource Error:</strong> During the recorded emulator run, onErrorReceive reported RESOURCE_ERROR, code -106, and ERR_INTERNET_DISCONNECTED for the intentionally invalid host. The exact transport code can vary with emulator connectivity; with an active network, an unresolvable host may instead produce a DNS-related error. The reliable assertion is that onErrorReceive fires for the main-frame request, not that every environment returns the same code.</li><li><strong>HTTP Error:</strong> onHttpErrorReceive recorded main-frame status 404; page begin and page end were also observed.</li><li><strong>SSL Error:</strong> onSslErrorEventReceive recorded certificate error 3. Proceed reached PAGE_END; Cancel also completed without a crash.</li><li><strong>HTTP Auth:</strong> onHttpAuthRequest reported httpbin.org and realm Fake Realm. The demo credentials resolved the challenge and reached PAGE_END.</li><li><strong>Client Auth:</strong> onClientAuthenticationRequest reported client.badssl.com:443, RSA/ECDSA key types, and one issuer. Ignore resolved the handler.</li></ul><blockquote><em>The result is driven by real ArkWeb callbacks. It does not simulate errors or progress with a timer.</em></blockquote><h3>Explanation: Why This Design Works</h3><ul><li>A persistent, non-zero Web component preserves the callback source.</li><li>Controller attachment prevents loadUrl from running before ArkWeb is ready.</li><li>Main-frame checks prevent subresource failures from replacing the primary page status.</li><li>Dedicated handler references make pending decisions explicit and cancellable.</li><li>A bounded immutable event update is predictable for ArkUI state observation and wearable memory use.</li><li>JavaScript and DOM storage stay disabled because this diagnostic sample does not require them.</li></ul><h3>Limitations and Production Considerations</h3><h4>Public test services</h4><p>httpbin.org and badssl.com are test dependencies, not production services. Availability, redirects, realm text, certificates, and subresource behavior can change.</p><h4>SSL security</h4><p>Proceeding through an SSL error weakens certificate protection for that request. The Proceed branch exists only for controlled testing. A production application should cancel unless it has a reviewed trust policy.</p><h4>Client-certificate credentials</h4><p>The sample does not call ClientAuthenticationHandler.confirm() because it does not ship a private key or certificate chain. Packaging such credentials inside an application bundle would be insecure.</p><h4>Resource cost</h4><p>The hidden 1 x 1 Web component still creates an ArkWeb instance and consumes memory, CPU, and network resources. It is not a substitute for a normal HTTP client.</p><h4>State and recovery</h4><p>The event history is held in memory, limited to eight entries, and not persisted across process recreation. The sample does not implement retry backoff, offline detection, custom error pages, certificate pinning, or certificate-chain inspection.</p><h4>Device validation</h4><p>Emulator verification does not replace testing on target hardware with its real certificate store, network policy, proxy configuration, authentication behavior, and performance characteristics.</p><h3>Conclusion</h3><p>Reliable ArkWeb error handling on a wearable depends less on visual browser space and more on correct callback semantics. Keeping a minimal Web component mounted, waiting for controller attachment, separating transport and HTTP failures, and resolving each handler exactly once produces a compact implementation that is both observable and safe to extend.</p><p>The resulting sample gives developers a practical baseline for ArkWeb diagnostics on HarmonyOS NEXT wearables. Production applications should retain this lifecycle discipline while replacing public endpoints, test credentials, and permissive SSL behavior with reviewed production policies.</p><blockquote><strong><em>Key takeaway:</em></strong><em> Keep the Web component mounted, wait for controller attachment, distinguish transport and HTTP failures, resolve every handler exactly once, and let verified callbacks drive the wearable UI.</em></blockquote><h3>Related Resources and References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-basic-components-web-events">Huawei Developers — Web component events</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-webview">Huawei Developers — WebviewController</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-basic-components-web-sslerrorhandler">Huawei Developers — SslErrorHandler</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-basic-components-web-httpauthhandler">Huawei Developers — HttpAuthHandler</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-basic-components-web-clientauthenticationhandler">Huawei Developers — ClientAuthenticationHandler</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-arclist">Huawei Developers — ArcList</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-arclistitem">Huawei Developers — ArcListItem</a></li><li><a href="https://developer.huawei.com/consumer/en/multidevice/wearables/get-started/">Huawei Developers — Wearable app development</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=baaea5b83ef5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/arkweb-error-handling-in-harmonyos-next-wearable-apps-with-arkts-baaea5b83ef5">ArkWeb Error Handling in HarmonyOS NEXT Wearable Apps with ArkTS</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Web Download Manager with ArkWeb on HarmonyOS NEXT]]></title>
            <link>https://medium.com/huawei-developers/building-a-web-download-manager-with-arkweb-on-harmonyos-next-5644aefda77b?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/5644aefda77b</guid>
            <category><![CDATA[ark-ts]]></category>
            <category><![CDATA[download]]></category>
            <category><![CDATA[harmony-os]]></category>
            <category><![CDATA[wearables]]></category>
            <dc:creator><![CDATA[Atakan Yıldız]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 11:39:06 GMT</pubDate>
            <atom:updated>2026-10-21T11:39:05.223Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to implement a real, state-driven ArkWeb download lifecycle in ArkTS for a wearable interface.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IlqWC-GanT0z3-rLB0Wccg.png" /><figcaption>State-driven ArkWeb download lifecycle on HarmonyOS NEXT</figcaption></figure><p>Downloading a file is easy until the interface must stay honest. A wearable user expects the progress ring, available actions, error state, and history to reflect the real transfer not a timer that merely looks convincing.</p><h3>Introduction</h3><p>ArkWeb provides the download lifecycle through <strong>WebviewController</strong>, <strong>WebDownloadDelegate</strong>, and <strong>WebDownloadItem</strong>. The engineering challenge is not starting the transfer. It is translating asynchronous callbacks into a small, predictable state machine that ArkUI can render safely.</p><p>This article demonstrates that design on HarmonyOS NEXT with ArkTS. It focuses on controller attachment, destination selection, progress propagation, action guards, cancellation semantics, and verifiable results.</p><h3>Problem and Requirements</h3><p>The download surface must:</p><ul><li>Start a network download through the active ArkWeb controller.</li><li>Accept the transfer into the application sandbox in onBeforeDownload.</li><li>Render real progress on a round, space-constrained display.</li><li>Guard pause, continue, cancel, restart, and download-again actions by current state.</li><li>Distinguish a user cancellation from a genuine failure.</li><li>Record completed, canceled, and failed transfers for the current session.</li></ul><h3>Background and Technical Overview</h3><p>WebviewController.startDownload(url) initiates a request. WebDownloadDelegate.onBeforeDownload then supplies the related WebDownloadItem. The application sets a destination and calls item.start(destination). Later callbacks deliver progress, completion, or interruption.</p><p>A WebviewController is not ready merely because it was constructed. It must be attached to a mounted Web component, and controller-dependent work must wait for onControllerAttached. A visually hidden Web component is therefore part of the architecture, not a decorative detail.</p><h3>API compatibility</h3><ul><li><strong>Web component:</strong> API 8.</li><li><strong>onControllerAttached:</strong> API 10.</li><li><strong>setDownloadDelegate, startDownload, callbacks, and transfer controls:</strong> API 11.</li><li><strong>UIAbilityContext.filesDir:</strong> API 9.</li></ul><blockquote><em>The runtime device must expose </em>SystemCapability.Web.Webview.Core<em>. Declaring a wearable device type does not add ArkWeb support to unsupported hardware.</em></blockquote><h3>State before UI</h3><p>The UI should not decide what a download means. The service owns one snapshot containing engine state, transfer state, progress, URL, file name, destination, and error message. Pages subscribe to that snapshot and derive labels and controls from it.</p><pre>export enum DownloadStatus {<br>  Idle = &#39;Idle&#39;,<br>  Downloading = &#39;Downloading&#39;,<br>  Paused = &#39;Paused&#39;,<br>  Canceled = &#39;Canceled&#39;,<br>  Completed = &#39;Completed&#39;,<br>  Failed = &#39;Failed&#39;<br>}</pre><h3>Implementation</h3><h3>1. Keep the Web engine alive</h3><p>The engine host owns one WebviewController. After attachment, it passes the controller and sandbox directory to the service. The Web component stays mounted in the root stack so navigation cannot destroy the engine.</p><pre>@Component<br>export struct DownloadEngineHost {<br>  private readonly webController: webview.WebviewController =<br>    new webview.WebviewController();<br><br>  build() {<br>    Web({<br>      src: &#39;about:blank&#39;,<br>      controller: this.webController<br>    })<br>      .height(1)<br>      .opacity(0.01)<br>      .onControllerAttached((): void =&gt; {<br>        const context = this.getUIContext().getHostContext()<br>          as common.UIAbilityContext;<br><br>        appWebDownloadService.initialize(<br>          this.webController,<br>          context.filesDir<br>        );<br>      });<br>  }<br>}</pre><h3>2. Attach one delegate to the active controller</h3><p>The service configures callbacks once and attaches the delegate. can throw setDownloadDelegate initialization error 17100001 when its controller is not associated with a Web component, so setup belongs after attachment and inside try/catch</p><pre>initialize(<br>  controller: webview.WebviewController,<br>  filesDir: string<br>): void {<br>  this.controller = controller;<br>  this.filesDir = filesDir;<br>  this.configureDelegate();<br><br>try {<br>    this.controller.setDownloadDelegate(this.downloadDelegate);<br>    this.snapshot.engineState = EngineState.Attached;<br>    this.notifySnapshot();<br>  } catch (error) {<br>    this.publish(DownloadStatus.Failed, 0,<br>      &#39;Unable to attach the delegate.&#39;);<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*PKAYKcL4xEX_f20QT1-Fcg.png" /><figcaption>The controller is attached and the Web engine is healthy.</figcaption></figure><h3>3. Initiate the request and prepare the destination</h3><p>A repeatable Download Again action must not collide with an existing file. Before initiating the request, the service creates a path inside filesDir, removes an existing same-name file, resets transfer flags, and publishes Idle. Because the destination is app-private, broad storage access is unnecessary.</p><pre>startDownload(sourceUrl: string, fileName: string): void {<br>  this.sourceUrl = sourceUrl;<br>  this.fileName = fileName;<br>  this.filePath = this.filesDir + &#39;/&#39; + this.fileName;<br>  this.cancelRequested = false;<br>  this.activityRecorded = false;<br><br>try {<br>    if (fileIo.accessSync(this.filePath)) {<br>      fileIo.unlinkSync(this.filePath);<br>    }<br>    this.publish(DownloadStatus.Idle, 0, &#39;&#39;);<br>    this.controller?.startDownload(this.sourceUrl);<br>  } catch (error) {<br>    this.publish(DownloadStatus.Failed, 0,<br>      &#39;Unable to start the download.&#39;);<br>  }<br>}</pre><h3>4. Accept the item and publish real progress</h3><p>The request becomes controllable when ArkWeb invokes onBeforeDownload. The service stores the item, accepts it at the prepared path, and publishes Downloading. Updates are real percentages from the item. Because the API can report a value outside the UI range, the example clamps it before sending it to the ring.</p><pre>this.downloadDelegate.onBeforeDownload(<br>  (item: webview.WebDownloadItem): void =&gt; {<br>    this.activeItem = item;<br><br>    try {<br>      item.start(this.filePath);<br>      this.publish(DownloadStatus.Downloading, 0, &#39;&#39;);<br>    } catch (error) {<br>      this.publish(<br>        DownloadStatus.Failed,<br>        0,<br>        &#39;Unable to create the file.&#39;<br>      );<br>    }<br>  }<br>);<br><br>this.downloadDelegate.onDownloadUpdated(<br>  (item: webview.WebDownloadItem): void =&gt; {<br>    this.activeItem = item;<br><br>    if (<br>      this.snapshot.status === DownloadStatus.Paused ||<br>      this.snapshot.status === DownloadStatus.Canceled<br>    ) {<br>      return;<br>    }<br><br>    this.publish(<br>      DownloadStatus.Downloading,<br>      Math.max(<br>        0,<br>        Math.min(100, item.getPercentComplete())<br>      ),<br>      &#39;&#39;<br>    );<br>  }<br>);</pre><blockquote><em>A late update callback must not overwrite a user-visible Paused or Canceled state with Downloading. The service, rather than the page, enforces that rule.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*EHsSQd2X90hPawoBHpOB9A.png" /><figcaption>The same engine drives a real active download.</figcaption></figure><h3>5. Map state to pause, continue, and cancel</h3><p>WebDownloadItem methods are state-sensitive. Pause requires a started task, while resume requires a paused task. Guarding each operation before calling ArkWeb prevents avoidable errors and keeps the visible state aligned with the item.</p><h3>Controlling the Active Transfer</h3><pre>pauseDownload(): void {<br>  if (<br>    this.activeItem === undefined ||<br>    this.snapshot.status !== DownloadStatus.Downloading<br>  ) {<br>    return;<br>  }<br><br>  this.activeItem.pause();<br>  this.publish(<br>    DownloadStatus.Paused,<br>    this.snapshot.progress,<br>    &#39;&#39;<br>  );<br>}<br><br>resumeDownload(): void {<br>  if (<br>    this.activeItem === undefined ||<br>    this.snapshot.status !== DownloadStatus.Paused<br>  ) {<br>    return;<br>  }<br><br>  this.activeItem.resume();<br>  this.publish(<br>    DownloadStatus.Downloading,<br>    this.snapshot.progress,<br>    &#39;&#39;<br>  );<br>}<br><br>cancelDownload(): void {<br>  if (this.activeItem === undefined) {<br>    return;<br>  }<br><br>  this.cancelRequested = true;<br>  this.activeItem.cancel();<br><br>  this.publish(<br>    DownloadStatus.Canceled,<br>    this.snapshot.progress,<br>    &#39;&#39;<br>  );<br><br>  this.recordActivity(DownloadStatus.Canceled);<br>}<br></pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*mFJLclytX30nJSgOwqWZSQ.png" /><figcaption>Paused keeps the last real percentage and exposes Continue.</figcaption></figure><h3>Cancellation is not a generic failure</h3><p>ArkWeb reports interrupted and canceled transfers through onDownloadFailed. Without an explicit cancellation flag, the interface would briefly show Canceled and then be overwritten by Failed. The service records user intent and preserves the last known progress when a canceled item reports zero.</p><pre>const progress: number = this.clampProgress(<br>  item.getPercentComplete()<br>);<br><br>if (this.cancelRequested) {<br>  this.publish(<br>    DownloadStatus.Canceled,<br>    Math.max(this.snapshot.progress, progress),<br>    &#39;&#39;<br>  );<br><br>  this.recordActivity(DownloadStatus.Canceled);<br>  return;<br>}<br><br>const message: string =<br>  &#39;Download failed (error &#39; +<br>  item.getLastErrorCode() +<br>  &#39;).&#39;;<br><br>this.publish(<br>  DownloadStatus.Failed,<br>  progress,<br>  message<br>);</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*Vk7L7Nhsugs4I7yB1J9qig.png" /><figcaption>Canceled is terminal and exposes Restart.</figcaption></figure><h3>Completion, Activity, and ArkUI Binding</h3><p>Completion is intentionally simple: publish 100 percent, clear the error, and record one terminal event. An activityRecorded guard prevents duplicate history rows.</p><pre>this.downloadDelegate.onDownloadFinish(<br>  (item: webview.WebDownloadItem): void =&gt; {<br>    this.activeItem = item;<br>    this.publish(DownloadStatus.Completed, 100, &#39;&#39;);<br>    this.recordActivity(DownloadStatus.Completed);<br>  }<br>);</pre><p>The page subscribes when it appears, copies snapshot fields into ArkUI state, and unsubscribes when it disappears. Button labels are pure functions of DownloadStatus: Downloading maps to Pause, Paused to Continue, Failed or Canceled to Restart, and Completed to Again.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*OIJmxDkZflIYCfrjShsY0g.png" /><figcaption>Terminal results are shown on the session Activity page.</figcaption></figure><h3>Complete Runnable Example</h3><p>The focused snippets above explain the download lifecycle one decision at a time. This section contains the complete files used by the tested sample, so the application can be reproduced without filling in missing classes, imports, state, or helper methods.</p><p>Create an ArkTS <strong>Empty Ability</strong> project with the following settings:</p><ul><li>Device type: wearable</li><li>Main page: pages/Index</li></ul><p>Keep the generated EntryAbility.ets and main_pages.json files unchanged.</p><h3>Required manifest change</h3><p>ArkWeb needs network access. Add the following property inside the generated module object in entry/src/main/module.json5:</p><pre>&quot;requestPermissions&quot;: [<br>  {<br>    &quot;name&quot;: &quot;ohos.permission.INTERNET&quot;<br>  }<br>]</pre><p>The project must also target a wearable. If the project was created with the wearable template, the generated module already contains this value:</p><pre>&quot;deviceTypes&quot;: [<br>  &quot;wearable&quot;<br>]</pre><h3>Project structure</h3><p>Add or replace these four files:</p><pre>entry/src/main/ets/<br>├── components/<br>│   └── DownloadEngineHost.ets<br>├── model/<br>│   └── DownloadModels.ets<br>├── pages/<br>│   └── Index.ets<br>└── service/<br>    └── AppWebDownloadService.ets</pre><p>entry/src/main/ets/model/DownloadModels.ets</p><pre>export enum DownloadStatus {<br>  Idle = &#39;Idle&#39;,<br>  Downloading = &#39;Downloading&#39;,<br>  Paused = &#39;Paused&#39;,<br>  Canceled = &#39;Canceled&#39;,<br>  Completed = &#39;Completed&#39;,<br>  Failed = &#39;Failed&#39;<br>}<br>export enum EngineState {<br>  Waiting = &#39;Waiting&#39;,<br>  Attached = &#39;Attached&#39;<br>}<br>export class DownloadSnapshot {<br>  engineState: EngineState = EngineState.Waiting;<br>  status: DownloadStatus = DownloadStatus.Idle;<br>  progress: number = 0;<br>  fileName: string = &#39;&#39;;<br>  filePath: string = &#39;&#39;;<br>  sourceUrl: string = &#39;&#39;;<br>  errorMessage: string = &#39;&#39;;<br>  copy(): DownloadSnapshot {<br>    const snapshot: DownloadSnapshot = new DownloadSnapshot();<br>    snapshot.engineState = this.engineState;<br>    snapshot.status = this.status;<br>    snapshot.progress = this.progress;<br>    snapshot.fileName = this.fileName;<br>    snapshot.filePath = this.filePath;<br>    snapshot.sourceUrl = this.sourceUrl;<br>    snapshot.errorMessage = this.errorMessage;<br>    return snapshot;<br>  }<br>}<br>export class DownloadActivityRecord {<br>  id: string = &#39;&#39;;<br>  title: string = &#39;&#39;;<br>  status: DownloadStatus = DownloadStatus.Idle;<br>  progress: number = 0;<br>  errorMessage: string = &#39;&#39;;<br>  createdAt: number = 0;<br>}<br>export type DownloadListener = (snapshot: DownloadSnapshot) =&gt; void;<br>export type ActivityListener = (records: Array&lt;DownloadActivityRecord&gt;) =&gt; void;</pre><p>entry/src/main/ets/service/AppWebDownloadService.ets</p><pre>import { webview } from &#39;@kit.ArkWeb&#39;;<br>import { fileIo } from &#39;@kit.CoreFileKit&#39;;<br>import {<br>  ActivityListener,<br>  DownloadActivityRecord,<br>  DownloadListener,<br>  DownloadSnapshot,<br>  DownloadStatus,<br>  EngineState<br>} from &#39;../model/DownloadModels&#39;;<br>export class AppWebDownloadService {<br>  private controller: webview.WebviewController | undefined = undefined;<br>  private activeItem: webview.WebDownloadItem | undefined = undefined;<br>  private filesDir: string = &#39;&#39;;<br>  private sourceUrl: string = &#39;&#39;;<br>  private fileName: string = &#39;&#39;;<br>  private filePath: string = &#39;&#39;;<br>  private snapshot: DownloadSnapshot = new DownloadSnapshot();<br>  private activityRecords: Array&lt;DownloadActivityRecord&gt; = [];<br>  private listeners: Array&lt;DownloadListener&gt; = [];<br>  private activityListeners: Array&lt;ActivityListener&gt; = [];<br>  private delegateConfigured: boolean = false;<br>  private cancelRequested: boolean = false;<br>  private activityRecorded: boolean = false;<br>  private readonly downloadDelegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();<br>  initialize(controller: webview.WebviewController, filesDir: string): void {<br>    this.controller = controller;<br>    this.filesDir = filesDir;<br>    this.configureDelegate();<br>    try {<br>      this.controller.setDownloadDelegate(this.downloadDelegate);<br>      this.snapshot.engineState = EngineState.Attached;<br>      this.notifySnapshot();<br>    } catch (error) {<br>      this.publish(DownloadStatus.Failed, 0, `Unable to attach the delegate: ${JSON.stringify(error)}`);<br>    }<br>  }<br>  subscribe(listener: DownloadListener): () =&gt; void {<br>    this.listeners.push(listener);<br>    listener(this.snapshot.copy());<br>    return (): void =&gt; {<br>      const index: number = this.listeners.indexOf(listener);<br>      if (index &gt;= 0) {<br>        this.listeners.splice(index, 1);<br>      }<br>    };<br>  }<br>  subscribeActivity(listener: ActivityListener): () =&gt; void {<br>    this.activityListeners.push(listener);<br>    listener(this.activityRecords.slice());<br>    return (): void =&gt; {<br>      const index: number = this.activityListeners.indexOf(listener);<br>      if (index &gt;= 0) {<br>        this.activityListeners.splice(index, 1);<br>      }<br>    };<br>  }<br>  startDownload(sourceUrl: string, fileName: string): void {<br>    if (this.controller === undefined || this.filesDir.length === 0) {<br>      this.publish(DownloadStatus.Failed, 0, &#39;The Web engine is not ready.&#39;);<br>      return;<br>    }<br>    this.sourceUrl = sourceUrl;<br>    this.fileName = fileName;<br>    this.filePath = `${this.filesDir}/${this.fileName}`;<br>    this.activeItem = undefined;<br>    this.cancelRequested = false;<br>    this.activityRecorded = false;<br>    try {<br>      if (fileIo.accessSync(this.filePath)) {<br>        fileIo.unlinkSync(this.filePath);<br>      }<br>      this.publish(DownloadStatus.Idle, 0, &#39;&#39;);<br>      this.controller.startDownload(this.sourceUrl);<br>    } catch (error) {<br>      this.publish(DownloadStatus.Failed, 0, `Unable to start: ${JSON.stringify(error)}`);<br>      this.recordActivity(DownloadStatus.Failed, this.snapshot.errorMessage);<br>    }<br>  }<br>  pauseDownload(): void {<br>    if (this.activeItem === undefined || this.snapshot.status !== DownloadStatus.Downloading) {<br>      return;<br>    }<br>    try {<br>      this.activeItem.pause();<br>      this.publish(DownloadStatus.Paused, this.snapshot.progress, &#39;&#39;);<br>    } catch (error) {<br>      this.publish(DownloadStatus.Failed, this.snapshot.progress, `Unable to pause: ${JSON.stringify(error)}`);<br>      this.recordActivity(DownloadStatus.Failed, this.snapshot.errorMessage);<br>    }<br>  }<br>  resumeDownload(): void {<br>    if (this.activeItem === undefined || this.snapshot.status !== DownloadStatus.Paused) {<br>      return;<br>    }<br>    try {<br>      this.activeItem.resume();<br>      this.publish(DownloadStatus.Downloading, this.snapshot.progress, &#39;&#39;);<br>    } catch (error) {<br>      this.publish(DownloadStatus.Failed, this.snapshot.progress, `Unable to continue: ${JSON.stringify(error)}`);<br>      this.recordActivity(DownloadStatus.Failed, this.snapshot.errorMessage);<br>    }<br>  }<br>  cancelDownload(): void {<br>    if (this.activeItem === undefined ||<br>      (this.snapshot.status !== DownloadStatus.Downloading &amp;&amp; this.snapshot.status !== DownloadStatus.Paused)) {<br>      return;<br>    }<br>    this.cancelRequested = true;<br>    try {<br>      this.activeItem.cancel();<br>      this.publish(DownloadStatus.Canceled, this.snapshot.progress, &#39;&#39;);<br>      this.recordActivity(DownloadStatus.Canceled);<br>    } catch (error) {<br>      this.cancelRequested = false;<br>      this.publish(DownloadStatus.Failed, this.snapshot.progress, `Unable to cancel: ${JSON.stringify(error)}`);<br>      this.recordActivity(DownloadStatus.Failed, this.snapshot.errorMessage);<br>    }<br>  }<br>  restartDownload(): void {<br>    if (this.sourceUrl.length === 0 || this.fileName.length === 0) {<br>      return;<br>    }<br>    this.startDownload(this.sourceUrl, this.fileName);<br>  }<br>  clearActivity(): void {<br>    this.activityRecords = [];<br>    this.notifyActivity();<br>  }<br>  private configureDelegate(): void {<br>    if (this.delegateConfigured) {<br>      return;<br>    }<br>    this.delegateConfigured = true;<br>    this.downloadDelegate.onBeforeDownload((item: webview.WebDownloadItem): void =&gt; {<br>      this.activeItem = item;<br>      try {<br>        item.start(this.filePath);<br>        this.publish(DownloadStatus.Downloading, 0, &#39;&#39;);<br>      } catch (error) {<br>        this.publish(DownloadStatus.Failed, 0, `Unable to create the file: ${JSON.stringify(error)}`);<br>        this.recordActivity(DownloadStatus.Failed, this.snapshot.errorMessage);<br>      }<br>    });<br>    this.downloadDelegate.onDownloadUpdated((item: webview.WebDownloadItem): void =&gt; {<br>      this.activeItem = item;<br>      if (this.snapshot.status === DownloadStatus.Paused || this.snapshot.status === DownloadStatus.Canceled) {<br>        return;<br>      }<br>      this.publish(DownloadStatus.Downloading, this.clampProgress(item.getPercentComplete()), &#39;&#39;);<br>    });<br>    this.downloadDelegate.onDownloadFinish((item: webview.WebDownloadItem): void =&gt; {<br>      this.activeItem = item;<br>      this.publish(DownloadStatus.Completed, 100, &#39;&#39;);<br>      this.recordActivity(DownloadStatus.Completed);<br>    });<br>    this.downloadDelegate.onDownloadFailed((item: webview.WebDownloadItem): void =&gt; {<br>      this.activeItem = item;<br>      const progress: number = this.clampProgress(item.getPercentComplete());<br>      if (this.cancelRequested) {<br>        this.publish(DownloadStatus.Canceled, Math.max(this.snapshot.progress, progress), &#39;&#39;);<br>        this.recordActivity(DownloadStatus.Canceled);<br>        return;<br>      }<br>      const message: string = `Download failed (error ${item.getLastErrorCode()}).`;<br>      this.publish(DownloadStatus.Failed, progress, message);<br>      this.recordActivity(DownloadStatus.Failed, message);<br>    });<br>  }<br>  private clampProgress(value: number): number {<br>    return Math.max(0, Math.min(100, value));<br>  }<br>  private publish(status: DownloadStatus, progress: number, errorMessage: string): void {<br>    this.snapshot.status = status;<br>    this.snapshot.progress = progress;<br>    this.snapshot.sourceUrl = this.sourceUrl;<br>    this.snapshot.fileName = this.fileName;<br>    this.snapshot.filePath = this.filePath;<br>    this.snapshot.errorMessage = errorMessage;<br>    this.notifySnapshot();<br>  }<br>  private notifySnapshot(): void {<br>    const snapshot: DownloadSnapshot = this.snapshot.copy();<br>    for (const listener of this.listeners) {<br>      listener(snapshot);<br>    }<br>  }<br>  private recordActivity(status: DownloadStatus, errorMessage: string = &#39;&#39;): void {<br>    if (this.activityRecorded) {<br>      return;<br>    }<br>    this.activityRecorded = true;<br>    const record: DownloadActivityRecord = new DownloadActivityRecord();<br>    record.id = `${Date.now()}`;<br>    record.title = this.fileName;<br>    record.status = status;<br>    record.progress = this.snapshot.progress;<br>    record.errorMessage = errorMessage;<br>    record.createdAt = Date.now();<br>    this.activityRecords = [record, ...this.activityRecords];<br>    this.notifyActivity();<br>  }<br>  private notifyActivity(): void {<br>    const records: Array&lt;DownloadActivityRecord&gt; = this.activityRecords.slice();<br>    for (const listener of this.activityListeners) {<br>      listener(records);<br>    }<br>  }<br>}<br>export const appWebDownloadService: AppWebDownloadService = new AppWebDownloadService();</pre><p>entry/src/main/ets/components/DownloadEngineHost.ets</p><pre>import { common } from &#39;@kit.AbilityKit&#39;;<br>import { webview } from &#39;@kit.ArkWeb&#39;;<br>import { appWebDownloadService } from &#39;../service/AppWebDownloadService&#39;;<br>@Component<br>export struct DownloadEngineHost {<br>  private readonly webController: webview.WebviewController = new webview.WebviewController();<br>  build() {<br>    Web({ src: &#39;about:blank&#39;, controller: this.webController })<br>      .width(1)<br>      .height(1)<br>      .opacity(0.01)<br>      .onControllerAttached((): void =&gt; {<br>        const context: common.UIAbilityContext =<br>          this.getUIContext().getHostContext() as common.UIAbilityContext;<br>        appWebDownloadService.initialize(this.webController, context.filesDir);<br>      });<br>  }<br>}</pre><p>entry/src/main/ets/pages/Index.ets</p><pre>import { ArcList, ArcListItem } from &#39;@kit.ArkUI&#39;;<br>import { DownloadEngineHost } from &#39;../components/DownloadEngineHost&#39;;<br>import { DownloadActivityRecord, DownloadSnapshot, DownloadStatus, EngineState } from &#39;../model/DownloadModels&#39;;<br>import { appWebDownloadService } from &#39;../service/AppWebDownloadService&#39;;<br>enum AppPage {<br>  Download = &#39;Download&#39;,<br>  Activity = &#39;Activity&#39;,<br>  System = &#39;System&#39;<br>}<br>const DOWNLOAD_URL: string = &#39;add your download address&#39;;<br>const DOWNLOAD_FILE_NAME: string = &#39;add your download name&#39;;<br>@Entry<br>@Component<br>struct Index {<br>  @State private page: AppPage = AppPage.Download;<br>  @State private engineState: EngineState = EngineState.Waiting;<br>  @State private status: DownloadStatus = DownloadStatus.Idle;<br>  @State private progressValue: number = 0;<br>  @State private fileName: string = &#39;&#39;;<br>  @State private errorMessage: string = &#39;&#39;;<br>  @State private records: Array&lt;DownloadActivityRecord&gt; = [];<br>  private unsubscribe: (() =&gt; void) | undefined = undefined;<br>  private unsubscribeActivity: (() =&gt; void) | undefined = undefined;<br>  aboutToAppear(): void {<br>    this.unsubscribe = appWebDownloadService.subscribe((snapshot: DownloadSnapshot): void =&gt; {<br>      this.engineState = snapshot.engineState;<br>      this.status = snapshot.status;<br>      this.progressValue = snapshot.progress;<br>      this.fileName = snapshot.fileName;<br>      this.errorMessage = snapshot.errorMessage;<br>    });<br>    this.unsubscribeActivity = appWebDownloadService.subscribeActivity(<br>      (records: Array&lt;DownloadActivityRecord&gt;): void =&gt; {<br>        this.records = records;<br>      }<br>    );<br>  }<br>  aboutToDisappear(): void {<br>    if (this.unsubscribe !== undefined) {<br>      this.unsubscribe();<br>    }<br>    if (this.unsubscribeActivity !== undefined) {<br>      this.unsubscribeActivity();<br>    }<br>  }<br>  private startDownload(): void {<br>    appWebDownloadService.startDownload(DOWNLOAD_URL, DOWNLOAD_FILE_NAME);<br>  }<br>  private statusColor(): string {<br>    if (this.status === DownloadStatus.Completed) {<br>      return &#39;#65E6A7&#39;;<br>    }<br>    if (this.status === DownloadStatus.Failed || this.status === DownloadStatus.Canceled) {<br>      return &#39;#FF8C8C&#39;;<br>    }<br>    if (this.status === DownloadStatus.Paused) {<br>      return &#39;#FFD166&#39;;<br>    }<br>    return &#39;#73C7FF&#39;;<br>  }<br>  private actionLabel(): string {<br>    if (this.status === DownloadStatus.Downloading) {<br>      return &#39;Pause&#39;;<br>    }<br>    if (this.status === DownloadStatus.Paused) {<br>      return &#39;Continue&#39;;<br>    }<br>    if (this.status === DownloadStatus.Canceled || this.status === DownloadStatus.Failed) {<br>      return &#39;Restart&#39;;<br>    }<br>    if (this.status === DownloadStatus.Completed) {<br>      return &#39;Again&#39;;<br>    }<br>    return &#39;Download&#39;;<br>  }<br>  private runPrimaryAction(): void {<br>    if (this.status === DownloadStatus.Downloading) {<br>      appWebDownloadService.pauseDownload();<br>    } else if (this.status === DownloadStatus.Paused) {<br>      appWebDownloadService.resumeDownload();<br>    } else if (this.status === DownloadStatus.Canceled || this.status === DownloadStatus.Failed ||<br>      this.status === DownloadStatus.Completed) {<br>      appWebDownloadService.restartDownload();<br>    } else {<br>      this.startDownload();<br>    }<br>  }<br>  private formatTime(timestamp: number): string {<br>    const date: Date = new Date(timestamp);<br>    return `${date.getHours().toString().padStart(2, &#39;0&#39;)}:` +<br>      `${date.getMinutes().toString().padStart(2, &#39;0&#39;)}`;<br>  }<br>  @Builder<br>  private header() {<br>    ArcListItem() {<br>      Column({ space: 2 }) {<br>        Text(&#39;WEB DOWNLOAD&#39;)<br>          .fontSize(15)<br>          .fontWeight(FontWeight.Bold)<br>          .fontColor(&#39;#F2F7FF&#39;)<br>        Text(this.page).fontSize(9).fontColor(&#39;#73C7FF&#39;)<br>      }<br>      .width(&#39;100%&#39;)<br>      .alignItems(HorizontalAlign.Center)<br>      .margin({ top: 20, bottom: 2 })<br>    }<br>    .height(58)<br>  }<br>  @Builder<br>  private navigation() {<br>    ArcListItem() {<br>      Row({ space: 5 }) {<br>        Button(&#39;Download&#39;).fontSize(9).height(32).onClick((): void =&gt; {<br>          this.page = AppPage.Download;<br>        })<br>        Button(&#39;Activity&#39;).fontSize(9).height(32).onClick((): void =&gt; {<br>          this.page = AppPage.Activity;<br>        })<br>        Button(&#39;System&#39;).fontSize(9).height(32).onClick((): void =&gt; {<br>          this.page = AppPage.System;<br>        })<br>      }<br>      .width(&#39;100%&#39;)<br>      .justifyContent(FlexAlign.Center)<br>    }<br>    .height(40)<br>  }<br>  @Builder<br>  private downloadPage() {<br>    ArcList() {<br>      this.header()<br>      ArcListItem() {<br>        Stack({ alignContent: Alignment.Center }) {<br>          Progress({ value: this.progressValue, total: 100, type: ProgressType.ScaleRing })<br>            .width(116)<br>            .height(116)<br>            .color(this.statusColor())<br>            .backgroundColor(&#39;#18314B&#39;)<br>          Column({ space: 2 }) {<br>            Text(`${Math.round(this.progressValue)}%`)<br>              .fontSize(22)<br>              .fontWeight(FontWeight.Bold)<br>              .fontColor(&#39;#F2F7FF&#39;)<br>            Text(this.status).fontSize(10).fontColor(this.statusColor())<br>          }<br>        }<br>        .width(&#39;100%&#39;)<br>      }<br>      .height(130)<br>      .autoScale(true)<br>      ArcListItem() {<br>        Column({ space: 3 }) {<br>          Text(this.fileName.length &gt; 0 ? this.fileName : &#39;Offline map package&#39;)<br>            .fontSize(11)<br>            .fontWeight(FontWeight.Medium)<br>            .fontColor(&#39;#E6EEF8&#39;)<br>          Text(this.engineState === EngineState.Attached ? &#39;Web engine ready&#39; : &#39;Preparing Web engine…&#39;)<br>            .fontSize(8)<br>            .fontColor(&#39;#8DA4BD&#39;)<br>          if (this.errorMessage.length &gt; 0) {<br>            Text(this.errorMessage)<br>              .fontSize(8)<br>              .fontColor(&#39;#FF9E9E&#39;)<br>              .maxLines(2)<br>              .textAlign(TextAlign.Center)<br>          }<br>        }<br>        .width(&#39;82%&#39;)<br>        .padding(8)<br>        .borderRadius(14)<br>        .backgroundColor(&#39;#10263C&#39;)<br>        .alignItems(HorizontalAlign.Center)<br>      }<br>      .height(this.errorMessage.length &gt; 0 ? 70 : 53)<br>      .autoScale(true)<br>      ArcListItem() {<br>        Row({ space: 7 }) {<br>          Button(this.actionLabel())<br>            .height(36)<br>            .fontSize(10)<br>            .enabled(this.engineState === EngineState.Attached)<br>            .onClick((): void =&gt; this.runPrimaryAction())<br>          if (this.status === DownloadStatus.Downloading || this.status === DownloadStatus.Paused) {<br>            Button(&#39;Cancel&#39;)<br>              .height(36)<br>              .fontSize(10)<br>              .backgroundColor(&#39;#7E3040&#39;)<br>              .onClick((): void =&gt; appWebDownloadService.cancelDownload())<br>          }<br>        }<br>        .width(&#39;100%&#39;)<br>        .justifyContent(FlexAlign.Center)<br>      }<br>      .height(50)<br>      this.navigation()<br>      ArcListItem() { Text(&#39;&#39;) }.height(32)<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .scrollBar(BarState.Off)<br>    .backgroundColor(&#39;#071522&#39;)<br>  }<br>  @Builder<br>  private activityPage() {<br>    ArcList() {<br>      this.header()<br>      if (this.records.length === 0) {<br>        ArcListItem() {<br>          Column({ space: 6 }) {<br>            Text(&#39;No activity yet&#39;).fontSize(14).fontColor(&#39;#E6EEF8&#39;)<br>            Text(&#39;Finished, canceled, and failed downloads appear here.&#39;)<br>              .fontSize(9)<br>              .fontColor(&#39;#8DA4BD&#39;)<br>              .width(&#39;72%&#39;)<br>              .textAlign(TextAlign.Center)<br>          }<br>          .width(&#39;100%&#39;)<br>          .alignItems(HorizontalAlign.Center)<br>        }<br>        .height(115)<br>      }<br>      ForEach(this.records, (record: DownloadActivityRecord) =&gt; {<br>        ArcListItem() {<br>          Column({ space: 3 }) {<br>            Row() {<br>              Text(record.title).fontSize(10).fontColor(&#39;#E6EEF8&#39;).layoutWeight(1).maxLines(1)<br>              Text(this.formatTime(record.createdAt)).fontSize(8).fontColor(&#39;#71869D&#39;)<br>            }<br>            .width(&#39;100%&#39;)<br>            Row() {<br>              Text(record.status).fontSize(9).fontColor(&#39;#73C7FF&#39;).layoutWeight(1)<br>              Text(`${Math.round(record.progress)}%`).fontSize(9).fontColor(&#39;#A9BDD1&#39;)<br>            }<br>            .width(&#39;100%&#39;)<br>            if (record.errorMessage.length &gt; 0) {<br>              Text(record.errorMessage).fontSize(8).fontColor(&#39;#FF9E9E&#39;).maxLines(1).width(&#39;100%&#39;)<br>            }<br>          }<br>          .width(&#39;82%&#39;)<br>          .padding(9)<br>          .borderRadius(15)<br>          .backgroundColor(&#39;#10263C&#39;)<br>        }<br>        .height(record.errorMessage.length &gt; 0 ? 70 : 56)<br>        .autoScale(true)<br>      }, (record: DownloadActivityRecord): string =&gt; record.id)<br>      if (this.records.length &gt; 0) {<br>        ArcListItem() {<br>          Button(&#39;Clear activity&#39;).height(34).fontSize(9)<br>            .onClick((): void =&gt; appWebDownloadService.clearActivity())<br>        }<br>        .height(45)<br>      }<br>      this.navigation()<br>      ArcListItem() { Text(&#39;&#39;) }.height(32)<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .scrollBar(BarState.Off)<br>    .backgroundColor(&#39;#071522&#39;)<br>  }<br>  @Builder<br>  private systemPage() {<br>    ArcList() {<br>      this.header()<br>      ArcListItem() {<br>        Column({ space: 8 }) {<br>          Text(this.engineState === EngineState.Attached ? &#39;✓&#39; : &#39;…&#39;)<br>            .fontSize(34)<br>            .fontColor(this.engineState === EngineState.Attached ? &#39;#65E6A7&#39; : &#39;#FFD166&#39;)<br>          Text(`Web Engine: ${this.engineState}`)<br>            .fontSize(13)<br>            .fontWeight(FontWeight.Bold)<br>            .fontColor(&#39;#F2F7FF&#39;)<br>          Text(this.engineState === EngineState.Attached ?<br>            &#39;System is running and healthy&#39; : &#39;Waiting for controller attachment&#39;)<br>            .fontSize(9)<br>            .fontColor(&#39;#8DA4BD&#39;)<br>        }<br>        .width(&#39;82%&#39;)<br>        .padding({ top: 16, bottom: 16 })<br>        .borderRadius(22)<br>        .backgroundColor(&#39;#10263C&#39;)<br>        .alignItems(HorizontalAlign.Center)<br>      }<br>      .height(142)<br>      .autoScale(true)<br>      this.navigation()<br>      ArcListItem() { Text(&#39;&#39;) }.height(32)<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .scrollBar(BarState.Off)<br>    .backgroundColor(&#39;#071522&#39;)<br>  }<br>  build() {<br>    Stack() {<br>      if (this.page === AppPage.Download) {<br>        this.downloadPage()<br>      } else if (this.page === AppPage.Activity) {<br>        this.activityPage()<br>      } else {<br>        this.systemPage()<br>      }<br>      DownloadEngineHost()<br>    }<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;);<br>  }<br>}</pre><h3>Run the sample</h3><p>Build and run the application on a wearable device or emulator that provides SystemCapability.Web.Webview.Core. Open the <strong>System</strong> page first and confirm that the engine state is <strong>Attached</strong>. Return to <strong>Download</strong>, start the transfer, and verify the Downloading, Paused, Canceled, Completed, and Activity states through the real ArkWeb callbacks.</p><h3>Testing and Results</h3><p>The implementation was validated as an application, not only as isolated code. The project was clean-built with HarmonyOS 6.1.1 API 24 SDK, installed on a wearable emulator, and operated through the real interface. A public 50 MB response was used for the completion run.</p><ul><li><strong>Clean build:</strong> assembleHap completed without ArkTS compiler errors or ArkTS code warnings.</li><li><strong>Environment:</strong> HarmonyOS 6.1.1 API 24 wearable emulator, 466 x 466 display.</li><li><strong>Engine setup:</strong> onControllerAttached completed and the System page reported Attached.</li><li><strong>Lifecycle:</strong> start, progress, pause, continue, cancel, restart, finish, and failure paths were exercised.</li><li><strong>File verification:</strong> offline_map.bin existed in the application files directory at exactly 50,000,000 bytes.</li><li><strong>UI verification:</strong> ring state, action labels, terminal state, and activity history matched the callbacks.</li></ul><blockquote><em>The displayed completion state corresponds to a real file written by ArkWeb, not to a simulated progress timer. The produced HAP remained unsigned because the Empty Ability project did not include a signing profile; the wearable emulator accepted the debug artifact.</em></blockquote><h3>Why this design is practical</h3><ul><li>The Web engine lifecycle is isolated from visible page navigation.</li><li>The page is a renderer and command surface, not the owner of transfer truth.</li><li>A single snapshot makes UI transitions deterministic and easy to inspect.</li><li>Cancellation intent is preserved across ArkWeb callback semantics.</li><li>The destination is predictable, private, and safe to replace for a restart.</li></ul><h3>Limitations and Production Considerations</h3><h4>One active transfer</h4><p>The singleton stores one active WebDownloadItem. Starting another transfer replaces the displayed URL, destination, flags, and item reference. A multi-download product should maintain a map keyed by item GUID and expose a collection of snapshots.</p><h4>Session-only history and recovery</h4><p>Activity records are kept in memory, and the active item is not serialized. The official ArkWeb guide documents serialization and WebDownloadManager.resume Download for recovery after process exit. Add persistence only when the product needs that lifecycle; do not imply recovery when it is not implemented.</p><h4>Background execution</h4><p>This sample does not request a continuous background-task mode. Behavior after the ability moves to the background or the process terminates is outside its guarantee. A production requirement for long-running background downloads must be designed and tested separately.</p><h4>Storage visibility and integrity</h4><p>filesDir is app-private. A user-visible export needs a picker or sharing flow. Production code should define free-space checks, filename sanitization, expected size or checksum validation, cleanup, and retention rules. The sample deletes an existing same-name file before starting again.</p><h4>Network and progress behavior</h4><p>The server controls transfer speed and resumability. A small response can finish before Pause is tapped. getPercentComplete can return -1 when progress is unknown. The demo endpoint is suitable for testing only; production should use an owned HTTPS endpoint with explicit retry and integrity policy.</p><h3>Conclusion</h3><p>A reliable Web Download Manager on HarmonyOS NEXT is less about drawing a progress ring and more about respecting ownership. ArkWeb owns the transfer lifecycle. The service translates that lifecycle into explicit application state. ArkUI renders the state and sends guarded commands back to the service.</p><p>Once those boundaries are clear, pause, continue, cancel, restart, completion, and activity tracking become small, understandable operations. The same pattern can scale beyond wearables: a phone or tablet UI may render more metadata, and a production service may persist serialized tasks or manage concurrent items, but the core rule remains the same, build the interface from verified callback state, not from assumptions.</p><blockquote><strong><em>Key takeaway:</em></strong><em> Keep the Web component mounted, wait for controller attachment, register one verified delegate, treat </em>WebDownloadItem<em> as the control handle, and let a state-driven service be the single source of truth for the UI.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*4pVQ4X_935XqTtgYG_8Viw.png" /><figcaption>The real finish callback produces a 100 percent state.</figcaption></figure><h3>Related Resources and References</h3><ol><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/web-download-V14">Huawei Developers: Downloading Files with ArkWeb</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-apis-webview-webviewcontroller">Huawei Developers: WebviewController reference</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-apis-webview-webdownloaddelegate">Huawei Developers: WebDownloadDelegate reference</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-apis-webview-webdownloaditem">Huawei Developers: WebDownloadItem reference</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-basic-components-web-events">Huawei Developers: Web component events</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-file-fs">Huawei Developers: File management APIs</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-declarative-ui-description">Huawei Developers: ArkTS declarative UI description</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5644aefda77b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/building-a-web-download-manager-with-arkweb-on-harmonyos-next-5644aefda77b">Building a Web Download Manager with ArkWeb on HarmonyOS NEXT</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Measure Storage on HarmonyOS in 2026?]]></title>
            <link>https://medium.com/huawei-developers/how-to-measure-storage-on-harmonyos-in-2026-5282ca486fa7?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/5282ca486fa7</guid>
            <category><![CDATA[wearables]]></category>
            <category><![CDATA[harmonyos-next]]></category>
            <category><![CDATA[ark-ts]]></category>
            <dc:creator><![CDATA[Doğa Şahin]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 06:26:16 GMT</pubDate>
            <atom:updated>2026-10-19T06:26:14.682Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*575n-ms73RETLnEHCfNx0g.png" /><figcaption><em>a storage screen on a HarmonyOS wearable, built with ArkTS.</em></figcaption></figure><h3>Introduction</h3><p>A watch with 5.9 GB of storage reports 5.3 GB free. Where did the other 0.6 GB go, and how much of it belongs to your app?</p><p>The ticket sounded simple: add a storage screen to a wearable app. It turned out to be three problems. HarmonyOS answers each one with a different API, and each API has its own return shape and availability rules.</p><p>This guide shows how to measure free space, app footprint and per-folder usage on HarmonyOS with ArkTS (the TypeScript-based language for HarmonyOS apps). It uses the Core File Kit, specifically statfs, storageStatistics and fileIo. The examples come from a sample project: one entry module, API 21, targeting phone and wearable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y37htMLDxy2zaNyR6LxMBw.png" /><figcaption><em>Figure 1. Three data sources run concurrently and merge into one storage snapshot.</em></figcaption></figure><h3>Three questions, three APIs</h3><ul><li><strong>How much space is left on the device?</strong> Use statfs.getFreeSize() and statfs.getTotalSize().</li><li><strong>How much of it does this app use?</strong> Use storageStatistics.getCurrentBundleStats().</li><li><strong>Which folders are growing?</strong> Use fileIo.listFile() and fileIo.stat(), walked recursively.</li></ul><p>Each API has a trap. Knowing them up front saves a lot of debugging.</p><p><strong>Trap 1: there is no “used space” API.</strong> statfs returns total and free size only. You compute used space as total - free.</p><p><strong>Trap 2: </strong><strong>storageStatistics may not exist on the device.</strong> It depends on SystemCapability.FileManagement.StorageService.SpatialStatistics, which a wearable is not required to declare. An unguarded call compiles but fails at runtime.</p><p><strong>Trap 3: the system figure does not say where the space went.</strong> getCurrentBundleStats() returns one number per category. That works for a summary but not for deciding what to delete.</p><p><strong>Trap 4: a naive recursive walk can hang or over-count.</strong> Directory sizes inflate the total, a symlink can cause an endless loop, and one unreadable file can abort the whole measurement.</p><h3>Background</h3><p>statfs (from @kit.CoreFileKit) wraps the POSIX statvfs call. It reports on the file system a path belongs to, not on a physical disk, so the path you pass decides the result. A normal app can only query paths it can read. Pass context.filesDir, which on a wearable sits on the user data partition. That is the figure a free-space screen should show.</p><p>storageStatistics returns a BundleStats object for the calling app:</p><ul><li>appSize: installation files (/data/storage/el1/bundle)</li><li>cacheSize: cache directories</li><li>dataSize: user data excluding the two above (base, database, distributedfiles)</li></ul><p>Querying your own bundle needs no permission. The other variants, getBundleStats(bundleName), getFreeSizeOfVolume and volumeManager, are restricted to system apps.</p><p>fileIo provides listFile and stat. For a regular file, Stat.size is the byte count. For a directory, it is the size of the inode record (the file system&#39;s metadata entry), not the size of its contents. This matters in Step 4.</p><h3>Step 1: Define shared data models</h3><p>The three APIs return unrelated shapes: two numbers, a BundleStats object and a tree you build by hand. I defined common models in model/StorageModels.ets: DeviceSpace, AppSpace, DirUsage and StorageSnapshot. An EmptyStorage factory gives the UI something to render before the first query returns.</p><p>AppSpace.available is false when the device lacks SpatialStatistics. The UI then shows -- instead of 0 B. 0 B would claim the app uses no space, while -- correctly says the value is unknown.</p><blockquote><strong>Pro Tip:</strong> Model “unavailable” explicitly. A flag like available costs one field and prevents the UI from showing a wrong number.</blockquote><h3>Step 2: Read total and free space with statfs</h3><pre>import { statfs } from &#39;@kit.CoreFileKit&#39;;</pre><pre>static async read(context: common.UIAbilityContext, path?: string): Promise&lt;DeviceSpace&gt; {<br>  const target: string = path ?? context.filesDir;<br>  const sizes: number[] = await Promise.all([<br>    statfs.getTotalSize(target),<br>    statfs.getFreeSize(target)<br>  ]);<br>  return {<br>    totalBytes: sizes[0],<br>    freeBytes: sizes[1],<br>    // statfs has no &quot;used&quot; call - used space is simply what is not free.<br>    usedBytes: Math.max(0, sizes[0] - sizes[1]),<br>    measuredPath: target<br>  };<br>}</pre><p>Synchronous variants exist (getTotalSizeSync, getFreeSizeSync). Avoid them in aboutToAppear. They block the UI thread, and they throw instead of rejecting, so they need their own try/catch.</p><h3>Step 3: Read app footprint behind a capability guard</h3><pre>import { storageStatistics } from &#39;@kit.CoreFileKit&#39;;</pre><pre>static async read(): Promise&lt;AppSpace&gt; {<br>  // The capability check has to appear literally here: the compiler does not accept<br>  // a boolean field holding the result of canIUse as a guard.<br>  if (canIUse(&#39;SystemCapability.FileManagement.StorageService.SpatialStatistics&#39;)) {<br>    try {<br>      const stats: storageStatistics.BundleStats = await storageStatistics.getCurrentBundleStats();<br>      return {<br>        appSize: stats.appSize,<br>        cacheSize: stats.cacheSize,<br>        dataSize: stats.dataSize,<br>        totalSize: stats.appSize + stats.cacheSize + stats.dataSize,<br>        available: true<br>      };<br>    } catch (error) {<br>      return EmptyStorage.app();          // available: false<br>    }<br>  }<br>  return EmptyStorage.app();              // capability absent - fall back to the walk<br>}</pre><p>A missing capability should not break the whole screen. If the module is absent, return an “unavailable” result and let the rest of the page render.</p><h3>canIUse must appear literally at the call site</h3><p>Caching the result in a field looks like good practice:</p><pre>private hasSpatial: boolean = canIUse(&#39;SystemCapability....&#39;);  // does not work as a guard</pre><p>It compiles, but every guarded call then reports “The API is not supported on all devices.” The compiler only recognises a literal canIUse call as a guard. The same applies to helper methods that construct guarded objects. Repeat the capability string at each call site.</p><blockquote><strong>Pro Tip:</strong> Do not wrap canIUse in a helper or a constant. It feels like duplication, but the compiler requires the literal call.</blockquote><h3>Step 4: Walk the sandbox directories</h3><p>Totals show how much space is used. Only a walk shows where. common/DirectorySizer.ets drives the recursion one level at a time, so each child directory gets its own byte count:</p><pre>import { fileIo } from &#39;@kit.CoreFileKit&#39;;</pre><pre>const entries: string[] = await fileIo.listFile(path);      // one level only<br>for (const entry of entries) {<br>  const childPath: string = `${path}/${entry}`;<br>  const stat: fileIo.Stat = await fileIo.stat(childPath);<br>  if (stat.isDirectory()) {<br>    if (maxDepth &lt;= 0) { totals.errorCount++; continue; }   // depth budget spent<br>    const child: DirUsage = await DirectorySizer.measure(childPath, entry, maxDepth - 1);<br>    node.children.push(child);<br>    // ...fold child totals into this node<br>  } else {<br>    totals.sizeBytes += stat.size;                          // only regular files add bytes<br>    totals.fileCount++;<br>  }<br>}<br>node.children.sort((a: DirUsage, b: DirUsage) =&gt; b.sizeBytes - a.sizeBytes);</pre><p>Four rules shape this code:</p><ol><li><strong>Only regular files add bytes.</strong> A directory’s Stat.size is its inode record. Adding it inflates the total.</li><li><strong>Cap the recursion depth.</strong> The sample uses 16. A symlink loop would otherwise never finish. Entries beyond the cap go into errorCount instead of being silently reported as zero.</li><li><strong>Count failures, do not abort.</strong> Files can disappear during a walk on a live device. A missing directory is also normal: a fresh install has no databaseDir until something writes to it.</li><li><strong>Keep everything async.</strong> A sandbox walk is real I/O (disk input and output) and must stay off the UI thread.</li></ol><h3>Why not listFile with recursion?</h3><p>listFile can recurse by itself:</p><pre>const options: ListFileOptions = { recursion: true, listNum: 0 };<br>const entries: string[] = await fileIo.listFile(path, options);  // every descendant, one call</pre><p>This is shorter, but it returns a flat list with no per-directory attribution and no depth control. Each entry still needs a stat call. A cleanup screen needs to know which folder grew, so the sample drives the recursion itself.</p><p>ListFileOptions is a top-level export of the kit, not a member of the fileIo namespace.</p><h3>Step 5: Run the sources concurrently</h3><p>StorageViewModel starts all three sources at once. A refresh then takes as long as the slowest source, not the sum of all three:</p><pre>const devicePromise: Promise&lt;DeviceSpace&gt; = DeviceStorage.read(context);<br>const appPromise: Promise&lt;AppSpace&gt; = BundleStorage.read();<br>device = await devicePromise;<br>app = await appPromise;</pre><pre>const walks: Promise&lt;DirUsage&gt;[] = StorageViewModel.sandboxRoots(context)<br>  .map((root: SandboxRoot) =&gt; DirectorySizer.measure(root.path, root.name));<br>roots = await Promise.all(walks);<br>roots.sort((a: DirUsage, b: DirUsage) =&gt; b.sizeBytes - a.sizeBytes);</pre><p>sandboxRoots() covers filesDir, cacheDir, tempDir, preferencesDir and databaseDir, and skips any the context does not expose. The snapshot also records elapsedMs, which shows what the walk costs.</p><p>The view model never throws. A failed source appears as zeroed figures in the snapshot instead of crashing the screen.</p><h3>Step 6: Format bytes in one place</h3><p>These APIs return raw numbers such as 12582912. ByteFormatter is the single place that converts them, using binary units (1 KB = 1024 B):</p><ul><li>formatBytes(bytes, decimals = 2): 1536 → 1.50 KB. Used for full-width readouts.</li><li>formatBytesShort(bytes): 12582912 → 12 MB. Used for watch rows, which have room for about six characters.</li><li>percent(part, whole): (1, 4) → 25. Used for the ring gauge value.</li><li>formatPercent(part, whole): (3, 4) → 75%. Used for the &quot;x% used&quot; caption.</li></ul><p>Invalid input returns -- instead of NaN or undefined. percent clamps to 0–100 and maps a zero whole to 0, because a gauge given NaN does not draw.</p><p>On the wearable, the page renders the snapshot as three full-screen cards in an ArcSwiper, with ArcList rows and an ArcButton to refresh. This is gated by canIUse(&#39;SystemCapability.ArkUI.ArkUI.Circle&#39;), so phones fall back to a plain Swiper and List.</p><h3>Result on a wearable</h3><p>Build against compatibleSdkVersion 6.0.1 (API 21) and run on a wearable emulator or device. Then swipe through the three cards.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*nAQEeAoJ40-4RO_iYthEiw.png" /><figcaption><em>Figure 2. Device card: free space from statfs, with the ring showing used / total.</em></figcaption></figure><p>The Device card should show non-zero total and free values. That confirms statfs resolved the sandbox path.</p><figure><img alt="Figure 2. Device card: free space from statfs, with the ring showing used / total." src="https://cdn-images-1.medium.com/max/500/1*pDvHt_VjCwilZ3QMlDheoA.png" /><figcaption><em>Figure 3. App card: footprint reported by getCurrentBundleStats().</em></figcaption></figure><p>The App card shows install, data, cache and total. If every row shows --, SpatialStatistics is absent on that device. That is expected, not a bug.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*VduflAYWhQ2eMmsWZ72lIA.png" /><figcaption><em>Figure 4. Sandbox card: per-directory sizes from the walk.</em></figcaption></figure><p>The Sandbox card lists directories sorted largest-first, with a walked total. This sample was freshly installed, so its sandbox is nearly empty.</p><blockquote><strong>Pro Tip:</strong> The App total can exceed the walked total, and that is correct. The system figure includes the installed HAP (HarmonyOS Ability Package), which lives outside the sandbox. Show both numbers instead of hiding the gap.</blockquote><h3>Limitations</h3><ul><li>statfs measures a file system, not a disk. Pass a path the app can read, or a non-system app gets an error.</li><li>getBundleStats(bundleName), getFreeSizeOfVolume and volumeManager are system-app only. A normal app can query only its own bundle.</li><li>The walk costs real I/O that grows with file count. Keep it async, keep the depth cap, and do not run it on every frame.</li><li>Arc components need API 18 or later and SystemCapability.ArkUI.ArkUI.Circle. Phones do not have them.</li></ul><h3>Troubleshooting</h3><p><strong>My walked total is far larger than the device reports.</strong> You are probably adding directory sizes. Add bytes only where stat.isDirectory() is false.</p><p><strong>Can I use </strong><strong>storageStatistics.getTotalSize() instead of </strong><strong>statfs?</strong> Yes, from API 15, with no path argument. I used statfs because it is per file system, works on earlier API levels, and falls under the widely available FileManagement.File.FileIO capability.</p><p><strong>The rows on my watch stay at their first values while the gauge keeps updating.</strong> Both causes are in the render layer, not storage. First, a @Builder with by-value parameters is evaluated once and does not re-run when @State changes. Render rows through a child @Component with @Prop instead. Second, ForEach reuses any item whose key has not changed, so make the key include the value: `${item.key}#${item.value}`.</p><h3>Conclusion</h3><p>Monitoring storage on HarmonyOS means combining three APIs, not choosing one. Use statfs for the device, getCurrentBundleStats() for the app, and a depth-capped fileIo walk for folders. Guard capability-dependent calls with a literal canIUse, and show -- instead of 0 B when data is unavailable. Run all sources concurrently into one snapshot that never throws, and show both totals, because they measure different things.</p><p>What would you add to a storage screen on a wearable: a cleanup action, a history graph, or per-file details? Share your ideas in the comments.</p><h3>References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/core-file-arkts">ArkTS APIs Core File Kit</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-file-statvfs">@ohos.file.statvfs (statfs)</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-file-storage-statistics">@ohos.file.storageStatistics</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-file-fs">@ohos.file.fs (fileIo)</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/app-file-access">Accessing Application Files (ArkTS)</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V5/app-sandbox-directory-V5">Application Sandbox Directory</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V13/share-app-file-V13">Sharing an Application File</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/syscap">SystemCapability and canIUse</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/core-file-arkts-dep">APIs No Longer Maintained Core File Kit</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkui-declarative-comp">ArkTS Components ArkUI</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-arclistitem">ArcListItem</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-advanced-components-arcbutton">Arc Button (ArcButton)</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkui-overview">ArkUI About This Kit</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5282ca486fa7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-to-measure-storage-on-harmonyos-in-2026-5282ca486fa7">How to Measure Storage on HarmonyOS in 2026?</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Do You Read Wearable Workouts in Your Own App Without a Second Health Kit Application?]]></title>
            <link>https://medium.com/huawei-developers/how-do-you-read-wearable-workouts-in-your-own-app-without-a-second-health-kit-application-a63b6d6ea6d5?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/a63b6d6ea6d5</guid>
            <category><![CDATA[harmonyos-next]]></category>
            <category><![CDATA[huawei]]></category>
            <category><![CDATA[huawei-health-kit]]></category>
            <category><![CDATA[huawei-cloud]]></category>
            <dc:creator><![CDATA[Enes Topal ]]></dc:creator>
            <pubDate>Fri, 16 Oct 2026 07:31:17 GMT</pubDate>
            <atom:updated>2026-10-16T07:33:33.057Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q4ohTjLJJbQ17sWMrVIODA.png" /><figcaption>Viewing workout data in third-party apps</figcaption></figure><h3>Introduction</h3><p>Getting a workout onto the watch is only half the job. You applied for Huawei Health Kit access as a HarmonyOS app, wired up Workout Service in your wearable app, and finished workouts now show up nicely in the Huawei Health app. Then the next step comes up: getting those same workouts into your own app.</p><p>Here’s what that usually turns into:</p><ul><li>Wondering if cloud access needs a whole new application</li><li>Hunting for a clientID and clientSecret you never explicitly created</li><li>Not knowing where the <strong>callback URL</strong> is supposed to live now</li><li>Guessing which scope actually covers workout records</li></ul><p>What I wanted was one clear path: take the HarmonyOS permission I already had and use it to read workout records from Health Cloud in my own app.</p><h4>One Application Is Enough</h4><p>Good news first: you <strong>don’t need a second application</strong>. Health Cloud works exactly the same way it always has. The only thing that changes is where you pick up the clientID, the clientSecret and the <strong>callback URL</strong> for the initial setup.</p><p>Once those three values are in place, everything else is the <strong>standard Health Kit cloud authorization flow</strong>. No extra configuration, no special case for HarmonyOS apps.</p><h4>Finding Your Credentials in AGC</h4><p>Everything lives in one place: AGC &gt; Development and Services &gt; App. AGC generates the clientID and clientSecret automatically when you create the app, so they’re already waiting for you there.</p><p>If your app has server to server permission, you probably added your <strong>callback URL</strong> earlier through Huawei Developer Console &gt; HUAWEI ID &gt; App &gt; Callback URL. Here you just enter that same <strong>callback URL</strong> in the Callback URL field on the AGC app page, and the configuration is done.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o1yCaZeR7CFgmRmAIK9NBw.png" /></figure><h4>Building the Authorization URL</h4><p>Next, build the authorization URL. It points to the HUAWEI ID authorization endpoint at <a href="https://oauth-login.cloud.huawei.com/oauth2/v3/authorize">https://oauth-login.cloud.huawei.com/oauth2/v3/authorize</a> and carries three things:</p><ul><li>The clientID from AGC</li><li>The <strong>callback URL</strong> you configured in AGC</li><li>The health data permissions you’re asking for, in the scope parameter</li></ul><p>To read workout records saved with Workout Service, the activityrecord.read permission (<a href="https://www.huawei.com/healthkit/activityrecord.read">https://www.huawei.com/healthkit/activityrecord.read</a>) is all you need in the scope.</p><h4>Getting the Authorization Code</h4><p>Open the URL for the user. They sign in with their HUAWEI ID and review the permissions you’re requesting. Once they grant them, HUAWEI ID redirects to your <strong>callback URL</strong> with an authorization code attached.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/495/1*NO9377VTAS09VqTjDWgYpg.png" /><figcaption>Request Authorization From User</figcaption></figure><h4>Trading the Code for an Access Token</h4><p>Now exchange that code for an access token. Send a POST request to the token endpoint at <a href="https://oauth-login.cloud.huawei.com/oauth2/v3/token">https://oauth-login.cloud.huawei.com/oauth2/v3/token</a> with:</p><ul><li>grant_type set to authorization_code</li><li>The code you just received</li><li>Your clientID and clientSecret</li><li>redirect_uri, set to your <strong>callback URL</strong></li></ul><p>The response includes an access token and, if you requested one, a refresh token.</p><h4>Querying Workout Records</h4><p>Finally, put the access token in the Authorization header as a <strong>Bearer token</strong> and call the Health Kit REST APIs. From here you can query whatever you need, like workout records within a specific time range.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/675/1*FbSR7avxFcuFBNxhnUppdg.png" /><figcaption>Read Request Result Data</figcaption></figure><h3>Wrapping Up</h3><p>Your existing HarmonyOS Health Kit permission already gets you into Health Cloud: collect the credentials from AGC and run the usual authorization flow.</p><p><strong>Here’s what it buys you:</strong></p><ul><li>No second application to submit</li><li>clientID and clientSecret generated for you in AGC</li><li>The same <strong>callback URL</strong> you already use carries over</li><li>One scope, activityrecord.read, is enough for workout records</li><li>Everything after setup is the cloud flow you may already know</li></ul><p><strong>Where it doesn’t fit:</strong></p><ul><li>The token exchange uses your clientSecret, so it needs to happen somewhere you control, like your own server</li><li>Reading data always starts with the user signing in and approving through HUAWEI ID, so there’s no silent access before that first consent</li></ul><p><strong>Happy coding! 😊</strong></p><h3>References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/HMSCore-Guides/overview-0000001177423513">Document</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/HMSCore-References/data-model-0000001054556973">Document</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a63b6d6ea6d5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-do-you-read-wearable-workouts-in-your-own-app-without-a-second-health-kit-application-a63b6d6ea6d5">How Do You Read Wearable Workouts in Your Own App Without a Second Health Kit Application?</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Build, Sign and Run HarmonyOS Apps via Linux CLI]]></title>
            <link>https://medium.com/huawei-developers/how-to-build-sign-and-run-harmonyos-apps-via-linux-cli-2717c03f4103?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/2717c03f4103</guid>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[huawei]]></category>
            <category><![CDATA[harmonyos-next]]></category>
            <category><![CDATA[wearables]]></category>
            <dc:creator><![CDATA[Cihat Furkan Eken]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 06:46:03 GMT</pubDate>
            <atom:updated>2026-10-15T06:46:02.101Z</atom:updated>
            <content:encoded><![CDATA[<p>This guide showshow to build, sign, and run HarmonyOS wearable device applications on Linux using Command Line Tools, Hvigor, hdc, and hap-sign-tool. The pipeline can be set up in this manner.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EWLOb378R-Obf6_t" /><figcaption>Photo by <a href="https://unsplash.com/@6heinz3r?utm_source=medium&amp;utm_medium=referral">Gabriel Heinzer</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>DevEco Studio is excellent for day-to-day development, but continuous integration and continuous delivery (CI/CD) pipelines do not have a graphical user interface, and they should not depend on one. A build agent running GitLab CI, GitHub Actions, or Jenkins needs a headless path from source code to a signed artifact that is ready for installation on a device.</p><p>The HarmonyOS Command Line Tools (CLI tools) ship with everything a pipeline needs: Hvigor as the build engine, ohpm (OpenHarmony Package Manager) for dependencies, hdc (HarmonyOS Device Connector) for device communication, and hap-sign-tool for signing. Combined with the standard Java keytool, you can generate keystores, Certificate Signing Requests (CSRs), digital certificates, and provisioning profiles entirely from the terminal.</p><p>In this tutorial we walk through installing the toolchain, compiling HarmonyOS Ability Packages (HAPs) and application bundles (APPs), running the result on a real watch, and automating production signing on Linux.</p><h3>Why a Headless Build Pipeline Matters</h3><p>Every manual step in a release process is a place where a human can make a mistake. Renaming a file, remembering the correct product name, or copying the right certificate into the right folder are all small actions that become expensive when they fail on a Friday release.</p><p>A headless workflow solves three concrete problems:</p><ol><li><strong>Reproducibility.</strong> The same commands run in the same order produce the same artifacts, whether they are executed on a developer laptop or on a build agent.</li><li><strong>Automation.</strong> Once compilation and signing are scripted, a Git tag can trigger a full release build without anybody opening DevEco Studio.</li><li><strong>Security.</strong> Credentials live in a secret store instead of a developer’s home directory, and signing keys are never typed into an interactive dialog where they can end up in a shell history file.</li></ol><h3>Setting Up the Headless Environment</h3><p>Start with the foundation: Java. HarmonyOS tooling expects Java Development Kit (JDK) 17, and the simplest approach is to unpack a distribution such as jdk-17.0.6_linux-x64_bin.tar.gz into /opt and point the environment at it.</p><pre>export JAVA_HOME=/opt/jdk-17.0.6<br>export PATH=$JAVA_HOME/bin:$PATH<br>java -version   # verify JDK 17 is installed</pre><p>Next, download the Command Line Tools package from the Huawei Developer portal and unzip it. Setting a single root variable keeps every later path short and portable across machines.</p><pre>unzip command-line-tools.zip<br>export COMMANDLINE_TOOL_DIR=/opt</pre><p>This produces ${COMMANDLINE_TOOL_DIR}/command-line-tools. Do not rename that folder, several internal scripts resolve paths relative to it.</p><p>The suite bundles its own Node.js runtime. Always prefer the bundled copy over the one installed by your distribution, because version skew between Node.js and Hvigor is a common source of obscure build failures.</p><pre>export NODE_HOME=$COMMANDLINE_TOOL_DIR/command-line-tools/tool/node<br>export PATH=$NODE_HOME/bin:$PATH<br>node -v   # should show Node.js (v18+ recommended)</pre><p>Then add the device connector and the build/package managers to the path. hdc lives in the OpenHarmony toolchains folder, while hvigorw and ohpm sit in the tools&#39; bin directory.</p><pre>export HDC_HOME=$COMMANDLINE_TOOL_DIR/command-line-tools/sdk/default/openharmony/toolchains<br>export PATH=$HDC_HOME:$PATH<br>export PATH=$COMMANDLINE_TOOL_DIR/command-line-tools/bin:$PATH<br>hvigorw -v   # should print the Hvigor version<br>ohpm -v      # should print the Ohpm version</pre><p><strong>Checklist before you continue:</strong> JAVA_HOME, NODE_HOME, HDC_HOME, and COMMANDLINE_TOOL_DIR must all be defined, and PATH must include $JAVA_HOME/bin, $NODE_HOME/bin, command-line-tools/bin, and openharmony/toolchains.</p><p><strong>Pro Tip:</strong> Store all of these exports in a single env.sh file and source it at the top of your pipeline script. Duplicated environment setup is the number one reason a build passes locally and fails on the agent.</p><h3>Building the Project with Hvigor</h3><p>HarmonyOS uses Hvigor as its build engine, and all tasks are driven through the ./hvigorw script. The two flags you will use constantly are --mode, which selects module-level or project-level assembly, and -p product=&lt;name&gt;, which chooses the product flavor declared in your build profile.</p><p>For day-to-day validation, compile all HAP modules without applying a production signature:</p><pre>hvigorw assembleHap --mode module -p product=default \<br>-p buildMode=debug --no-daemon</pre><p>To produce a complete .app bundle that contains every module and is ready for release signing:</p><pre>hvigorw assembleApp --mode project -p product=default \<br>-p buildMode=release --no-daemon</pre><p>And to purge previous caches and output directories when a build starts behaving strangely:</p><pre>hvigorw clean --no-daemon</pre><p>All compiled artifacts (.hap, .har, .app) land in the build/ directory of the corresponding module, or in the root build/ folder for project-level output. On a CI agent, hvigorw clean before every release build is cheap insurance against stale incremental state.</p><p><strong>Why </strong><strong>--no-daemon?</strong> The Hvigor daemon keeps a process alive between invocations to speed up interactive use. Build agents run one build and then throw the container away, so the daemon adds nothing but a leftover process that can hold a port. Always disable it in pipelines.</p><p><strong>Directory tree showing build/ output</strong></p><pre>&lt;project-root&gt;/<br>├── build/<br>│   └── outputs/<br>│       └── default/<br>│           ├── &lt;project-name&gt;-default-signed.app<br>│           └── &lt;project-name&gt;-default-unsigned.app<br>└── entry/<br>    └── build/<br>        └── default/<br>            └── outputs/<br>                └── default/<br>                    ├── entry-default-signed.hap<br>                    └── entry-default-unsigned.hap</pre><h3>Generating the Signing Chain from the Terminal</h3><p>Signing a HarmonyOS application requires a four-file chain: a keystore (.p12), a Certificate Signing Request (.csr), a digital certificate (.cer), and a provisioning profile (.p7b). Miss one link and the device will refuse to install your package.</p><h3>Step 1 - Key pair and keystore (.p12)</h3><p>Use the standard Java keytool with an elliptic curve (EC) key on the secp256r1 curve:</p><pre>keytool -genkeypair -alias &quot;my_app_alias&quot; \<br>-keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA \<br>-dname &quot;C=CN,O=HUAWEI,OU=HUAWEI IDE,CN=demo_key&quot; \<br>-keystore &quot;my_app_key.p12&quot; -storetype PKCS12 \<br>-validity 9125 \<br>-storepass &quot;KeystorePassword123&quot; \<br>-keypass &quot;KeystorePassword123&quot;</pre><p>A few details matter more than they look. CN should match the alias, because some verification paths compare them. validity is expressed in days, 9125 is roughly 25 years. And storepass must contain at least 8 characters using at least two of the following classes: uppercase letters, lowercase letters, digits, and special characters.</p><h3>Step 2 - Certificate Signing Request (.csr)</h3><p>The CSR is what proves you own the private key without revealing it:</p><pre>keytool -certreq -alias &quot;my_app_alias&quot; \<br>-sigalg SHA256withECDSA \<br>-keystore &quot;my_app_key.p12&quot; -storetype pkcs12 \<br>-file &quot;my_app.csr&quot;</pre><h3>Step 3 -Certificate (.cer) and profile (.p7b)</h3><ul><li>Upload my_app.csr to AppGallery Connect in the Huawei Developer Console</li><li>Download the generated application certificate (my_app.cer)</li><li>Register the Unique Device Identifiers (UDIDs) of your test watches, create an App Provisioning Profile</li><li>Download the resulting .p7b file</li></ul><p><strong>Pro Tip:</strong> For a wearable, forgetting to register a device UDID is the single most common reason a correctly signed HAP is rejected. Keep a scripted list of test watches and refresh the provisioning profile whenever that fleet changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SOJ5qkgxpGM20eyalgrVtQ.png" /><figcaption>AppGallery Connect certificate configuration page</figcaption></figure><h3>Signing an Unsigned HAP File</h3><p>After a debug build with assembleHap, you get an entry-default-unsigned.hap. The signing tool operates on the raw binary inside that package, so the process is a short unpack–sign–repack cycle.</p><p>Change the extension of the -unsigned.hap file to .zip, extract the .bin file contained in the archive, then run the signing tool:</p><pre>java -jar hap-sign-tool.jar sign-app \<br>-keyAlias &quot;demo_key&quot; -signAlg &quot;SHA256withECDSA&quot; -mode &quot;localSign&quot; \<br>-appCertFile &quot;/path/demo.cer&quot; -profileFile &quot;/path/demo.p7b&quot; \<br>-inFile &quot;/path/entry-default-unsigned.bin&quot; \<br>-keystoreFile &quot;/path/demo.p12&quot; \<br>-outFile &quot;/path/entry-default-signed.bin&quot; \<br>-keyPwd &quot;123456Abc&quot; -keystorePwd &quot;123456Abc&quot; -inForm bin</pre><ul><li>Execute the command from ${COMMANDLINE_TOOL_DIR}/command-line-tools/sdk/default/openharmony/toolchains/lib.</li><li>Then archive the newly generated .bin back into a .zip, revert the extension to .hap, and continue with the deployment steps below.</li></ul><h3>Running an App</h3><p>Use hdc to push a signed HAP to a real device for installation. Note that if the pushed HAP is not signed, the installation will fail, this is exactly why the four-file signing chain comes first in the pipeline.</p><p><strong>Note: </strong>hdc is not supported on lite wearable devices (GT, FIT, D, and Runner series). Therefore, this step does not apply to these devices.</p><p>To push a HAP, run the following commands</p><pre># Push the HAP to the device.<br>hdc file send &quot;{PROJECT_PATH}/entry/build/default/outputs/default/entry-default-signed.hap&quot; &quot;data/local/tmp/entry-default-signed.hap&quot;<br># Install the HAP.<br>hdc shell bm install -p &quot;data/local/tmp/entry-default-signed.hap&quot;<br># Delete the HAP.<br>hdc shell rm -rf &quot;data/local/tmp/entry-default-signed.hap&quot;</pre><p>To run the HAP on the device, run the following command:</p><pre>hdc shell aa start -a EntryAbility -b com.example.myapplication -m entry</pre><p>Replace com.example.myapplication with your own bundle name and EntryAbility with the ability you want to launch.</p><p>More details about hdc are available in the official documentation: <a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/hdc">https://developer.huawei.com/consumer/en/doc/harmonyos-guides/hdc</a></p><h3>Signing a Release APP File</h3><p>The release path is conceptually simpler: a project-level build already assembles everything into a single bundle.</p><pre>hvigorw assembleApp --mode project -p product=default \<br>-p buildMode=release --no-daemon</pre><p>Then sign app-unsigned.app</p><pre>java -jar hap-sign-tool.jar sign-app \<br>-keyAlias &quot;demo_key&quot; -signAlg &quot;SHA256withECDSA&quot; -mode &quot;localSign&quot; \<br>-appCertFile &quot;/path/demo.cer&quot; -profileFile &quot;/path/demo.p7b&quot; \<br>-inFile &quot;/path/app-unsigned.app&quot; \<br>-keystoreFile &quot;/path/demo.p12&quot; \<br>-outFile &quot;/path/app-signed.app&quot; \<br>-keyPwd &quot;$KEY_PWD&quot; -keystorePwd &quot;$KEYSTORE_PWD&quot;</pre><p>Run it from the same toolchains/lib directory. In a pipeline, pass the passwords through environment variables sourced from your CI secret store rather than hard-coding them in the command.</p><p><strong>Pro Tip:</strong> Signing is not encryption. The private key stays in the keystore. That means rotating a certificate is a pipeline configuration change, not a code change, so design your scripts to accept certificate paths as parameters from day one.</p><h3>Common Mistakes to Avoid</h3><ul><li><strong>Mixing up </strong><strong>--mode module and </strong><strong>--mode project.</strong> Debug HAPs come from module mode; release bundles come from project mode. Using the wrong one produces an artifact that cannot be signed the way you expect.</li><li><strong>Using the system Node.js.</strong> Always export NODE_HOME pointing at the bundled runtime.</li><li><strong>Forgetting to revert the </strong><strong>.zip extension.</strong> A signed binary with the wrong extension will not install.</li><li><strong>Running </strong><strong>hap-sign-tool.jar from an arbitrary directory.</strong> Relative resource loading fails silently in some versions.</li><li><strong>Different </strong><strong>storepass and </strong><strong>keypass.</strong> The signing tool requires them to match.</li><li><strong>Committing the </strong><strong>.p12 keystore to version control.</strong> Treat it like a production credential, never like an asset.</li></ul><h3>Putting It All Together: An Example Pipeline Script</h3><p>The sections above become far more useful when they are stitched into a single script that a CI agent can execute from top to bottom. The following example initializes the JDK, the device connector, and ohpm, builds the HAP and installs it on a connected device.</p><pre>#!/bin/bash<br>set -ex<br><br>JAVA_HOME=xxx # Specify the installation directory of the JDK.<br>COMMANDLINE_TOOL_DIR=xxx # Specify the installation directory of the command line tool.<br><br># Configure the hvigor and ohpm environment variables.<br>export PATH=${COMMANDLINE_TOOL_DIR}/command-line-tools/bin:$PATH<br><br># Download and configure JDK.<br>function init_JDK() {<br>  if [ ! -d &quot;${JAVA_HOME}&quot; ]; then<br>     mkdir &quot;${JAVA_HOME}&quot;<br>  fi<br>  cd ${JAVA_HOME}<br>  wget --no-check-certificate -q &quot;${jdk_download_path}&quot; -O jdk-linux.tar.xz # Download JDK, specifying the actual download path.<br>  tar -vxf jdk-linux.tar.xz<br>  JDK_DIR=xxx # Specify the directory in the compressed JDK package.<br>  cd ${JDK_DIR}<br>  mv -f ./* .[^.]* ../<br>  cd ..<br>  rm -rf JDK_DIR jdk-linux.tar.xz<br>  export JAVA_HOME=${JAVA_HOME}<br>  export PATH=$JAVA_HOME/bin:$PATH<br>  java -version<br>}<br><br># Configure the hdc environment variable.<br>function init_hdc() {<br>  export HDC_HOME=${COMMANDLINE_TOOL_DIR}/command-line-tools/sdk/default/openharmony/toolchains # Set the hdc environment variable. hdc is stored in toolchains.<br>  export PATH=$HDC_HOME:$PATH<br>}<br><br># Install ohpm. Skip this step if ohpm already exists in the image.<br>function init_ohpm() {<br>    ohpm -v<br>    # Configure the address of the ohpm repository.<br>    ohpm config set registry https://ohpm.openharmony.cn/ohpm/<br>}<br><br># Initialize related paths.<br>PROJECT_PATH=xxx  # Project directory.<br># Go to the package directory and install the dependency.<br>function ohpm_install {<br>    cd $1<br>    ohpm install<br>}<br># Environment adaptation<br>function buildHAP() {<br># Install the third-party library dependencies based on service requirements.<br>    ohpm_install &quot;${PROJECT_PATH}&quot;<br>    ohpm_install &quot;${PROJECT_PATH}/entry&quot;<br>    ohpm_install &quot;${PROJECT_PATH}/xxx&quot;<br>    # Run commands based on your service requirements. For details, see the commands in the DevEco Studio build log.<br>    cd ${PROJECT_PATH}<br>    hvigorw clean --no-daemon<br>    hvigorw assembleHap --mode module -p product=default -p debuggable=false --no-daemon # It is recommended that --no-daemon be added to the end of the command.<br>}<br>function install_hap() {<br>    hdc file send &quot;${PROJECT_PATH}/entry/build/default/outputs/default/entry-default-signed.hap&quot; &quot;data/local/tmp/entry-default-signed.hap&quot;<br>    hdc shell bm install -p &quot;data/local/tmp/entry-default-signed.hap&quot;<br>    hdc shell rm -rf &quot;data/local/tmp/entry-default-signed.hap&quot;<br>    hdc shell aa start -a MainAbility -b com.example.myapplication -m entry<br>}<br><br>function main {<br>  local startTime=$(date &#39;+%s&#39;)<br>  init_JDK<br>  init_hdc<br>  init_ohpm<br>  buildHAP<br>  install_hap<br>  local endTime=$(date &#39;+%s&#39;)<br>  local elapsedTime=$(expr $endTime - $startTime)<br>  echo &quot;build success in ${elapsedTime}s...&quot;<br>}<br>main</pre><p><strong>NOTE: </strong>The following example is not ready for direct use. Adapt it to meet your specific requirements.</p><p><strong>Pro Tip:</strong> Extend the script with a signing step between buildHAP and install_hap, reading the keystore password from an environment variable. That keeps the entire release path: compile, sign, install, in one auditable file.</p><h3>Conclusion</h3><p>A fully GUI-less HarmonyOS build and signing workflow on Linux is not only possible. It is the most reliable way to release a HarmonyOS application repeatedly. By installing the CLI tools, exporting a consistent environment, driving Hvigor with the correct mode and product parameters, automating the four-file signing chain through keytool and hap-sign-tool, and deploying the result with hdc, you remove every manual step between a commit and a running app on a watch.</p><h3>References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-hvigor-commandline">Document</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/hdc">Document</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-commandline-get">Document</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2717c03f4103" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-to-build-sign-and-run-harmonyos-apps-via-linux-cli-2717c03f4103">How to Build, Sign and Run HarmonyOS Apps via Linux CLI</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[“No available product for service release” warning in Appgallery Connect Wear Engine Application]]></title>
            <link>https://medium.com/huawei-developers/no-available-product-for-service-release-warning-in-appgallery-connect-wear-engine-application-1bf0006e0f05?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/1bf0006e0f05</guid>
            <category><![CDATA[huawei]]></category>
            <category><![CDATA[huawei-appgallery]]></category>
            <category><![CDATA[appgallery-connect]]></category>
            <category><![CDATA[wearables]]></category>
            <category><![CDATA[harmonyos-next]]></category>
            <dc:creator><![CDATA[Cihat Furkan Eken]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 06:45:56 GMT</pubDate>
            <atom:updated>2026-10-15T06:45:54.703Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ipWp9Vd_pcSjzLWC1i1m1g.jpeg" /><figcaption>AppGallery Connect</figcaption></figure><p>Learn why the Wear Engine application form shows “No available product for service release” and how to fix it</p><h3>Introduction</h3><p>Wear Engine is the Huawei service used for app-to-app messaging between the Android companion app on your phone and the app on your wearable device, such as a Huawei Watch GT series or Watch Fit series. It lets the two sides send messages, transfer data, and stay in sync.</p><p>Using the service requires submitting an application through your HUAWEI Developers account, and that submission form. However, when this application form is accessed and the application type is selected, the warning “No available product for service release” is displayed on the selection element.</p><h3>Understanding The Problem</h3><p>This prerequisite exists for a good reason. Wear Engine needs to know in advance which application is allowed to talk to which wearable, and which identity the phone-side app should use during messaging. If the identity and service behind that account are not enabled, there is literally nothing the form can list, hence “no available product.”</p><h3>How To Handle It</h3><p>In practice, the error traces back to one of three structural causes:</p><p><strong>Fix 1: Apply for and Configure the HUAWEI ID Service.</strong></p><p>The most common trigger is the simplest one to miss: the HUAWEI ID service has not been applied for, or has not been fully configured.</p><p>Head to the Management Center in the HUAWEI Developers console, click HUAWEI ID, and apply for it.</p><p>Two checks matter here. First, verify whether your existing application is correctly linked to the Developer Console through the Huawei ID service. Second, if it is not, create the application directly through this service.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ehdbp0_Se6x8KqZw.png" /><figcaption>Huawei Developer Console</figcaption></figure><p>For detailed instructions, refer to the official guide “Registering a HUAWEI ID and Applying for the HUAWEI ID Service” listed in the References section at the end. Follow it in order, and you close off the first failure mode completely.</p><p><strong>Fix 2: Enable the Account Kit Service in AppGallery Connect.</strong></p><p>You can have a perfectly registered HUAWEI ID and still see the error, because the identity alone is not enough.</p><p>Wear Engine also depends on Account Kit, the service that lets users sign in with their HUAWEI ID inside your app. If Account Kit is switched off, the Wear Engine form still finds nothing to offer.</p><p>To check it, follow these steps:</p><ul><li>Log in to AppGallery Connect.</li><li>Access your project through the “Development and services” page.</li><li>Select your specific app and open the “Manage APIs” tab.</li><li>Verify whether Account Kit is enabled. If it is not, switch it on.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GvVzzQ_KzauKEx6u.png" /><figcaption>AppGallery Connect - Project Settings</figcaption></figure><p><strong>Fix 3: Recheck Your Application and Project Setup</strong></p><p>If your identity is complete and Account Kit is enabled, the remaining cause is the application object itself. Two variations of this problem are common, and they are easy to confuse with each other.</p><p><strong>The application was never created:</strong> You may have set up a project on AppGallery Connect without initializing a corresponding application inside it.</p><p>Sign in to AppGallery Connect, select your project, and check whether an application actually exists under it. If there is no app, create one first. The Wear Engine form can only list existing products, an empty project offers it nothing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*V1yuW7QSZDXVe7xS.png" /><figcaption>AppGallery Connect - Project Settings</figcaption></figure><p><strong>The platform does not match: </strong>The application may exist, but its platform type might not match the platform selected in your Wear Engine application form.</p><p>Open the project and inspect the platform configuration. If the required platform does not exist, create a new application for that platform within the same project, rather than trying to edit the existing one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*x5IYWHSNACFagddm.png" /><figcaption>AppGallery Connect - Project Settings</figcaption></figure><p><strong><em>Pro Tip:</em></strong> Create the application directly through the HUAWEI ID service in the Developer Console, instead of creating a project first and linking the identity afterwards. This single habit eliminates the two most common failure modes</p><h3>Conclusion</h3><p>The “No available product for service release” error is a setup problem. It appears whenever the identity behind your account, the Account Kit service, or the application object itself falls out of alignment. Resolving it means finishing the HUAWEI ID service setup, enabling Account Kit in AppGallery Connect, and verifying that your application and its platform exist and match.</p><p>Start by creating the application through the HUAWEI ID flow, and most of these pitfalls disappear. Have you hit this error while connecting your phone and wearable, and which of the three causes turned out to be yours?</p><h3>References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/connectivity-Guides/registering-applying-account-0000001050818011">Document</a></li><li><a href="https://developer.huawei.com/consumer/en/console/">HUAWEI Developer</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1bf0006e0f05" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/no-available-product-for-service-release-warning-in-appgallery-connect-wear-engine-application-1bf0006e0f05">“No available product for service release” warning in Appgallery Connect Wear Engine Application</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Take a Screenshot on a Kids Watch with HarmonyOS?]]></title>
            <link>https://medium.com/huawei-developers/how-to-take-a-screenshot-on-a-kids-watch-with-harmonyos-df11cc8d9748?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/df11cc8d9748</guid>
            <category><![CDATA[tutorial]]></category>
            <category><![CDATA[wearables]]></category>
            <category><![CDATA[kids-watch]]></category>
            <category><![CDATA[wearabledevelopment]]></category>
            <category><![CDATA[harmonyos-next]]></category>
            <dc:creator><![CDATA[Can Kankaya]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 06:39:44 GMT</pubDate>
            <atom:updated>2026-10-15T06:39:43.746Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OiORB6_BLYsQWBJ6mN2QVg.png" /><figcaption>Huawei Watch Kids X1</figcaption></figure><h3>Introduction</h3><p>How do you take a screenshot on a device that has no power button, no volume keys, and a single crown button? That is the reality of building apps for Kids Watches running HarmonyOS NEXT. The usual hardware key combo simply does not exist here, so native screen capture is off the table.</p><p>In this tutorial, we will build a reusable ScreenshotSaver class in ArkTS that captures any app UI and saves it straight to the gallery. We will rely on two HarmonyOS capabilities: getComponentSnapshot() to grab the rendered view, and the photoAccessHelper API to persist the result into the system gallery.</p><h3>Why Kids Watches Have No Native Screenshot</h3><p>Kids Watches are intentionally minimal devices. They have one crown button for navigation, which means the classic “press two buttons at once” screenshot gesture cannot exist.</p><p>The good news? If you only need a screenshot of <em>your app’s content</em>, not the full system screen, you can capture the root component of your page instead.</p><h3>The Workaround in a Nutshell</h3><p>The solution follows a three-stage pipeline:</p><ol><li><strong>Snapshot: </strong>getComponentSnapshot() returns a ComponentSnapshot object that gives you a PixelMap of any component with an id.</li><li><strong>Encode: </strong>The Image Kit’s ImagePacker compresses that PixelMap into PNG bytes.</li><li><strong>Save:</strong> The photoAccessHelper API creates an image asset in the system gallery and attaches the bytes to it.</li></ol><p>Let’s build it step by step.</p><h4>Step 1: Declare the Permission</h4><p>Saving into the gallery requires the ohos.permission.WRITE_IMAGEVIDEO permission. Add it to your module.json5 file first, because the code will request it from the user at runtime.</p><pre>{<br>  &quot;name&quot;: &quot;ohos.permission.WRITE_IMAGEVIDEO&quot;,<br>  &quot;reason&quot;: &quot;$string:module_desc&quot;,<br>  &quot;usedScene&quot;: {<br>    &quot;abilities&quot;: [<br>      &quot;EntryAbility&quot;<br>    ],<br>    &quot;when&quot;: &quot;inuse&quot;<br>  },<br>}</pre><h4>Step 2: Build the ScreenshotSaver Class</h4><p>The core of the workaround is a small utility class. First, we need a helper that asks the user for gallery write access and reports whether it was granted:</p><pre>// ScreenshotSaver.ets (part 1: permission handling)<br>import { abilityAccessCtrl, common, Permissions } from &#39;@kit.AbilityKit&#39;;<br><br>private static async ensurePermission(context: common.UIAbilityContext): Promise&lt;boolean&gt; {<br>  const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();<br>  try {<br>    const result = await atManager.requestPermissionsFromUser(context, [ScreenshotSaver.PERMISSION]);<br>    return result.authResults.length &gt; 0 &amp;&amp; result.authResults[0] === 0;<br>  } catch (err) {<br>    const e = err as BusinessError;<br>    console.error(`${ScreenshotSaver.TAG} requestPermissions failed: ${e.code} - ${e.message}`);<br>    return false;<br>  }<br>}</pre><blockquote>A nice detail here: if the permission was already granted, the dialog never appears and the method returns immediately.</blockquote><p>With permission out of the way, the capture pipeline itself is surprisingly compact:</p><pre>// ScreenshotSaver.ets (part 2: capture, encode, save)<br>import { image } from &#39;@kit.ImageKit&#39;;<br>import { photoAccessHelper } from &#39;@kit.MediaLibraryKit&#39;;<br><br>static async captureAndSave(uiContext: UIContext, rootComponentId: string): Promise&lt;void&gt; {<br>  let pixelMap: image.PixelMap | undefined = undefined;<br>  try {<br>    const context = uiContext.getHostContext() as common.UIAbilityContext;<br>    // 0. Ensure WRITE_IMAGEVIDEO is actually granted.<br>    const granted: boolean = await ScreenshotSaver.ensurePermission(context);<br>    if (!granted) {<br>      console.error(`${ScreenshotSaver.TAG} WRITE_IMAGEVIDEO not granted`);<br>      uiContext.getPromptAction().showToast({ message: &#39;No gallery-write permission&#39; });<br>      return;<br>    }<br>    // 1. Snapshot the screen.<br>    pixelMap = await uiContext.getComponentSnapshot().get(rootComponentId, { waitUntilRenderFinished: true });<br>    // 2. Encode it to PNG bytes.<br>    const packer = image.createImagePacker();<br>    const png = await packer.packToData(pixelMap, { format: &#39;image/png&#39;, quality: 100 });<br>    packer.release();<br>    // 3. Save to the gallery<br>    const helper = photoAccessHelper.getPhotoAccessHelper(context);<br>    const request =<br>      photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context, photoAccessHelper.PhotoType.IMAGE, &#39;png&#39;);<br>    request.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, png);<br>    await helper.applyChanges(request);<br>    uiContext.getPromptAction().showToast({ message: &#39;Saved to gallery&#39; });<br>  } catch (err) {<br>    const e = err as BusinessError;<br>    console.error(`ScreenshotSaver failed: ${e.code} - ${e.message}`);<br>    try {<br>      uiContext.getPromptAction().showToast({ message: &#39;Save failed&#39; });<br>    } catch (error) {<br>      console.error(`showToast failed: ${e.code} - ${e.message}`);<br>    }<br>  } finally {<br>    if (pixelMap !== undefined) {<br>      pixelMap.release();<br>    }<br>  }<br>}</pre><h4>Step 3: Wire It into Your Page</h4><p>Using the class takes two moves. First, give the container you want to capture a unique id:</p><pre>@Entry<br>@Component<br>struct Index {<br>  build() {<br>    Column() {<br>      Text(&#39;Test&#39;)<br>    }<br>    .id(&#39;SCREEN_ROOT&#39;)<br>    .width(&#39;100%&#39;)<br>    .height(&#39;100%&#39;)<br>    .alignItems(HorizontalAlign.Center)<br>    .justifyContent(FlexAlign.Center)<br>  }<br>}</pre><p>Then call the saver once the first frame has been rendered:</p><pre>aboutToAppear(): void {<br>  setTimeout(() =&gt; {<br>    ScreenshotSaver.captureAndSave(this.getUIContext(), &#39;SCREEN_ROOT&#39;)<br>  }, 1000)<br>}</pre><p><strong>A complete example for a ScreenshotSaver class:</strong></p><pre>import { image } from &#39;@kit.ImageKit&#39;;<br>import { photoAccessHelper } from &#39;@kit.MediaLibraryKit&#39;;<br>import { abilityAccessCtrl, common, Permissions } from &#39;@kit.AbilityKit&#39;;<br>import { UIContext } from &#39;@kit.ArkUI&#39;;<br>import { BusinessError } from &#39;@kit.BasicServicesKit&#39;;<br><br>/**<br> * HOW TO USE<br> * ---<br> * 0. Make sure that &#39;ohos.permission.WRITE_IMAGEVIDEO&#39; is added in module.json5.<br> * 1. Give the container to capture a unique id:<br> * ```<br> * Column() {<br> *   // ... your page content ...<br> * }<br> * .width(&#39;100%&#39;)<br> * .height(&#39;100%&#39;)<br> * .id(&#39;SCREEN_ROOT&#39;)<br> * ```<br> * 2. Call it from anywhere:<br> * ```<br> * import { ScreenshotSaver } from &#39;../util/ScreenshotSaver&#39;;<br> *<br> * aboutToAppear(): void {<br> *   setTimeout(() =&gt; {<br> *     ScreenshotSaver.captureAndSave(this.getUIContext(), &#39;SCREEN_ROOT&#39;);<br> *   }, 1000);<br> * }<br> * ```<br> */<br>export class ScreenshotSaver {<br>  private static readonly TAG: string = &#39;ScreenshotSaver&#39;;<br>  private static readonly PERMISSION: Permissions = &#39;ohos.permission.WRITE_IMAGEVIDEO&#39;;<br><br>  /**<br>   * Snapshots the component with the given id and saves it straight to the<br>   * gallery. Requests ohos.permission.WRITE_IMAGEVIDEO first.<br>   *<br>   * @param uiContext        The current UI context, e.g. this.getUIContext().<br>   * @param rootComponentId  The .id set on the container to capture.<br>   */<br>  static async captureAndSave(uiContext: UIContext, rootComponentId: string): Promise&lt;void&gt; {<br>    let pixelMap: image.PixelMap | undefined = undefined;<br>    try {<br>      const context = uiContext.getHostContext() as common.UIAbilityContext;<br>      // 0. Ensure WRITE_IMAGEVIDEO is actually granted.<br>      const granted: boolean = await ScreenshotSaver.ensurePermission(context);<br>      if (!granted) {<br>        console.error(`${ScreenshotSaver.TAG} WRITE_IMAGEVIDEO not granted`);<br>        uiContext.getPromptAction().showToast({ message: &#39;No gallery-write permission&#39; });<br>        return;<br>      }<br>      // 1. Snapshot the screen.<br>      pixelMap = await uiContext.getComponentSnapshot().get(rootComponentId, { waitUntilRenderFinished: true });<br>      // 2. Encode it to PNG bytes.<br>      const packer = image.createImagePacker();<br>      const png = await packer.packToData(pixelMap, { format: &#39;image/png&#39;, quality: 100 });<br>      packer.release();<br>      // 3. Save to the gallery<br>      const helper = photoAccessHelper.getPhotoAccessHelper(context);<br>      const request =<br>        photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context, photoAccessHelper.PhotoType.IMAGE, &#39;png&#39;);<br>      request.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, png);<br>      await helper.applyChanges(request);<br>      uiContext.getPromptAction().showToast({ message: &#39;Saved to gallery&#39; });<br>    } catch (err) {<br>      const e = err as BusinessError;<br>      console.error(`ScreenshotSaver failed: ${e.code} - ${e.message}`);<br>      try {<br>        uiContext.getPromptAction().showToast({ message: &#39;Save failed&#39; });<br>      } catch (error) {<br>        console.error(`showToast failed: ${e.code} - ${e.message}`);<br>      }<br>    } finally {<br>      if (pixelMap !== undefined) {<br>        pixelMap.release();<br>      }<br>    }<br>  }<br><br>  // Requests WRITE_IMAGEVIDEO. Returns true when granted (already-granted returns<br>  // immediately with no dialog); false when denied or the app cannot hold it.<br>  private static async ensurePermission(context: common.UIAbilityContext): Promise&lt;boolean&gt; {<br>    const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();<br>    try {<br>      const result = await atManager.requestPermissionsFromUser(context, [ScreenshotSaver.PERMISSION]);<br>      return result.authResults.length &gt; 0 &amp;&amp; result.authResults[0] === 0;<br>    } catch (err) {<br>      const e = err as BusinessError;<br>      console.error(`${ScreenshotSaver.TAG} requestPermissions failed: ${e.code} - ${e.message}`);<br>      return false;<br>    }<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/730/1*V3EjQSCuWVrUMbpY11QbYA.png" /><figcaption>Testing the implementation</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/730/1*zMimJ_iJYIrd3ms0s2R4Fg.png" /><figcaption>Snapshot saved successfully</figcaption></figure><h3>Tips and Common Pitfalls</h3><p><strong>Pro Tip: Always pass </strong><strong>waitUntilRenderFinished: true when grabbing a snapshot.</strong> Without it, you can capture a half-drawn frame and end up with a blank or partially rendered PNG in the gallery.</p><p>A few more lessons worth remembering:</p><ul><li><strong>Delay the first capture.</strong> Calling the saver directly inside aboutToAppear() can race against the first render. A one-second setTimeout keeps things safe.</li><li><strong>Snapshot the component, not the screen.</strong> This approach only captures your own UI tree. That is a feature for privacy, but do not expect status bar or system UI in the output.</li></ul><h3>Conclusion</h3><p>Native screenshots may be impossible on Kids Watches, but a well-designed workaround gets you the same result with two public APIs. getComponentSnapshot() hands you the rendered view, the Image Kit encodes it, and photoAccessHelper files it neatly into the gallery. The whole solution fits in a single reusable class you can drop into any HarmonyOS NEXT wearable project. Try it in your next Kids Watch app, and you will never be blocked by a missing button combo again.</p><h3>References</h3><ul><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-arkui-componentsnapshot">ComponentSnapshot API Reference</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-apis-photoaccesshelper-photoaccesshelper">PhotoAccessHelper API Reference</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/restricted-permissions#ohospermissionwrite_imagevideo">Permission: ohos.permission.WRITE_IMAGEVIDEO</a></li><li><a href="https://developer.huawei.com/consumer/en/doc/harmonyos-references/arkts-apis-image-f">Image Kit Reference</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df11cc8d9748" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-to-take-a-screenshot-on-a-kids-watch-with-harmonyos-df11cc8d9748">How to Take a Screenshot on a Kids Watch with HarmonyOS?</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Run Your First HarmonyOS Next App in 2026? A Complete Beginner Tutorial]]></title>
            <link>https://medium.com/huawei-developers/how-to-run-your-first-harmonyos-next-app-in-2026-a-complete-beginner-tutorial-524ab92e5ede?source=rss----25443cf6c73---4</link>
            <guid isPermaLink="false">https://medium.com/p/524ab92e5ede</guid>
            <category><![CDATA[application-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[harmonyos-next]]></category>
            <category><![CDATA[huawei]]></category>
            <category><![CDATA[wearables]]></category>
            <dc:creator><![CDATA[Can Kankaya]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 06:35:18 GMT</pubDate>
            <atom:updated>2026-10-15T06:35:17.646Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fS5u-07W3XGOi2F-" /><figcaption>Photo by <a href="https://unsplash.com/@javiwoo?utm_source=medium&amp;utm_medium=referral">Jimmy Wu</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>Every new platform has a “first run” milestone, and it is rarely as smooth as the marketing page promises. Between toolchains, device pairing, and signing certificates, getting a single “Hello World” onto a real wearable can quietly eat an entire afternoon. That is exactly the gap this guide closes.</p><p>In this tutorial, you will learn how to set up the HarmonyOS Next development environment, prepare three different kinds of target devices (a physical smart wearable, the built-in emulator, and the Previewer), handle application signing both automatically and manually, and finally launch your first application with DevEco Studio.</p><p>By the end, your app will be running on a device you can actually hold in your hand.</p><h3>Setting Up the Development Environment</h3><p>Before anything else, you need two things installed and wired together: DevEco Studio, and its command line toolchains.</p><h4>Installing DevEco Studio</h4><p>DevEco Studio is Huawei’s official IDE for HarmonyOS development, and it is the backbone of everything in this guide. If you have used Android Studio or IntelliJ before, the layout will feel familiar within minutes, which lowers the learning curve considerably.</p><p>Download the appropriate version for your operating system from the official Huawei developer portal at:</p><p><a href="https://developer.huawei.com/consumer/en/deveco-studio/">DevEco Studio — HUAWEI Developers</a></p><h4>Configuring the Toolchains Path</h4><p>Installing the IDE is not quite the end of setup. The HarmonyOS toolchains, which include the HDC (HarmonyOS Device Connector) utility you will rely on later for device communication, need to be reachable from your system’s PATH environment variable.</p><p>Add the following directory to Environment Variables &gt; PATH:</p><p>{Your_DevEco_Studio_Path}/sdk/default/openharmony/toolchains</p><blockquote><strong>Important note:</strong> Without this step, commands like hdc will simply not be recognized in your terminal, which is a surprisingly common stumbling block for first-time HarmonyOS developers.</blockquote><h3>Preparing Your Target Devices</h3><p>HarmonyOS Next gives you three distinct ways to see your app in action, and each one serves a different point in your development loop. Let’s walk through all three.</p><h4>Using a Physical Device (Smart Wearable)</h4><p>There is nothing quite like seeing your UI render on an actual watch. But before DevEco Studio can talk to your wearable, you need to unlock its developer mode, and Huawei has hidden this behind a gesture-based easter egg.</p><p>Here is how to enable it:</p><ol><li>In the watch’s Settings, open the watch information screen and locate “Software Version”.</li><li>Tap “Software Version” 3 times in a row. Developer Options will be enabled.</li><li>You will now find Developer Options under Settings &gt; System &gt; Developer Options.</li><li>Inside Developer Options, enable both “HDC Debugging” and “Debug via Wi-Fi”.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gXEoc2aS2LhAhsulosa_Gg.png" /><figcaption>Enabling HDC Debugging and Debug via Wi-Fi</figcaption></figure><h4>Using the Emulator</h4><p>No physical device on your desk? No problem. DevEco Studio ships with a full emulator, which is perfect for testing layouts across different device profiles without buying hardware.</p><p>Setting it up takes only a few clicks:</p><ol><li>Go to <strong>Tools &gt; Device Manager</strong> in DevEco Studio.</li><li>Click <strong>New Emulator</strong> and select a device definition, such as a Phone or a Wearable model.</li><li>Choose the system image you want, download it, and let it run.</li><li>Click <strong>Finish </strong>to create your virtual device.</li></ol><h4>Using the Previewer</h4><p>Sometimes a full emulator boot is overkill. When you are fine-tuning a layout or just want to sanity-check your ArkTS UI code, the Previewer gives you near-instant visual feedback without launching an entire operating system.</p><ol><li>Open your UI code file, for example Index.ets.</li><li>Click the Previewer button, usually located on the right side of the editor.</li><li>Make sure the module is set to entry and the profile is correct, and you will see a live visual rendering of your code.</li></ol><h3>Configuring Application Signing</h3><p>Here is the part that trips up most newcomers: HarmonyOS applications must be signed before they will install on any device. Unlike some platforms where debug builds work out of the box, HarmonyOS Next requires a deliberate signing configuration.</p><p>Both automatic and manual signing live in the same place: File &gt; Project Structure &gt; Signing Configs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1010/1*crwNkbdA62ItfXHYAFjbhg.png" /><figcaption>Application Signing Configs</figcaption></figure><h4>Automatic Signing (Recommended)</h4><p>For the vast majority of developers, automatic signing is the right choice. DevEco Studio generates the required certificates and profiles for you, applying the configuration without any manual certificate juggling.</p><ol><li>Go to File &gt; Project Structure &gt; Signing Configs.</li><li>Check the box for “Automatically generate signature”.</li><li>Wait for DevEco Studio to generate the necessary files and apply the configuration.</li></ol><p>That is genuinely all there is to it. Unless you have a specific organizational requirement, stop here and enjoy the time you just saved.</p><h4>Manual Signing (Optional)</h4><p>Manual signing exists for developers who need explicit control over their certificates and device registrations, which is common in enterprise settings or when working with registered hardware. If automatic signing works for you, skip this section entirely.</p><p>The manual process has one important prerequisite you should understand first: debug profiles are tied to specific devices, and each device is identified by a UDID (Unique Device Identifier). So the manual flow becomes a chain: get the device’s UDID, register it, create a certificate, then create a profile that binds them together.</p><p><strong>Retrieving the Device’s UDID:</strong></p><p>First, make sure both the smart wearable and your computer are connected to the same Wi-Fi network. Then:</p><p>1. On the watch, go to Settings &gt; System &gt; Developer Options and open the “Debug via Wi-Fi” setting to find the device’s IP address.</p><p>2. Connect to the watch over the network:</p><pre>hdc tconn {WATCH_IP_ADDRESS:PORT}</pre><p>For example:</p><pre>hdc tconn 172.10.10.1:11111</pre><p>3. Retrieve the device’s UDID:</p><pre>hdc shell bm get --udid</pre><p><strong>Registering the UDID in AppGallery Connect:</strong></p><p>With the UDID in hand, head to Huawei’s developer console:</p><ol><li>Open AppGallery Connect and navigate to <strong>Certificates, app IDs, and Profiles &gt; Devices</strong>.</li><li>Click Add Device and register your device’s UDID.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ahlyBGXA3qD0S3TjkfZQjw.png" /><figcaption>Adding a new device in AppGallery Connect</figcaption></figure><p><strong>Creating a Certificate Signing Request (CSR):</strong></p><p>Next, you need a CSR (Certificate Signing Request), which DevEco Studio can generate for you:</p><ol><li>In DevEco Studio, go to<strong> Build &gt; Generate Key and CSR</strong>.</li><li>Fill in the required fields to create your key store and the resulting CSR file.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/584/1*6LTjnJQrCestL_x1skbGuA.png" /><figcaption>Generating a Key and CSR</figcaption></figure><p><strong>Creating a Debug Certificate:</strong></p><p>Now the certificate itself:</p><ol><li>In AppGallery Connect, go to <strong>Certificates, app IDs, and Profiles &gt; Certificates</strong>.</li><li>Click <strong>New Certificate</strong> and create a new debug certificate, typically by uploading the CSR you just generated.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XzHUoLlq2Xx1PdNuIsEOvw.png" /><figcaption>Creating a Debug Certificate</figcaption></figure><h4>Creating a Debug Profile</h4><p>Finally, you tie everything together with a debug profile:</p><ol><li>In AppGallery Connect, go to <strong>Certificates, app IDs, and Profiles &gt; Profiles</strong>.</li><li>Click Add and create a new debug profile with these settings:</li></ol><ul><li><strong>App Name:</strong> Select your application in AppGallery Connect.</li><li><strong>Profile Name:</strong> Anything you like; it is just a label.</li><li><strong>Type:</strong> Debug.</li><li><strong>Certificate:</strong> Choose the certificate you created in the previous step.</li><li><strong>Device:</strong> Choose the device you registered earlier.</li></ul><p>Once the profile is downloaded and applied in your project’s Signing Configs, your app is cleared for installation on that specific device.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*enDTiHXFFPJx9FJR3nRjvg.png" /><figcaption>Creating a new Profile</figcaption></figure><h3>Running the Application</h3><p>With the environment configured, the device prepared, and signing sorted, this is the satisfying part.</p><h4>Selecting the Target</h4><p>Look at the top toolbar in DevEco Studio and use the dropdown menu to pick where your app will run:</p><ul><li>Your physical device, if it is connected via HDC.</li><li>Your emulator instance, if one is currently running.</li></ul><p>Note that the Previewer does not appear in this list. It runs independently in the editor pane and does not require any target selection.</p><h4>Launching the App</h4><p>Click the green Run (triangle) icon to build and install your application on the selected target. Alternatively, click the Debug (bug) icon to launch in debug mode, which lets you set breakpoints and inspect logs and variables as the app runs.</p><p>Moments later, your application should appear on the device. If it does, congratulations, you have officially joined the HarmonyOS Next developer community.</p><h3>Tips to Avoid Common Pitfalls</h3><p>Before you close this tab, a few lessons that will save you time:</p><ul><li>Enable Developer Options on the wearable before troubleshooting connection issues. HDC debugging cannot start without it.</li><li>Keep the watch and the computer on the same Wi-Fi network during wireless debugging. Different subnets are a classic silent failure.</li><li>Prefer automatic signing unless you truly need manual control. The manual flow with UDIDs, CSRs, certificates, and profiles is powerful but easy to get wrong on the first pass.</li><li>Use the Previewer for fast UI iteration and reserve full device runs for behavior testing.</li></ul><h3>Conclusion</h3><p>Running your first HarmonyOS Next application comes down to four pillars: a properly configured DevEco Studio installation, a prepared target device, a valid signing configuration, and a target selection in the toolbar. The environment setup is a one-time investment, and after it is done, your edit-run cycle becomes fast enough to support real, iterative development on wearables. If you are building for Huawei’s ecosystem in 2026, mastering this foundation is the gateway to everything HarmonyOS Next has to offer, from ArkTS UI development to distributed cross-device experiences.</p><h3>References</h3><ul><li>DevEco Studio Download: <a href="https://developer.huawei.com/consumer/en/deveco-studio/">DevEco Studio — HUAWEI Developers</a></li><li>Huawei HarmonyOS Next Official Documentation: <a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/application-dev-guide">Application Development Overview-Getting Started — HUAWEI Developers</a></li><li>HarmonyOS Application Signing Guide: <a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-signing#section5301916183411">Configuring a Debug Signature-Coding and Debugging — HUAWEI Developers</a></li><li>Debugging HarmonyOS Applications: <a href="https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-debug-device">Debugging Overview-Debugging Your App-Coding and Debugging — HUAWEI Developers</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=524ab92e5ede" width="1" height="1" alt=""><hr><p><a href="https://medium.com/huawei-developers/how-to-run-your-first-harmonyos-next-app-in-2026-a-complete-beginner-tutorial-524ab92e5ede">How to Run Your First HarmonyOS Next App in 2026? A Complete Beginner Tutorial</a> was originally published in <a href="https://medium.com/huawei-developers">Huawei Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>