一、介绍
HarmonyOS提供系统剪贴板服务的操作接口,支持用户程序从系统剪贴板中读取、写入和查询剪贴板数据,以及添加、移除系统剪贴板数据变化的回调。
设备内:
用户通过系统剪贴板服务,可实现应用之间的简单数据传递。例如:在应用A中复制的数据,可以在应用B中粘贴,反之亦可。
设备间:
在分布式粘贴板场景中,粘贴的数据可以跨设备写入。例如,设备A上的应用程序使用系统粘贴板接口将从设备A复制的数据通过IDL接口存储到设备B的系统粘贴板中。如果数据允许,设备B上的应用程序可以读取并粘贴系统粘贴板中的复制数据。实现设备之间粘贴板的分布式协同。
基于以上理解,实现一个分布式粘贴板应用程序,应用程序分为客户端(copy)和服务端(paste)两部分,通过idl实现数据传递。
客户端负责数据采集,服务端负责数据的展示和应用,客户端和服务端可以安装在同一台设备中,也可以安装在不同的设备中,服务端也可以按照在多台设备中,服务端通过分布式数据库实现粘贴板数据的自动同步。
二、效果展示
三、搭建环境
安装DevEco Studio,详情请参考DevEco Studio下载。
设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
下载源码后,使用DevEco Studio 打开项目,模拟器运行即可。
真机运行需要将config.json中的buddleName修改为自己的,如果没有请到AGC上进行配置,参见 使用模拟器进行调试 。
四、项目结构
五、代码讲解
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"
- }
- ]
六、思考总结
1.粘贴板板传递数据可能会存在安全问题,需要注意,要根据具体场景来使用。
设备内每次传输的粘贴数据大小不能超过 800 KB。每次设备间传输的数据不能超过64KB,且数据必须为文本格式。
2.idl的使用,在上述案例中,客户端(copy) 和 服务端(paste) 项目idl下内容完全一致即可。
七、完整代码
附件可以直接下载
https://harmonyos.51cto.com/resource/1489