System Share on HarmonyOS: Custom Actions, Multi-Image Share, and Network Image Share
Requirement Description
Developers commonly need to:
- Launch the system share panel and customize action buttons (e.g., hide “Print”).
- Share multiple images at once.
- Share a network image (by saving it locally first).
Background Knowledge
- System Share Service: Guide
-
systemShareAPI: Reference · SelectionMode: Reference (section) -
bindContextMenu: Reference - Uniform Data Type (UTD): Guide
- App Sandbox: Guide
Implementation Steps
Scenario 1 — Launch the share panel & customize action area
- Build a
SharedDatawith 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 — seeShareAbilityTypein thesystemShareAPI 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 withutd.UniformDataType.IMAGE. - System Share does not support direct
http/httpsimage 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%')
}
}
Test Results
Related Documents or Links
- System Share overview: Guide
-
systemShareAPI: Reference - SelectionMode: Reference (section)
-
bindContextMenu: Reference - UTD list: Guide
- App sandbox: Guide
- Target app handling (share target): Guide
- System share flow overview: Guide
- Get share result: Guide
Top comments (0)