鸿蒙分布式剪贴板同步器设计与实现
·
鸿蒙分布式剪贴板同步器设计与实现
一、系统架构概述
基于HarmonyOS的分布式能力,我们设计了一个跨设备剪贴板同步系统,能够在手机、平板和PC之间实现无缝的复制粘贴体验。系统采用分布式数据管理和剪贴板服务,确保数据在不同设备间安全、高效地同步。
https://example.com/distributed-clipboard-arch.png
系统包含三个核心模块:
- 本地剪贴板监听模块 - 使用
@ohos.pasteboard监听本地剪贴板变化 - 分布式数据同步模块 - 通过
@ohos.distributedData实现跨设备数据同步 - 剪贴板历史管理模块 - 提供剪贴板历史记录和选择性粘贴功能
二、核心代码实现
1. 分布式剪贴板服务(ArkTS)
// DistributedClipboard.ets
import pasteboard from '@ohos.pasteboard';
import distributedData from '@ohos.distributedData';
import deviceManager from '@ohos.distributedHardware.deviceManager';
const CLIPBOARD_SYNC_CHANNEL = 'dist_clipboard_sync';
class DistributedClipboard {
private static instance: DistributedClipboard = null;
private dataManager: distributedData.DataManager;
private deviceManager: deviceManager.DeviceManager;
private listeners: ClipboardListener[] = [];
private lastSyncTime: number = 0;
private syncThrottle: number = 1000; // 1秒同步节流
private constructor() {
this.initDataManager();
this.initDeviceManager();
this.startLocalClipboardMonitor();
}
public static getInstance(): DistributedClipboard {
if (!DistributedClipboard.instance) {
DistributedClipboard.instance = new DistributedClipboard();
}
return DistributedClipboard.instance;
}
private initDataManager(): void {
this.dataManager = distributedData.createDataManager({
bundleName: 'com.example.distclipboard',
area: distributedData.Area.GLOBAL
});
this.dataManager.registerDataListener(CLIPBOARD_SYNC_CHANNEL, (data) => {
this.handleSyncData(data);
});
}
private initDeviceManager(): void {
this.deviceManager = deviceManager.createDeviceManager('com.example.distclipboard');
this.deviceManager.on('deviceOnline', (device) => {
console.log(`设备上线: ${device.deviceName}`);
});
}
private startLocalClipboardMonitor(): void {
const systemPasteboard = pasteboard.getSystemPasteboard();
systemPasteboard.on('update', () => {
this.syncLocalClipboardToDevices();
});
}
public async syncLocalClipboardToDevices(): Promise<void> {
const now = Date.now();
if (now - this.lastSyncTime < this.syncThrottle) {
return;
}
this.lastSyncTime = now;
const systemPasteboard = pasteboard.getSystemPasteboard();
const pasteData = await systemPasteboard.getData();
if (!pasteData) return;
const devices = this.getAvailableDevices();
if (devices.length === 0) return;
const syncData = this.createSyncData(pasteData);
devices.forEach(device => {
this.dataManager.sendData(device.deviceId, CLIPBOARD_SYNC_CHANNEL, syncData);
});
}
private createSyncData(pasteData: pasteboard.PasteData): ClipboardSyncData {
const data: ClipboardSyncData = {
type: 'clipboardData',
dataType: pasteData.type,
timestamp: Date.now(),
sourceDeviceId: this.getLocalDeviceId(),
attributes: {}
};
switch (pasteData.type) {
case pasteboard.PasteDataType.TEXT:
data.text = pasteData.text;
data.attributes.mimeTypes = ['text/plain'];
break;
case pasteboard.PasteDataType.URI:
data.uri = pasteData.uri;
data.attributes.mimeTypes = ['text/uri-list'];
break;
case pasteboard.PasteDataType.PIXELMAP:
data.pixelMap = pasteData.pixelMap;
data.attributes.mimeTypes = ['image/*'];
break;
case pasteboard.PasteDataType.HTML:
data.html = pasteData.htmlText;
data.attributes.mimeTypes = ['text/html'];
break;
}
return data;
}
private async applySyncData(syncData: ClipboardSyncData): Promise<void> {
if (syncData.sourceDeviceId === this.getLocalDeviceId()) {
return; // 不处理来自本机的同步数据
}
const systemPasteboard = pasteboard.getSystemPasteboard();
let pasteData: pasteboard.PasteData | null = null;
switch (syncData.dataType) {
case pasteboard.PasteDataType.TEXT:
pasteData = pasteboard.createPlainTextData(syncData.text || '');
break;
case pasteboard.PasteDataType.URI:
pasteData = pasteboard.createUriData(syncData.uri || '');
break;
case pasteboard.PasteDataType.PIXELMAP:
if (syncData.pixelMap) {
pasteData = pasteboard.createPixelMapData(syncData.pixelMap);
}
break;
case pasteboard.PasteDataType.HTML:
if (syncData.html) {
pasteData = pasteboard.createHtmlData(syncData.html);
}
break;
}
if (pasteData) {
await systemPasteboard.setData(pasteData);
this.notifyClipboardUpdated(syncData);
}
}
private handleSyncData(data: any): void {
if (data?.type === 'clipboardData') {
this.applySyncData(data as ClipboardSyncData);
}
}
private notifyClipboardUpdated(syncData: ClipboardSyncData): void {
this.listeners.forEach(listener => {
listener.onClipboardUpdated(syncData);
});
}
public addListener(listener: ClipboardListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}
}
public removeListener(listener: ClipboardListener): void {
this.listeners = this.listeners.filter(l => l !== listener);
}
public getAvailableDevices(): deviceManager.DeviceInfo[] {
try {
return this.deviceManager.getAvailableDeviceListSync();
} catch (err) {
console.error('获取可用设备列表失败:', JSON.stringify(err));
return [];
}
}
public getLocalDeviceId(): string {
try {
return this.deviceManager.getLocalDeviceInfoSync().deviceId;
} catch (err) {
console.error('获取本地设备ID失败:', JSON.stringify(err));
return 'unknown_device';
}
}
}
interface ClipboardSyncData {
type: string;
dataType: pasteboard.PasteDataType;
text?: string;
uri?: string;
pixelMap?: image.PixelMap;
html?: string;
timestamp: number;
sourceDeviceId: string;
attributes: {
mimeTypes?: string[];
};
}
interface ClipboardListener {
onClipboardUpdated(data: ClipboardSyncData): void;
}
export const distClipboard = DistributedClipboard.getInstance();
2. 剪贴板历史管理(ArkTS)
// ClipboardHistory.ets
import pasteboard from '@ohos.pasteboard';
class ClipboardHistory {
private static instance: ClipboardHistory = null;
private history: ClipboardItem[] = [];
private maxHistorySize: number = 20;
private constructor() {
this.loadHistory();
}
public static getInstance(): ClipboardHistory {
if (!ClipboardHistory.instance) {
ClipboardHistory.instance = new ClipboardHistory();
}
return ClipboardHistory.instance;
}
public async addItemFromPasteboard(): Promise<void> {
const systemPasteboard = pasteboard.getSystemPasteboard();
const pasteData = await systemPasteboard.getData();
if (!pasteData) return;
const newItem = this.createHistoryItem(pasteData);
// 避免重复添加相同内容
if (!this.isDuplicate(newItem)) {
this.history.unshift(newItem);
// 限制历史记录大小
if (this.history.length > this.maxHistorySize) {
this.history.pop();
}
this.saveHistory();
}
}
public async addItemFromSync(data: ClipboardSyncData): Promise<void> {
const newItem = this.createHistoryItemFromSync(data);
if (!this.isDuplicate(newItem)) {
this.history.unshift(newItem);
if (this.history.length > this.maxHistorySize) {
this.history.pop();
}
this.saveHistory();
}
}
public getHistory(): ClipboardItem[] {
return [...this.history];
}
public clearHistory(): void {
this.history = [];
this.saveHistory();
}
public async pasteItem(item: ClipboardItem): Promise<void> {
const systemPasteboard = pasteboard.getSystemPasteboard();
let pasteData: pasteboard.PasteData | null = null;
switch (item.type) {
case 'text':
pasteData = pasteboard.createPlainTextData(item.content);
break;
case 'uri':
pasteData = pasteboard.createUriData(item.content);
break;
case 'html':
pasteData = pasteboard.createHtmlData(item.content);
break;
}
if (pasteData) {
await systemPasteboard.setData(pasteData);
}
}
private createHistoryItem(pasteData: pasteboard.PasteData): ClipboardItem {
const item: ClipboardItem = {
id: Date.now().toString(),
timestamp: Date.now(),
sourceDevice: 'local'
};
switch (pasteData.type) {
case pasteboard.PasteDataType.TEXT:
item.type = 'text';
item.content = pasteData.text || '';
item.preview = pasteData.text?.substring(0, 50) || '';
break;
case pasteboard.PasteDataType.URI:
item.type = 'uri';
item.content = pasteData.uri || '';
item.preview = pasteData.uri || '';
break;
case pasteboard.PasteDataType.HTML:
item.type = 'html';
item.content = pasteData.htmlText || '';
item.preview = this.extractTextFromHtml(pasteData.htmlText || '');
break;
default:
item.type = 'unknown';
item.content = '';
item.preview = 'Unknown content type';
}
return item;
}
private createHistoryItemFromSync(data: ClipboardSyncData): ClipboardItem {
const item: ClipboardItem = {
id: `${data.sourceDeviceId}_${data.timestamp}`,
timestamp: data.timestamp,
sourceDevice: data.sourceDeviceId
};
switch (data.dataType) {
case pasteboard.PasteDataType.TEXT:
item.type = 'text';
item.content = data.text || '';
item.preview = data.text?.substring(0, 50) || '';
break;
case pasteboard.PasteDataType.URI:
item.type = 'uri';
item.content = data.uri || '';
item.preview = data.uri || '';
break;
case pasteboard.PasteDataType.HTML:
item.type = 'html';
item.content = data.html || '';
item.preview = this.extractTextFromHtml(data.html || '');
break;
default:
item.type = 'unknown';
item.content = '';
item.preview = 'Unknown content type';
}
return item;
}
private isDuplicate(newItem: ClipboardItem): boolean {
if (this.history.length === 0) return false;
const lastItem = this.history[0];
return lastItem.content === newItem.content &&
lastItem.type === newItem.type;
}
private extractTextFromHtml(html: string): string {
// 简单提取文本内容
return html.replace(/<[^>]*>/g, '').substring(0, 50);
}
private saveHistory(): void {
// 实际实现需要持久化存储
}
private loadHistory(): void {
// 实际实现需要从存储加载
}
}
interface ClipboardItem {
id: string;
type: 'text' | 'uri' | 'html' | 'unknown';
content: string;
preview: string;
timestamp: number;
sourceDevice: string;
}
export const clipboardHistory = ClipboardHistory.getInstance();
3. 剪贴板管理界面(ArkUI)
// ClipboardManagerUI.ets
import { distClipboard } from './DistributedClipboard';
import { clipboardHistory } from './ClipboardHistory';
@Entry
@Component
struct ClipboardManagerUI {
@State connectedDevices: deviceManager.DeviceInfo[] = [];
@State clipboardHistory: ClipboardItem[] = [];
@State activeTab: 'local' | 'devices' | 'history' = 'local';
@State currentClipboardContent: string = '';
aboutToAppear() {
this.loadConnectedDevices();
this.loadClipboardHistory();
this.loadCurrentClipboard();
distClipboard.addListener({
onClipboardUpdated: (data) => {
clipboardHistory.addItemFromSync(data);
this.loadClipboardHistory();
this.loadCurrentClipboard();
}
});
}
build() {
Column() {
// 顶部选项卡
this.buildTabs()
// 主内容区
this.buildContent()
}
.width('100%')
.height('100%')
.padding(12)
}
@Builder
buildTabs() {
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
this.buildLocalClipboard()
}
.tabBar('本地剪贴板')
TabContent() {
this.buildDevicesList()
}
.tabBar('设备列表')
TabContent() {
this.buildHistoryList()
}
.tabBar('历史记录')
}
.onChange((index: number) => {
this.activeTab = index === 0 ? 'local' : index === 1 ? 'devices' : 'history';
})
.height('100%')
}
@Builder
buildContent() {
if (this.activeTab === 'local') {
this.buildLocalClipboard();
} else if (this.activeTab === 'devices') {
this.buildDevicesList();
} else {
this.buildHistoryList();
}
}
@Builder
buildLocalClipboard() {
Column() {
Text('当前剪贴板内容')
.fontSize(16)
.margin({ bottom: 8 })
TextInput({ text: this.currentClipboardContent })
.width('100%')
.height(120)
.border({ width: 1, color: '#EEEEEE' })
.margin({ bottom: 16 })
Button('复制到所有设备')
.width('100%')
.height(40)
.onClick(() => {
distClipboard.syncLocalClipboardToDevices();
promptAction.showToast({ message: '已同步到所有设备', duration: 2000 });
})
}
}
@Builder
buildDevicesList() {
Column() {
Text('已连接设备')
.fontSize(16)
.margin({ bottom: 8 })
if (this.connectedDevices.length === 0) {
Text('没有可用的设备')
.fontSize(14)
.margin({ top: 20 })
} else {
List({ space: 8 }) {
ForEach(this.connectedDevices, (device) => {
ListItem() {
Row() {
Image($r('app.media.ic_device'))
.width(24)
.height(24)
.margin({ right: 12 })
Column() {
Text(device.deviceName)
.fontSize(16)
Text(device.deviceId)
.fontSize(12)
.fontColor('#888888')
}
.layoutWeight(1)
Button('同步剪贴板')
.width(120)
.height(32)
.onClick(() => {
this.syncToDevice(device.deviceId);
})
}
.padding(12)
}
})
}
.width('100%')
.layoutWeight(1)
}
}
}
@Builder
buildHistoryList() {
Column() {
Text('剪贴板历史记录')
.fontSize(16)
.margin({ bottom: 8 })
if (this.clipboardHistory.length === 0) {
Text('没有历史记录')
.fontSize(14)
.margin({ top: 20 })
} else {
List({ space: 8 }) {
ForEach(this.clipboardHistory, (item) => {
ListItem() {
Row() {
Column() {
Text(item.preview)
.fontSize(14)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(this.formatTimestamp(item.timestamp))
.fontSize(12)
.fontColor('#888888')
Text(`来自: ${this.getDeviceName(item.sourceDevice)}`)
.fontSize(12)
.fontColor('#888888')
.margin({ left: 12 })
}
}
.layoutWeight(1)
Button('粘贴')
.width(80)
.height(28)
.onClick(() => {
this.pasteHistoryItem(item);
})
}
.padding(12)
}
})
}
.width('100%')
.layoutWeight(1)
}
}
}
private loadConnectedDevices(): void {
this.connectedDevices = distClipboard.getAvailableDevices();
}
private loadClipboardHistory(): void {
this.clipboardHistory = clipboardHistory.getHistory();
}
private async loadCurrentClipboard(): Promise<void> {
const systemPasteboard = pasteboard.getSystemPasteboard();
const pasteData = await systemPasteboard.getData();
if (pasteData?.type === pasteboard.PasteDataType.TEXT) {
this.currentClipboardContent = pasteData.text || '';
} else {
this.currentClipboardContent = '[非文本内容]';
}
}
private syncToDevice(deviceId: string): void {
distClipboard.syncLocalClipboardToDevices();
promptAction.showToast({
message: `已同步到设备 ${this.getDeviceName(deviceId)}`,
duration: 2000
});
}
private async pasteHistoryItem(item: ClipboardItem): Promise<void> {
await clipboardHistory.pasteItem(item);
promptAction.showToast({
message: '已粘贴到剪贴板',
duration: 1000
});
}
private formatTimestamp(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString();
}
private getDeviceName(deviceId: string): string {
const device = this.connectedDevices.find(d => d.deviceId === deviceId);
return device ? device.deviceName : deviceId;
}
}
三、项目配置
1. 权限配置
// module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "跨设备数据同步"
},
{
"name": "ohos.permission.PASTEBOARD_WRITE",
"reason": "写入剪贴板"
},
{
"name": "ohos.permission.PASTEBOARD_READ",
"reason": "读取剪贴板"
},
{
"name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE",
"reason": "发现和管理分布式设备"
}
],
"abilities": [
{
"name": "MainAbility",
"type": "page",
"visible": true
}
]
}
}
2. 资源文件
<!-- resources/base/element/string.json -->
{
"string": [
{
"name": "app_name",
"value": "分布式剪贴板"
},
{
"name": "local_clipboard",
"value": "本地剪贴板"
},
{
"name": "device_list",
"value": "设备列表"
},
{
"name": "history",
"value": "历史记录"
},
{
"name": "current_clipboard",
"value": "当前剪贴板内容"
},
{
"name": "copy_to_all_devices",
"value": "复制到所有设备"
},
{
"name": "connected_devices",
"value": "已连接设备"
},
{
"name": "no_devices",
"value": "没有可用的设备"
},
{
"name": "sync_clipboard",
"value": "同步剪贴板"
},
{
"name": "clipboard_history",
"value": "剪贴板历史记录"
},
{
"name": "no_history",
"value": "没有历史记录"
},
{
"name": "paste",
"value": "粘贴"
},
{
"name": "from",
"value": "来自"
},
{
"name": "synced_to_device",
"value": "已同步到设备"
},
{
"name": "pasted_to_clipboard",
"value": "已粘贴到剪贴板"
}
]
}
四、功能扩展
1. 内容类型过滤
// 在DistributedClipboard中添加内容过滤
class DistributedClipboard {
private allowedTypes: pasteboard.PasteDataType[] = [
pasteboard.PasteDataType.TEXT,
pasteboard.PasteDataType.URI,
pasteboard.PasteDataType.HTML
];
private async syncLocalClipboardToDevices(): Promise<void> {
// ...原有代码
if (!pasteData || !this.allowedTypes.includes(pasteData.type)) {
return;
}
// ...后续代码
}
public setAllowedTypes(types: pasteboard.PasteDataType[]): void {
this.allowedTypes = types;
}
}
2. 加密传输
// 添加加密模块
import cipher from '@ohos.security.crypto';
class DistributedClipboard {
private async encryptData(data: string): Promise<string> {
try {
const cipherAlg = cipher.createCipher('AES128|ECB|PKCS7');
const key = new Uint8Array(16); // 实际应用中应该使用安全的密钥
const input: cipher.DataBlob = { data: new Uint8Array(Array.from(data).map(c => c.charCodeAt(0))) };
const output = await cipherAlg.init(cipher.CryptoMode.ENCRYPT_MODE, key, null);
const encrypted = await cipherAlg.doFinal(input);
return Array.from(encrypted.data).map(b => String.fromCharCode(b)).join('');
} catch (err) {
console.error('加密失败:', JSON.stringify(err));
return data;
}
}
private async decryptData(data: string): Promise<string> {
try {
const cipherAlg = cipher.createCipher('AES128|ECB|PKCS7');
const key = new Uint8Array(16); // 实际应用中应该使用安全的密钥
const input: cipher.DataBlob = { data: new Uint8Array(Array.from(data).map(c => c.charCodeAt(0))) };
const output = await cipherAlg.init(cipher.CryptoMode.DECRYPT_MODE, key, null);
const decrypted = await cipherAlg.doFinal(input);
return Array.from(decrypted.data).map(b => String.fromCharCode(b)).join('');
} catch (err) {
console.error('解密失败:', JSON.stringify(err));
return data;
}
}
private async createSyncData(pasteData: pasteboard.PasteData): Promise<ClipboardSyncData> {
const data: ClipboardSyncData = {
// ...其他字段
};
switch (pasteData.type) {
case pasteboard.PasteDataType.TEXT:
data.text = await this.encryptData(pasteData.text || '');
break;
// ...其他类型处理
}
return data;
}
private async applySyncData(syncData: ClipboardSyncData): Promise<void> {
// ...原有代码
switch (syncData.dataType) {
case pasteboard.PasteDataType.TEXT:
const decryptedText = await this.decryptData(syncData.text || '');
pasteData = pasteboard.createPlainTextData(decryptedText);
break;
// ...其他类型处理
}
// ...后续代码
}
}
3. 设备白名单管理
// 添加设备白名单功能
class DistributedClipboard {
private deviceWhitelist: string[] = [];
public addToWhitelist(deviceId: string): void {
if (!this.deviceWhitelist.includes(deviceId)) {
this.deviceWhitelist.push(deviceId);
}
}
public removeFromWhitelist(deviceId: string): void {
this.deviceWhitelist = this.deviceWhitelist.filter(id => id !== deviceId);
}
public getWhitelist(): string[] {
return [...this.deviceWhitelist];
}
private getAvailableDevices(): deviceManager.DeviceInfo[] {
try {
const allDevices = this.deviceManager.getAvailableDeviceListSync();
if (this.deviceWhitelist.length === 0) {
return allDevices;
}
return allDevices.filter(device =>
this.deviceWhitelist.includes(device.deviceId)
);
} catch (err) {
console.error('获取可用设备列表失败:', JSON.stringify(err));
return [];
}
}
}
五、总结
通过这个分布式剪贴板同步器的实现,我们学习了:
- 使用HarmonyOS剪贴板服务监听和操作剪贴板内容
- 利用分布式数据管理实现跨设备数据同步
- 设备管理和发现机制
- 剪贴板历史记录管理
- 数据安全和隐私保护策略
系统特点:
- 支持多种数据类型(文本、URI、HTML等)
- 实时同步剪贴板内容
- 完善的剪贴板历史管理
- 可配置的内容过滤和设备白名单
- 可选的数据加密传输
这个系统可以进一步扩展为更强大的剪贴板管理工具,如:
- 添加图片和富文本支持
- 实现剪贴板内容搜索
- 添加云同步功能
- 支持自定义同步规则
- 开发浏览器插件和桌面客户端
更多推荐

所有评论(0)