HarmonyOS Sample 之 Pasteboard 分布式粘贴板 原创 精华

Buty9147
发布于 2021-11-11 21:38
浏览
8收藏

本文正在参与优质创作者激励
@toc

HarmonyOS Sample 之 Pasteboard 分布式粘贴板

1.介绍

HarmonyOS提供系统剪贴板服务的操作接口,支持用户程序从系统剪贴板中读取、写入和查询剪贴板数据,以及添加、移除系统剪贴板数据变化的回调。
设备内:
用户通过系统剪贴板服务,可实现应用之间的简单数据传递。例如:在应用A中复制的数据,可以在应用B中粘贴,反之亦可。
设备间:
在分布式粘贴板场景中,粘贴的数据可以跨设备写入。例如,设备A上的应用程序使用系统粘贴板接口将从设备A复制的数据通过IDL接口存储到设备B的系统粘贴板中。如果数据允许,设备B上的应用程序可以读取并粘贴系统粘贴板中的复制数据。实现设备之间粘贴板的分布式协同。

基于以上理解,实现一个分布式粘贴板应用程序,应用程序分为客户端(copy)和服务端(paste)两部分,通过idl实现数据传递。
客户端负责数据采集,服务端负责数据的展示和应用,客户端和服务端可以安装在同一台设备中,也可以安装在不同的设备中,服务端也可以按照在多台设备中,服务端通过分布式数据库实现粘贴板数据的自动同步。

2.效果展示

HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区

3.搭建环境

安装DevEco Studio,详情请参考DevEco Studio下载
设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:

如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
下载源码后,使用DevEco Studio 打开项目,模拟器运行即可。
真机运行需要将config.json中的buddleName修改为自己的,如果没有请到AGC上进行配置,参见 使用模拟器进行调试

4.项目结构

HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区
HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区

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 分布式粘贴板应用构建思路介绍

HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区

选择远端连接设备
本实例是通过新增加一个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下内容完全一致即可。

HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区 HarmonyOS Sample 之 Pasteboard 分布式粘贴板-鸿蒙开发者社区

7.完整代码

附件可以直接下载

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分布式粘贴板.zip 3.57M 59次下载
已于2021-11-12 09:22:52修改
10
收藏 8
回复
举报
5条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

楼主这篇文章的项目真的能在生活中解决很多问题。

回复
2021-11-12 09:53:56
甜甜爱开发
甜甜爱开发

这个项目很好的体现了分布式数据库的完美应用啊,很方便啊。

回复
2021-11-15 09:50:07
mb61e428166ecd8
mb61e428166ecd8

深有体会,粘贴板传递数据确实容易出很多问题,文章很有用

回复
2022-5-20 14:36:42
小威爱学习
小威爱学习

学到了

回复
2022-5-20 14:54:15
wx6245597465b5f
wx6245597465b5f

get!

回复
2022-5-20 15:51:29
回复
    相关推荐