HarmonyOS Sample 之 Pasteboard 分布式粘贴板 原创 精华
本文正在参与优质创作者激励
@toc
HarmonyOS Sample 之 Pasteboard 分布式粘贴板
1.介绍
HarmonyOS提供系统剪贴板服务的操作接口,支持用户程序从系统剪贴板中读取、写入和查询剪贴板数据,以及添加、移除系统剪贴板数据变化的回调。
设备内:
用户通过系统剪贴板服务,可实现应用之间的简单数据传递。例如:在应用A中复制的数据,可以在应用B中粘贴,反之亦可。
设备间:
在分布式粘贴板场景中,粘贴的数据可以跨设备写入。例如,设备A上的应用程序使用系统粘贴板接口将从设备A复制的数据通过IDL接口存储到设备B的系统粘贴板中。如果数据允许,设备B上的应用程序可以读取并粘贴系统粘贴板中的复制数据。实现设备之间粘贴板的分布式协同。
基于以上理解,实现一个分布式粘贴板应用程序,应用程序分为客户端(copy)和服务端(paste)两部分,通过idl实现数据传递。
客户端负责数据采集,服务端负责数据的展示和应用,客户端和服务端可以安装在同一台设备中,也可以安装在不同的设备中,服务端也可以按照在多台设备中,服务端通过分布式数据库实现粘贴板数据的自动同步。
2.效果展示
3.搭建环境
安装DevEco Studio,详情请参考DevEco Studio下载。
设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
下载源码后,使用DevEco Studio 打开项目,模拟器运行即可。
真机运行需要将config.json中的buddleName修改为自己的,如果没有请到AGC上进行配置,参见 使用模拟器进行调试 。
4.项目结构
5.代码讲解
5.1 系统粘贴板基础功能介绍
系统粘贴板对象介绍
1.SystemPasteboard //系统粘贴板对象,定义系统粘贴板操作,包括复制、粘贴和设置粘贴板内容更改的侦听器。
2.PasteData//表示粘贴板上的粘贴数据。
3.PasteData.DataProperty //该类定义了系统粘贴板上 PasteData 的属性,包括时间戳、MIME 类型和其他属性数据。
4.PasteData.Record//该类将单个粘贴的数据定义为 Record,它可以是纯文本、HTML 文本、URI 和意图。 PasteData 对象包含一个或多个记录。
==客户端(copy)CopyAbilitySlice.java==
获取系统粘贴板,监听粘贴板数据变化
/**
* 获取系统粘贴板
* 监听粘贴板数据变化
*/
private void initPasteboard() {
HiLog.debug(LABEL, "initPasteboard");
//获取系统粘贴板对象
pasteboard = SystemPasteboard.getSystemPasteboard(this);
//监听粘贴板数据变化
pasteboard.addPasteDataChangedListener(() -> {
if (pasteboard.hasPasteData()) {
sync_text = getPasteData();
HiLog.debug(LABEL, "%{public}s", "pasteStr:" + sync_text);
}
});
}
获取粘贴板内容
/**
* 获取粘贴板记录
*
* @return
*/
private String getPasteData() {
HiLog.debug(LABEL, "getPasteData");
String result = "";
//粘贴板数据对象
PasteData pasteData = pasteboard.getPasteData();
if (pasteData == null) {
return result;
}
PasteData.DataProperty dataProperty = pasteData.getProperty();
//
boolean hasHtml = dataProperty.hasMimeType(PasteData.MIMETYPE_TEXT_HTML);
boolean hasText = dataProperty.hasMimeType(PasteData.MIMETYPE_TEXT_PLAIN);
//数据格式类型
if (hasHtml || hasText) {
for (int i = 0; i < pasteData.getRecordCount(); i++) {
//粘贴板数据记录
PasteData.Record record = pasteData.getRecordAt(i);
//不同类型获取方式不同
String mimeType = record.getMimeType();
//HTML文本
if (mimeType.equals(PasteData.MIMETYPE_TEXT_HTML)) {
result = record.getHtmlText();
//纯文本
} else if (mimeType.equals(PasteData.MIMETYPE_TEXT_PLAIN)) {
result = record.getPlainText().toString();
//
} else {
HiLog.info(LABEL, "%{public}s", "getPasteData mimeType :" + mimeType);
}
}
}
return result;
}
设置文本到粘贴板中
/**
* 设置文本到粘贴板
*
* @param component
*/
private void setTextToPaste(Component component) {
HiLog.info(LABEL, "setTextToPaste");
if (pasteboard != null) {
String text = syncText.getText();
if (text.isEmpty()) {
showTips(this, "请填写内容");
return;
}
//把记录添加到粘贴板
PasteData pasteData= PasteData.creatPlainTextData(text);
//设置文本到粘贴板
pasteboard.setPasteData(pasteData);
showTips(this, "复制成功");
HiLog.info(LABEL, "setTextToPaste succeeded");
}
}
清空粘贴板
/**
* 清空粘贴板
*
* @param component
*/
private void clearPasteboard(Component component) {
if (pasteboard != null) {
pasteboard.clear();
showTips(this, "Clear succeeded");
}
}
5.2 分布式粘贴板应用构建思路介绍
选择远端连接设备
本实例是通过新增加一个DevicesSelectAbility来实现的。
private void showDevicesDialog() {
Intent intent = new Intent();
//打开选择设备的Ability页面DevicesSelectAbility
Operation operation =
new Intent.OperationBuilder()
.withDeviceId("")
.withBundleName(getBundleName())
.withAbilityName(DevicesSelectAbility.class)
.build();
intent.setOperation(operation);
//携带一个设备选择请求标识,打开设备选择页面(DevicesSelectAbility) TODO
startAbilityForResult(intent, Constants.PRESENT_SELECT_DEVICES_REQUEST_CODE);
}
/**
* 打开设备选择Ability后,选择连接的设备执行setResult后触发
*
* @param requestCode
* @param resultCode
* @param resultIntent
*/
@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultIntent) {
HiLog.debug(LABEL, "onAbilityResult");
if (requestCode == Constants.PRESENT_SELECT_DEVICES_REQUEST_CODE && resultIntent != null) {
//获取用户选择的设备
String devicesId = resultIntent.getStringParam(Constants.PARAM_DEVICE_ID);
//连接粘贴板服务端
connectService(devicesId);
return;
}
}
连接粘贴板服务端ServiceAbility服务
idl文件放在ohos.samples.pasteboard.paste目录下,
Gradl窗口,执行compileDebugIdl 后,系统生成代理对象。
interface ohos.samples.pasteboard.paste.ISharePasteAgent {
/*
* 设置系统粘贴板
*/
void setSystemPaste([in] String param);
}
连接服务端ServiceAbility,如果组网中没有其他设备就连接本地的服务端。
连接成功后,初始化idl的SharePasteAgentProxy代理,用于下一步的同步数据。
//idl共享粘贴板代理
private SharePasteAgentProxy remoteAgentProxy;
/**
* 连接粘贴板服务中心
*/
private void connectService(String deviceId) {
HiLog.debug(LABEL, "%{public}s", "connectService");
if (!isConnect) {
boolean isConnectRemote = deviceId != null;
//三元表达式,判断连接本地还是远端Service
Intent intent = isConnectRemote
? getRemoteServiceIntent(REMOTE_BUNDLE, REMOTE_SERVICE, deviceId)
: getLocalServiceIntent(REMOTE_BUNDLE, REMOTE_SERVICE);
HiLog.debug(LABEL, "%{public}s", "intent:" + intent);
//连接 Service
connectAbility(intent, new IAbilityConnection() {
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
//发个通知,Service 连接成功了
eventHandler.sendEvent(EVENT_ABILITY_CONNECT_DONE);
//初始化代理
remoteAgentProxy = new SharePasteAgentProxy(iRemoteObject);
HiLog.debug(LABEL, "%{public}s", "remoteAgentProxy:" + remoteAgentProxy);
}
@Override
public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
//发个通知,Service 断开连接了,主动断开不会执行,关闭服务端会执行
eventHandler.sendEvent(EVENT_ABILITY_DISCONNECT_DONE);
}
});
}
}
/**
* 获取远端粘贴板服务中心
*
* @param bundleName
* @param serviceName
* @return
*/
private Intent getRemoteServiceIntent(String bundleName, String serviceName, String deviceId) {
HiLog.debug(LABEL, "%{public}s", "getRemoteServiceIntent");
Operation operation = new Intent.OperationBuilder()
.withDeviceId(deviceId)
.withBundleName(bundleName)
.withAbilityName(serviceName)
//重要
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
Intent intent = new Intent();
intent.setOperation(operation);
return intent;
}
/**
* 获取本地粘贴板服务中心
*
* @param bundleName
* @param serviceName
* @return
*/
private Intent getLocalServiceIntent(String bundleName, String serviceName) {
HiLog.debug(LABEL, "%{public}s", "getLocalServiceIntent");
Operation operation = new Intent.OperationBuilder().withDeviceId("")
.withBundleName(bundleName)
.withAbilityName(serviceName)
.build();
Intent intent = new Intent();
intent.setOperation(operation);
return intent;
}
同步数据到服务端
/**
* 同步粘贴板记录到粘贴板服务中心
*
* @param component
*/
private void syncData(Component component) {
HiLog.debug(LABEL, "sync_text:" + sync_text);
if (!sync_text.isEmpty()) {
if (isConnect && remoteAgentProxy != null) {
//调用服务端IPC方法
try {
remoteAgentProxy.setSystemPaste(sync_text);
//更换文本
syncText.setText(getRandomText());
sync_text = "";
showTips(this, "同步成功");
} catch (RemoteException remoteException) {
remoteException.printStackTrace();
}
} else {
showTips(this, "正在连接设备");
}
} else {
showTips(this, "点击复制到粘贴板");
}
}
随机生成粘贴文本
/**
* 随机文本,模拟数据
*
* @return
*/
public String getRandomText() {
List<String> list = Arrays.asList(
"快马加鞭未下鞍,离天三尺三",
"我自横刀向天笑,去留肝胆两昆仑",
"飞流直下三千尺,疑是银河落九天",
"君子求诸己,小人求诸人",
"吾日三省吾身:为人谋而不忠乎?与朋友交而不信乎?传不习乎?");
int random = new SecureRandom().nextInt(list.size());
return list.get(random);
}
==服务端(paste)ServiceAbility.java==
设置粘贴板服务
idl文件放在ohos.samples.pasteboard.paste目录下,
Gradl窗口,执行compileDebugIdl 后,系统生成代理对象,idl提供了setSystemPaste接口供远端调用。
interface ohos.samples.pasteboard.paste.ISharePasteAgent {
/*
* 设置系统粘贴板
*/
void setSystemPaste([in] String param);
}
//idl的服务端实现,
SharePasteAgentStub sharePasteAgentStub = new SharePasteAgentStub(DESCRIPTOR) {
@Override
public void setSystemPaste(String param) {
HiLog.info(LABEL, "%{public}s", "param:" + param);
//插入数据库
ItemChild itemChild = new ItemChild();
String currentTime = DateUtils.getCurrentDate("yyMMdd HH:mm:ss");
itemChild.setWriteTime(currentTime);
itemChild.setWriteContent(param);
itemChild.setIndex(String.valueOf(UUID.randomUUID()));
//默认添加到未分类
itemChild.setTagName(Const.CATEGORY_TAG_UNCATEGOORIZED);
//添加粘贴板记录到分布式数据库
kvManagerUtils.addItemChild(itemChild);
}
};
@Override
protected IRemoteObject onConnect(Intent intent) {
HiLog.info(LABEL, "%{public}s", "ServiceAbility onConnect");
return sharePasteAgentStub;
}
**初始化数据库**
```java
//初始化数据库工具
kvManagerUtils = KvManagerUtils.getInstance(this);
//初始化数据库管理对象
kvManagerUtils.initDbManager(eventHandler);
//初始化数据库数据按钮
Image initDb = (Image) findComponentById(ResourceTable.Id_init_db);
initDb.setClickedListener(component -> {
//默认选中“未定义”标签
current_select_category_index = 0;
//初始化数据库数据
kvManagerUtils.initDbData();
showTip("初始化完成");
});
初始化数据列表
/**
* 从分布式数据库中查询数据
*/
public void queryData() {
HiLog.debug(LABEL, "queryData");
try {
//加载选中类别下的数据列表
initItemChild(kvManagerUtils.queryDataByTag(CategoryData.tagList.get(current_select_category_index)));
} catch (KvStoreException exception) {
HiLog.info(LABEL, "the value must be String");
}
}
/**
* 初始化选中标签的子项列表
*
* @param itemChildList itemChildList, the bean of itemChild
*/
private void initItemChild(List<ItemChild> itemChildList) {
HiLog.debug(LABEL, "initItemChild:" + itemChildList);
if (itemChildList == null) {
return;
}
//清空组件
itemChildLayout.removeAllComponents();
// Create itemChild
for (ItemChild itemChild : itemChildList) {
//获取子项类别所在的组件
Component childComponent =
LayoutScatter.getInstance(this).parse(ResourceTable.Layout_paste_record_per, null, false);
//写入时间
Text writeTime = (Text) childComponent.findComponentById(ResourceTable.Id_writeTime);
writeTime.setText(itemChild.getWriteTime());
//粘贴板内容
Text writeContent = (Text) childComponent.findComponentById(ResourceTable.Id_writeContent);
writeContent.setText(itemChild.getWriteContent());
//复制按钮
Text copy = (Text) childComponent.findComponentById(ResourceTable.Id_itemChildPerCopy);
//复制按钮的监听事件
copy.setClickedListener(component -> {
//复制内容到粘贴板
pasteboard.setPasteData(PasteData.creatPlainTextData(itemChild.getWriteContent()));
showTip("已复制到粘贴板");
});
//收藏按钮
Text favorite = (Text) childComponent.findComponentById(ResourceTable.Id_itemChildPerFavorite);
//收藏按钮的监听事件
favorite.setClickedListener(component -> {
//修改标签微已收藏
itemChild.setTagName(Const.CATEGORY_TAG_FAVORITED);
//保存数据
kvManagerUtils.addItemChild(itemChild);
showTip("已加入到收藏中");
});
/**************just for test********************/
//复选框
Checkbox noteId = (Checkbox) childComponent.findComponentById(ResourceTable.Id_noteId);
//子项列表的点击事件
childComponent.setClickedListener(component -> {
if (noteId.getVisibility() == Component.VISIBLE) {
noteId.setChecked(!noteId.isChecked());
}
});
//子项列表的长按事件,长按显示复选框
childComponent.setLongClickedListener(component -> {
//checkbox显示
noteId.setVisibility(Component.VISIBLE);
//设置复选框样式,以及其他文本组件的缩进
Element element = ElementScatter.getInstance(getContext()).parse(ResourceTable.Graphic_check_box_checked);
noteId.setBackground(element);
noteId.setChecked(true);
writeTime.setMarginLeft(80);
writeContent.setMarginLeft(80);
});
//复选框的状态变化监听事件,state表示是否被选中
noteId.setCheckedStateChangedListener((component, state) -> {
// 状态改变的逻辑
Element element;
if (state) {
//设置选中的样式
element = ElementScatter.getInstance(getContext())
.parse(ResourceTable.Graphic_check_box_checked);
} else {
//设置未选中的样式
element = ElementScatter.getInstance(getContext())
.parse(ResourceTable.Graphic_check_box_uncheck);
}
noteId.setBackground(element);
});
/**************just for test********************/
//添加子项列表组件到布局
itemChildLayout.addComponent(childComponent);
}
}
标签分类显示
//初始化列表列表的点击的监听事件
categoryList.setItemClickedListener(
(listContainer, component, index, l1) -> {
//点的就是当前类别
if (categoryListProvider.getSelectIndex() == index) {
return;
}
//切换类别索引
categoryListProvider.setSelectIndex(index);
//设置选中的标签索引
current_select_category_index = index;
//获取当前选中的标签名称
String tagName = CategoryData.tagList.get(index);
//从数据库中查询标签子项列表
initItemChild(kvManagerUtils.queryDataByTagAndKewWord(searchTextField.getText(), tagName));
//通知数据更新
categoryListProvider.notifyDataChanged();
//滚动条到最顶部
itemListScroll.fluentScrollYTo(0);
});
搜索粘贴记录
//搜索key监听事件
searchTextField.setKeyEventListener(
(component, keyEvent) -> {
if (keyEvent.isKeyDown() && keyEvent.getKeyCode() == KeyEvent.KEY_ENTER) {
//获取当前选中的标签名称
String tagName = CategoryData.tagList.get(current_select_category_index);
List<ItemChild> itemChildList = kvManagerUtils.queryDataByTagAndKewWord(searchTextField.getText(), tagName);
//从数据库中查询标签子项列表
initItemChild(itemChildList);
//通知数据更新
categoryListProvider.notifyDataChanged();
//滚动条到最顶部
itemListScroll.fluentScrollYTo(0);
}
return false;
});
==分布式数据库工具KvManagerUtils.java==
数据变化通知
提供了分布式数据库管理工具KvManagerUtils.java,数据库操作都集中在这里了。
为了在数据库数据发生变化时能及时更新页面显示,页面在初始化数据库时,传递eventHandler对象,这样在数据库变化是可以通知到页面。
/**
* 订阅数据库更改通知
* @param singleKvStore Data operation
*/
private void subscribeDb(SingleKvStore singleKvStore) {
HiLog.info(LABEL, "subscribeDb");
//数据库观察者客户端
KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
//订阅远程数据更改
singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_REMOTE, kvStoreObserverClient);
}
/**
* 自定义分布式数据库观察者客户端
* 数据发生变化时触发对应函数
* Receive database messages
*/
private class KvStoreObserverClient implements KvStoreObserver {
@Override
public void onChange(ChangeNotification notification) {
HiLog.error(LABEL, "onChange");
eventHandler.sendEvent(Const.DB_CHANGE_MESS);
}
}
数据自动同步
默认开启自动同步
/**
* Initializing Database Management
* 初始化数据库管理员
*/
public void initDbManager(EventHandler eventHandler) {
this.eventHandler = eventHandler;
HiLog.info(LABEL, "initDbManager");
if (singleKvStore == null || kvManager == null) {
HiLog.info(LABEL, "initDbData");
//创建数据库管理员
kvManager = createManager();
//创建数据库
singleKvStore = createDb(kvManager);
subscribeDb(singleKvStore);
}
}
/**
* Create a distributed database manager instance
* 创建数据库管理员
*
* @return database manager
*/
private KvManager createManager() {
HiLog.info(LABEL, "createManager");
KvManager manager = null;
try {
//
KvManagerConfig config = new KvManagerConfig(context);
manager = KvManagerFactory.getInstance().createKvManager(config);
} catch (KvStoreException exception) {
HiLog.error(LABEL, "some exception happen");
}
return manager;
}
/**
* Creating a Single-Version Distributed Database
* 创建数据库
*
* @param kvManager Database management
* @return SingleKvStore
*/
private SingleKvStore createDb(KvManager kvManager) {
HiLog.info(LABEL, "createDb");
SingleKvStore kvStore = null;
try {
Options options = new Options();
//单版本数据库,不加密,没有可用的 KvStore 数据库就创建
//单版本分布式数据库,默认开启组网设备间自动同步功能,
//如果应用对性能比较敏感建议设置关闭自动同步功能setAutoSync(false),主动调用sync接口同步。
options.setCreateIfMissing(true)
.setEncrypt(false)
.setKvStoreType(KvStoreType.SINGLE_VERSION);
//创建数据库
kvStore = kvManager.getKvStore(options, STORE_ID);
} catch (KvStoreException exception) {
HiLog.error(LABEL, "some exception happen");
}
return kvStore;
}
权限config.json
"reqPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "同步粘贴板数据",
"usedScene": {
"when": "inuse",
"ability": [
"ohos.samples.pasteboard.paste.MainAbility",
"ohos.samples.pasteboard.paste.ServiceAbility"
]
}
},
{
"name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO"
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
6.思考总结
1.粘贴板板传递数据可能会存在安全问题,需要注意,要根据具体场景来使用。
设备内每次传输的粘贴数据大小不能超过 800 KB。每次设备间传输的数据不能超过64KB,且数据必须为文本格式。
2.idl的使用,在上述案例中,客户端(copy) 和 服务端(paste) 项目idl下内容完全一致即可。
7.完整代码
附件可以直接下载
楼主这篇文章的项目真的能在生活中解决很多问题。
这个项目很好的体现了分布式数据库的完美应用啊,很方便啊。
深有体会,粘贴板传递数据确实容易出很多问题,文章很有用
学到了
get!