DEV Community

HarmonyOS
HarmonyOS

Posted on

System Share on HarmonyOS: Custom Actions, Multi-Image Share, and Network Image Share

Read the original article:System Share on HarmonyOS: Custom Actions, Multi-Image Share, and Network Image Share

System Share on HarmonyOS: Custom Actions, Multi-Image Share, and Network Image Share

Requirement Description

Developers commonly need to:

  1. Launch the system share panel and customize action buttons (e.g., hide “Print”).
  2. Share multiple images at once.
  3. Share a network image (by saving it locally first).

Background Knowledge

Implementation Steps

Scenario 1 — Launch the share panel & customize action area

  • Build a SharedData with at least one valid record (e.g., text), optionally add another (e.g., image).
  • Show the panel with ShareController.show(...) and exclude undesired system abilities (e.g., PRINT — see ShareAbilityType in the systemShare API Reference).

Scenario 2 — Share multiple images

  • Add each image as a record into SharedData.
  • Show panel with SelectionMode.BATCH (see API Reference).

Scenario 3 — Share a network image

  • Download into your sandbox, convert the local path to a file:// URI, then share with utd.UniformDataType.IMAGE.
  • System Share does not support direct http/https image URIs; save locally first (see System Share Guide).

Code Snippet

You’ll find a complete wearable-ready Index.ets at the end that implements all three scenarios with strict typing (ArkTS lint-friendly).

import { http } from '@kit.NetworkKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { promptAction } from '@kit.ArkUI'
import { fileIo, fileUri } from '@kit.CoreFileKit'
import { systemShare } from '@kit.ShareKit'
import { uniformTypeDescriptor as utd } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

function getUiContext(component: ESObject): common.UIAbilityContext {
  return getContext(component) as common.UIAbilityContext
}

function toUri(path: string): string {
  return fileUri.getUriFromPath(path)
}

const SAMPLE_PNG_BYTES: number[] = [
  0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A,0x00,0x00,0x00,0x0D,0x49,0x48,0x44,0x52,
  0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x08,0x06,0x00,0x00,0x00,0x1F,0x15,0xC4,
  0x89,0x00,0x00,0x00,0x0A,0x49,0x44,0x41,0x54,0x78,0xDA,0x63,0xF8,0xCF,0xC0,0x00,
  0x00,0x04,0xBF,0x01,0xFE,0xA7,0x4D,0xA3,0x2E,0x00,0x00,0x00,0x00,0x49,0x45,0x4E,
  0x44,0xAE,0x42,0x60,0x82
];

async function ensureFile(path: string, data: Uint8Array): Promise<void> {
  try {
    const fd = await fileIo.open(path, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)
    try {
      await fileIo.write(fd.fd, data.buffer)
    } finally {
      await fileIo.close(fd.fd)
    }
  } catch (err) {
    console.error('ensureFile error:', JSON.stringify(err))
    throw new Error('File creation failed')
  }
}

interface ShareScenario {
  id: string
  icon: string
  title: string
  description: string
  color: string
}

interface ShareHistory {
  id: string
  type: string
  time: string
  status: 'success' | 'failed' | 'cancelled'
  error?: string
}

@Entry
@Component
struct WearableShareHub {
  @State private currentView: number = 0
  @State private imageUrl: string = 'https://picsum.photos/200'
  @State private shareHistory: ShareHistory[] = []
  @State private isSharing: boolean = false
  @State private statusMessage: string = ''

  private filesDir: string = ''

  private scenarios: ShareScenario[] = [
    {
      id: 'text',
      icon: '📝',
      title: 'Share Text',
      description: 'Share plain text content',
      color: '#0A59F7'
    },
    {
      id: 'image',
      icon: '🖼️',
      title: 'Share Image',
      description: 'Share single image file',
      color: '#34C759'
    },
    {
      id: 'multi',
      icon: '📸',
      title: 'Multi Share',
      description: 'Share multiple images',
      color: '#FF9500'
    },
    {
      id: 'network',
      icon: '🌐',
      title: 'Web Image',
      description: 'Download & share from URL',
      color: '#AF52DE'
    }
  ]

  aboutToAppear(): void {
    try {
      this.filesDir = getUiContext(this).filesDir
      console.info('Files directory:', this.filesDir)
    } catch (err) {
      console.error('Failed to get filesDir:', JSON.stringify(err))
      this.setStatus('Init failed: Cannot get files directory', true)
    }
  }

  private addToHistory(type: string, status: 'success' | 'failed' | 'cancelled', error?: string): void {
    const now = new Date()
    const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
    this.shareHistory.unshift({
      id: String(Date.now()),
      type: type,
      time: time,
      status: status,
      error: error
    })
    if (this.shareHistory.length > 10) {
      this.shareHistory = this.shareHistory.slice(0, 10)
    }
  }

  private setStatus(msg: string, isError: boolean = false): void {
    this.statusMessage = msg
    console.info('Status:', msg)
    promptAction.showToast({
      message: msg,
      duration: 2500,
      bottom: '10%'
    })
  }

  private async shareText(): Promise<void> {
    if (this.isSharing) return
    this.isSharing = true
    this.setStatus('Preparing text share...')

    try {
      const data: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.PLAIN_TEXT,
        content: 'Hello from HarmonyOS Wearable! 🎉\n\nThis is a test message from the Share Hub app.'
      })

      const controller: systemShare.ShareController = new systemShare.ShareController(data)
      const ctx: common.UIAbilityContext = getUiContext(this)

      let dismissed = false

      controller.on('dismiss', () => {
        dismissed = true
        this.addToHistory('Text', 'cancelled')
        this.setStatus('Share dismissed')
        this.isSharing = false
      })

      await controller.show(ctx, {
        previewMode: systemShare.SharePreviewMode.DETAIL,
        selectionMode: systemShare.SelectionMode.SINGLE
      })

      if (!dismissed) {
        this.addToHistory('Text', 'success')
        this.setStatus('Text shared successfully!')
      }
    } catch (err) {
      const errorMsg = `Error: ${JSON.stringify(err)}`
      console.error('shareText error:', errorMsg.toString())
      this.addToHistory('Text', 'failed', errorMsg)
      this.setStatus('Text share failed', true)
    } finally {
      this.isSharing = false
    }
  }

  private async shareSingleImage(): Promise<void> {
    if (this.isSharing) return
    this.isSharing = true
    this.setStatus('Preparing image...')

    try {
      if (!this.filesDir) {
        throw new Error('Files directory not initialized')
      }

      const imagePath: string = `${this.filesDir}/wearable_share.png`
      console.info('Creating image at:', imagePath)

      await ensureFile(imagePath, new Uint8Array(SAMPLE_PNG_BYTES))
      console.info('Image file created successfully')

      const uri = toUri(imagePath)
      console.info('File URI:', uri)

      const data: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.PNG,
        uri: uri
      })

      const controller: systemShare.ShareController = new systemShare.ShareController(data)
      const ctx: common.UIAbilityContext = getUiContext(this)

      let dismissed = false

      controller.on('dismiss', () => {
        dismissed = true
        this.addToHistory('Image', 'cancelled')
        this.setStatus('Share dismissed')
        this.isSharing = false
      })

      this.setStatus('Opening share panel...')
      await controller.show(ctx, {
        previewMode: systemShare.SharePreviewMode.DETAIL,
        selectionMode: systemShare.SelectionMode.SINGLE
      })

      if (!dismissed) {
        this.addToHistory('Image', 'success')
        this.setStatus('Image shared successfully!')
      }
    } catch (err) {
      const errorMsg = `Error: ${JSON.stringify(err)}`
      console.error('shareSingleImage error:', errorMsg)
      this.addToHistory('Image', 'failed', errorMsg)
      this.setStatus('Image share failed', true)
    } finally {
      this.isSharing = false
    }
  }

  private async shareMultipleImages(): Promise<void> {
    if (this.isSharing) return
    this.isSharing = true
    this.setStatus('Preparing multiple images...')

    try {
      if (!this.filesDir) {
        throw new Error('Files directory not initialized')
      }

      const images: string[] = []
      for (let i = 1; i <= 3; i++) {
        const path = `${this.filesDir}/multi_${i}.png`
        await ensureFile(path, new Uint8Array(SAMPLE_PNG_BYTES))
        images.push(path)
        console.info(`Image ${i} created:`, path)
      }

      const data: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.IMAGE,
        uri: toUri(images[0])
      })

      for (let i = 1; i < images.length; i++) {
        data.addRecord({
          utd: utd.UniformDataType.IMAGE,
          uri: toUri(images[i])
        })
      }

      const controller: systemShare.ShareController = new systemShare.ShareController(data)
      const ctx: common.UIAbilityContext = getUiContext(this)

      let dismissed = false

      controller.on('dismiss', () => {
        dismissed = true
        this.addToHistory('Multiple', 'cancelled')
        this.setStatus('Share dismissed')
        this.isSharing = false
      })

      this.setStatus('Opening share panel...')
      await controller.show(ctx, {
        previewMode: systemShare.SharePreviewMode.DETAIL,
        selectionMode: systemShare.SelectionMode.BATCH
      })

      if (!dismissed) {
        this.addToHistory('Multiple', 'success')
        this.setStatus('Images shared successfully!')
      }
    } catch (err) {
      const errorMsg = `Error: ${JSON.stringify(err)}`
      console.error('shareMultipleImages error:', errorMsg)
      this.addToHistory('Multiple', 'failed', errorMsg)
      this.setStatus('Multi-share failed', true)
    } finally {
      this.isSharing = false
    }
  }

  private async shareNetworkImage(): Promise<void> {
    const url: string = this.imageUrl.trim()
    if (!url) {
      this.setStatus('Enter image URL first', true)
      return
    }

    if (this.isSharing) return
    this.isSharing = true
    this.setStatus('Downloading image...')

    try {
      if (!this.filesDir) {
        throw new Error('Files directory not initialized')
      }

      const client = http.createHttp()
      console.info('Downloading from:', url)

      const resp: http.HttpResponse = await new Promise<http.HttpResponse>((resolve, reject) => {
        client.request(url, {
          method: http.RequestMethod.GET,
          expectDataType: http.HttpDataType.ARRAY_BUFFER,
          connectTimeout: 10000,
          readTimeout: 10000
        }, (err: BusinessError, data: http.HttpResponse) => {
          if (err) {
            console.error('HTTP request error:', JSON.stringify(err))
            reject(err)
          } else {
            console.info('HTTP response code:', data.responseCode)
            resolve(data)
          }
        })
      })

      if (resp.responseCode !== 200) {
        throw new Error(`HTTP error: ${resp.responseCode}`)
      }

      if (!(resp.result instanceof ArrayBuffer)) {
        throw new Error('Response is not ArrayBuffer')
      }

      const buf = resp.result as ArrayBuffer
      console.info('Downloaded bytes:', buf.byteLength)

      const path: string = `${this.filesDir}/network_image.png`

      const fd = await fileIo.open(path, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC)
      try {
        await fileIo.write(fd.fd, buf)
        console.info('File written successfully')
      } finally {
        await fileIo.close(fd.fd)
      }

      this.setStatus('Preparing share...')

      const data: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.IMAGE,
        uri: toUri(path)
      })

      const controller: systemShare.ShareController = new systemShare.ShareController(data)
      const ctx: common.UIAbilityContext = getUiContext(this)

      let dismissed = false

      controller.on('dismiss', () => {
        dismissed = true
        this.addToHistory('Network', 'cancelled')
        this.setStatus('Share dismissed')
        this.isSharing = false
      })

      this.setStatus('Opening share panel...')
      await controller.show(ctx, {
        previewMode: systemShare.SharePreviewMode.DETAIL,
        selectionMode: systemShare.SelectionMode.SINGLE
      })

      if (!dismissed) {
        this.addToHistory('Network', 'success')
        this.setStatus('Network image shared!')
      }
    } catch (err) {
      const errorMsg = `Error: ${JSON.stringify(err)}`
      console.error('shareNetworkImage error:', errorMsg)
      this.addToHistory('Network', 'failed', errorMsg)
      this.setStatus('Network share failed', true)
    } finally {
      this.isSharing = false
    }
  }

  private getStatusIcon(status: string): string {
    if (status === 'success') {
      return ''
    } else if (status === 'failed') {
      return ''
    } else if (status === 'cancelled') {
      return '⚠️'
    } else {
      return '📤'
    }
  }

  private getStatusText(status: string): string {
    if (status === 'success') {
      return 'Shared successfully'
    } else if (status === 'failed') {
      return 'Share failed'
    } else if (status === 'cancelled') {
      return 'Share cancelled'
    } else {
      return 'Unknown status'
    }
  }

  private getStatusColor(status: string): string {
    if (status === 'success') {
      return '#34C759'
    } else if (status === 'failed') {
      return '#FF4444'
    } else if (status === 'cancelled') {
      return '#FF9500'
    } else {
      return '#888888'
    }
  }

  build() {
    Column() {
      Row({ space: 4 }) {
        Text(this.currentView === 0 ? '📤' : '📤')
          .fontSize(18)
          .width('48%')
          .height(45)
          .textAlign(TextAlign.Center)
          .backgroundColor(this.currentView === 0 ? '#0A59F7' : '#2A2A2A')
          .borderRadius(10)
          .fontColor('#FFFFFF')
          .onClick(() => this.currentView = 0)

        Text(this.currentView === 1 ? '📋' : '📋')
          .fontSize(18)
          .width('48%')
          .height(45)
          .textAlign(TextAlign.Center)
          .backgroundColor(this.currentView === 1 ? '#0A59F7' : '#2A2A2A')
          .borderRadius(10)
          .fontColor('#FFFFFF')
          .onClick(() => this.currentView = 1)
      }
      .width('95%')
      .padding({ top: 8, bottom: 8 })

      if (this.currentView === 0) {
        this.ShareOptionsView()
      } else {
        this.HistoryView()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }

  @Builder
  ShareOptionsView() {
    Column({ space: 8 }) {
      Text('Share Content')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('90%')

      List({ space: 8 }) {
        ForEach(this.scenarios, (scenario: ShareScenario) => {
          ListItem() {
            Column({ space: 6 }) {
              Row({ space: 10 }) {
                Text(scenario.icon)
                  .fontSize(24)
                  .width(50)
                  .height(50)
                  .textAlign(TextAlign.Center)
                  .backgroundColor('#2A2A2A')
                  .borderRadius(25)

                Column({ space: 2 }) {
                  Text(scenario.title)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')

                  Text(scenario.description)
                    .fontSize(11)
                    .fontColor('#888888')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')

              if (scenario.id === 'network') {
                TextInput({
                  placeholder: 'https://picsum.photos/200',
                  text: this.imageUrl
                })
                  .fontSize(11)
                  .height(35)
                  .backgroundColor('#1A1A1A')
                  .fontColor('#FFFFFF')
                  .placeholderColor('#666666')
                  .onChange((v: string) => {
                    this.imageUrl = v
                  })
              }

              Button(this.isSharing ? 'Sharing...' : 'Share')
                .fontSize(13)
                .width('100%')
                .height(40)
                .backgroundColor(scenario.color)
                .enabled(!this.isSharing)
                .onClick(() => {
                  if (scenario.id === 'text') {
                    void this.shareText()
                  } else if (scenario.id === 'image') {
                    void this.shareSingleImage()
                  } else if (scenario.id === 'multi') {
                    void this.shareMultipleImages()
                  } else if (scenario.id === 'network') {
                    void this.shareNetworkImage()
                  }
                })
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#1A1A1A')
            .borderRadius(12)
          }
        })
      }
      .width('95%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      if (this.statusMessage) {
        Text(this.statusMessage)
          .fontSize(11)
          .fontColor('#888888')
          .width('90%')
          .maxLines(3)
          .textAlign(TextAlign.Center)
          .padding({ bottom: 8 })
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  HistoryView() {
    Column({ space: 8 }) {
      Row({ space: 8 }) {
        Text('Share History')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)

        Text(`${this.shareHistory.length} items`)
          .fontSize(12)
          .fontColor('#888888')
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .backgroundColor('#2A2A2A')
          .borderRadius(10)
      }
      .width('90%')

      if (this.shareHistory.length === 0) {
        Column({ space: 10 }) {
          Text('📭')
            .fontSize(48)

          Text('No share history yet')
            .fontSize(14)
            .fontColor('#666666')

          Text('Share content to see history')
            .fontSize(12)
            .fontColor('#444444')
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      } else {
        List({ space: 6 }) {
          ForEach(this.shareHistory, (item: ShareHistory) => {
            ListItem() {
              Column({ space: 4 }) {
                Row({ space: 10 }) {
                  Text(this.getStatusIcon(item.status))
                    .fontSize(20)

                  Column({ space: 2 }) {
                    Text(item.type)
                      .fontSize(13)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FFFFFF')

                    Text(this.getStatusText(item.status))
                      .fontSize(11)
                      .fontColor(this.getStatusColor(item.status))
                  }
                  .alignItems(HorizontalAlign.Start)
                  .layoutWeight(1)

                  Text(item.time)
                    .fontSize(11)
                    .fontColor('#666666')
                }
                .width('100%')

                if (item.error) {
                  Text(item.error)
                    .fontSize(10)
                    .fontColor('#FF4444')
                    .maxLines(2)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .width('100%')
                }
              }
              .width('100%')
              .padding(12)
              .backgroundColor('#1A1A1A')
              .borderRadius(12)
            }
          }, (item: ShareHistory) => item.id)
        }
        .width('95%')
        .layoutWeight(1)
        .scrollBar(BarState.Off)

        Button('Clear History')
          .fontSize(12)
          .width('90%')
          .height(40)
          .backgroundColor('#FF4444')
          .onClick(() => {
            this.shareHistory = []
            this.setStatus('History cleared')
          })
          .padding({ bottom: 8 })
      }
    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Results

3.gif

Related Documents or Links

Written by Bunyamin Eymen Alagoz

Top comments (0)