1. 介绍
当前,在不同的设备上迁移一个任务的操作通常十分复杂,比如路上在手机里写了一半的邮件,回到家想切换到平板电脑更方便的处理;或者有时需要调用不同设备中的文档或图片素材,此时需要在不同设备间反复操作。
想要解决这些问题,我们可以通过HarmonyOS的分布式能力实现任务的跨设备迁移,保证业务在手机、平板等终端间无缝衔接,轻松的完成多设备之间的协同办公。本篇Codelab文档,我们通过模拟不同设备间协同的邮件编辑来做一个简单的演示,如下图,我们可以通过迁移按钮完成任务的跨设备迁移,并通过附件按钮调用跨设备的图片。
图1-1 设备A完成邮件编写并选择附件,流转到另一设备
图1-2 设备B弹出邮件界面,可继续完成邮件编写
2. 搭建HarmonyOS环境
我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
你可以通过如下设备完成Codelab:
3. 代码结构解读
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在11 参考中提供下载方式,首先介绍一下整个工程的代码结构:
- bean:MailDataBean封装了邮件信息。
- Slice:MailEditSlice为进入邮件应用的回复页面,同时里面也展现了我们大部分的逻辑实现。
- ui:迁移设备选择框等功能实现。
- Utils:存放所有封装好的公共方法,如DeviceUtils、LogUtil等。
- MainAbility:动态权限的申请以及页面路由信息处理。
- resources:存放工程使用到的资源文件,其中resourcesbaselayout下存放xml布局文件;resourcesbasemedia下存放图片资源。
- config.json:配置文件。
4. 权限申请 完成准备工作后,在HUAWEI DevEco Studio里创建项目。
本程序开发需要申请以下6个权限:
- ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。
- ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。
- ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。
- ohos.permission.READ_USER_STORAGE:读取存储卡中的内容。
- ohos.permission.WRITE_USER_STORAGE:修改或删除存储卡中的内容。
- ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息。
在项目配置文件"entrysrcmainconfig.json"中增加以下内容:
- "reqPermissions": [
- {
- "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO"
- },
- {
- "name": "ohos.permission.DISTRIBUTED_DATASYNC"
- },
- {
- "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"
- },
- {
- "name": "ohos.permission.READ_USER_STORAGE"
- },
- {
- "name": "ohos.permission.WRITE_USER_STORAGE"
- },
- {
- "name": "ohos.permission.GET_BUNDLE_INFO"
- }
- ]
复制代码在MainAbility.java的onStart()中增加下面权限申请代码:
- String[] permissions = {
- "ohos.permission.READ_USER_STORAGE",
- "ohos.permission.WRITE_USER_STORAGE",
- "ohos.permission.DISTRIBUTED_DATASYNC"
- };
- List<String> applyPermissions = new ArrayList<>();
- for (String element : permissions) {
- LogUtil.info(TAG, "check permission: " + element);
- if (verifySelfPermission(element) != 0) {
- if (canRequestPermission(element)) {
- applyPermissions.add(element);
- } else {
- LogUtil.info(TAG, "user deny permission");
- }
- } else {
- LogUtil.info(TAG, "user granted permission: " + element);
- }
- }
- requestPermissionsFromUser(applyPermissions.toArray(new String[0]), 0);
复制代码 5. 界面实现在"slice"文件夹中新建一个MailEditSlice.java用做回复邮件界面,在"resourcesbaselayout"路径下的moudle_mail_edit.xml来定义页面的布局。
moudle_mail_edit.xml内容如下:
创建bean/MailDataBean.java,用于表示邮件正文数据,代码如下:
- public class MailDataBean {
- private static final String ARGS_RECEIVER = "receiver";
-
- private static final String ARGS_CC = "cc";
-
- private static final String ARGS_TITLE = "title";
-
- private static final String ARGS_CONTENT = "content";
-
- private static final String ARGS_PIC_LIST = "pic_list";
-
- ...
-
- /**
- * Constructor
- *
- * [url=home.php?mod=space&uid=3142012]@param[/url] receiver mail receiver
- * @param cc mail cc
- * @param title mail title
- * @param content mail content
- */
- public MailDataBean(String receiver, String cc, String title, String content) {
- super();
- this.receiver = receiver;
- this.cc = cc;
- this.title = title;
- this.content = content;
- }
-
- /**
- * Constructor with IntentParams
- *
- * @param params IntentParams
- */
- public MailDataBean(IntentParams params) {
- if (params == null) {
- LogUtil.info(this.getClass(), "Invalid intent params, can't create MailDataBean");
- return;
- }
-
- this.receiver = getStringParam(params, ARGS_RECEIVER);
- this.cc = getStringParam(params, ARGS_CC);
- this.title = getStringParam(params, ARGS_TITLE);
- this.content = getStringParam(params, ARGS_CONTENT);
- this.pictureDataList = (List<PictureData>) params.getParam(ARGS_PIC_LIST);
- }
-
- private String getStringParam(IntentParams intentParams, String key) {
- Object value = intentParams.getParam(key);
- if ((value != null) && (value instanceof String)) {
- return (String) value;
- }
- return "";
- }
-
- /**
- * MailDataBean to IntentParams
- *
- * @param params intent params
- */
- public void saveDataToParams(IntentParams params) {
- params.setParam(ARGS_RECEIVER, this.receiver == null ? "" : this.receiver);
- params.setParam(ARGS_CC, this.cc == null ? "" : this.cc);
- params.setParam(ARGS_TITLE, this.title == null ? "" : this.title);
- params.setParam(ARGS_CONTENT, this.content == null ? "" : this.content);
- params.setParam(ARGS_PIC_LIST, this.pictureDataList == null ? null : this.pictureDataList);
- }
-
- ...
- }
复制代码在MailEditSlice页面中,我们把邮件内容初始化。
- Component view = rootView.findComponentById(ResourceTable.Id_mail_edit_receiver);
- if (view instanceof TextField) {
- receiver = (TextField) view;
- }
- view = rootView.findComponentById(ResourceTable.Id_mail_edit_cc);
- if (view instanceof TextField) {
- cc = (TextField) view;
- }
- view = rootView.findComponentById(ResourceTable.Id_mail_edit_title);
- if (view instanceof TextField) {
- title = (TextField) view;
- }
- doConnectImg = (Image) rootView.findComponentById(ResourceTable.Id_mail_edit_continue);
- view = rootView.findComponentById(ResourceTable.Id_mail_edit_content);
- if (view instanceof TextField) {
- content = (TextField) view;
- }
- ...
- mAttachmentContainer = (ListContainer) rootView.findComponentById(ResourceTable.Id_attachment_list);
- mAttachmentProvider = new ListComponentAdapter<String>(
- getContext(), mAttachmentDataList, ResourceTable.Layout_attachment_item_horizontal) {
- @Override
- public void onBindViewHolder(CommentViewHolder commonViewHolder, String item, int position) {
- commonViewHolder.getTextView(ResourceTable.Id_item_title1).setText(item.substring(item.lastIndexOf(File.separator) + 1));
- FileInputStream fileInputStream = null;
- try {
- fileInputStream = new FileInputStream(item);
- ImageSource source = ImageSource.create(fileInputStream, null);
- commonViewHolder.getImageView(ResourceTable.Id_image).setPixelMap(source.createPixelmap(0, null));
- } catch (FileNotFoundException e) {
- LogUtil.error(TAG, "setAttachmentProvider Error");
- }
- }
- };
- mAttachmentContainer.setItemProvider(mAttachmentProvider);
- ...
复制代码 6. 页面迁移 我们需要通过实现IAbilityContinuation接口来完成跨设备迁移,具体可参照跨设备迁移开发指导。我们将MainAbility和MailEditSlice补充如下:
- public class MainAbility extends Ability implements IAbilityContinuation {
- ...
- [url=home.php?mod=space&uid=2735960]@Override[/url]
- public void onCompleteContinuation(int code) {}
-
- @Override
- public boolean onRestoreData(IntentParams params) {
- return true;
- }
-
- @Override
- public boolean onSaveData(IntentParams params) {
- return true;
- }
-
- @Override
- public boolean onStartContinuation() {
- return true;
- }
- }
- public class MailEditSlice extends AbilitySlice implements IAbilityContinuation {
- ...
- @Override
- public boolean onStartContinuation() {
- LogUtil.info(TAG, "is start continue");
- return true;
- }
-
- @Override
- public boolean onSaveData(IntentParams params) {
- ...
- LogUtil.info(TAG, "begin onSaveData:" + mailData);
- ...
- LogUtil.info(TAG, "end onSaveData");
- return true;
- }
-
- @Override
- public boolean onRestoreData(IntentParams params) {
- LogUtil.info(TAG, "begin onRestoreData");
- ...
- LogUtil.info(TAG, "end onRestoreData, mail data: " + cachedMailData);
- return true;
- }
-
- @Override
- public void onCompleteContinuation(int i) {
- LogUtil.info(TAG, "onCompleteContinuation");
- terminateAbility();
- }
- }
复制代码 7. 邮件数据处理
在5 界面实现章节中我们已经实现了界面设计和初始化。
迁移的数据我们可以通过6 页面迁移中的onSaveData()和onRestoreData()方法来进行传递和恢复。
- private MailDataBean cachedMailData;
-
- public boolean onSaveData(IntentParams params) {
- MailDataBean mailData = getMailData();
- LogUtil.info(TAG, "begin onSaveData");
- mailData.saveDataToParams(params);
- LogUtil.info(TAG, "end onSaveData");
- return true;
- }
-
- @Override
- public boolean onRestoreData(IntentParams params) {
- LogUtil.info(TAG, "begin onRestoreData");
- cachedMailData = new MailDataBean(params);
- LogUtil.info(TAG, "end onRestoreData, mail data");
- return true;
- }
-
- private MailDataBean getMailData() {
- MailDataBean data = new MailDataBean();
- data.setReceiver(receiver.getText());
- data.setCc(cc.getText());
- data.setTitle(title.getText());
- data.setContent(content.getText());
- data.setPictureDataList(mAttachmentDataList);
- return data;
- }
复制代码 上面步骤完成了在不同设备间数据的传递,同时我们在MailEditSlice的界面初始化之前插入对传递数据的处理:
- //写入邮件数据
- if (cachedMailData == null) {
- receiver.setText("user1;user2");
- cc.setText("user3");
- title.setText("RE:HarmonyOS 2.0 Codelab体验");
- } else {
- receiver.setText(cachedMailData.getReceiver());
- cc.setText(cachedMailData.getCc());
- title.setText(cachedMailData.getTitle());
- content.setText(cachedMailData.getContent());
- if (cachedMailData.getPictureDataList().size() > 0) {
- // 清空现有数据,并刷新
- mAttachmentDataList.clear();
- mAttachmentDataList.addAll(cachedMailData.getPictureDataList());
- }
- }
复制代码
8. 获取分布式设备
在回复邮件界面中监听迁移按钮的点击事件,从窗口底部滑出分布式设备列表界面可供选择迁移,如果不存在分布式设备则弹出提示。
- doConnectImg.setClickedListener(
- clickedView -> {
- // 通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表
- List<DeviceInfo> deviceInfoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
- if (deviceInfoList.size() < 1) {
- WidgetHelper.showTips(this, "无在网设备");
- } else {
- DeviceSelectDialog dialog = new DeviceSelectDialog(this);
- // 点击后迁移到指定设备
- dialog.setListener(
- deviceInfo -> {
- LogUtil.debug(TAG, deviceInfo.getDeviceName());
- LogUtil.info(TAG, "continue button click");
- try {
- // 开始任务迁移
- continueAbility();
- LogUtil.info(TAG, "continue button click end");
- } catch (IllegalStateException | UnsupportedOperationException e) {
- WidgetHelper.showTips(this, ResourceTable.String_tips_mail_continue_failed);
- }
- dialog.hide();
- });
- dialog.show();
- }
- });
复制代码
9. 读写分布式文件分布式文件服务能够为用户设备中的应用程序提供多设备之间的文件共享能力,支持相同帐号下同一应用程序的跨设备访问,应用程序可以不感知文件所在的存储设备,能够在多个设备之间无缝获取文件。关于分布式文件的开发指导,可以参考分布式文件开发指导。
我们这里简单举例,通过 AVStorage来获取本机的媒体文件,然后通过Context.getDistributedDir()接口获取属于自己的分布式目录,再将本地图片保存到分布式目录下。
获取公共目录图片的代码如下:
- public void initPublicPictureFile(Context context) {
- DataAbilityHelper helper = DataAbilityHelper.creator(context);
- InputStream in = null;
- OutputStream out = null;
- String[] projections =
- new String[] {AVStorage.Images.Media.ID, AVStorage.Images.Media.DISPLAY_NAME, AVStorage.Images.Media.DATA};
- try {
- ResultSet results = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, projections, null);
- while (results != null && results.goToNextRow()) {
- int mediaId = results.getInt(results.getColumnIndexForName(AVStorage.Images.Media.ID));
- String fullFileName = results.getString(results.getColumnIndexForName(AVStorage.Images.Media.DATA));
- String fileName = fullFileName.substring(fullFileName.lastIndexOf(File.separator) + 1);
-
- Uri contentUri =
- Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, "" + mediaId);
- FileDescriptor fileDescriptor = helper.openFile(contentUri, "r");
-
- if (getDistributedDir() == null) {
- WidgetHelper.showTips(this, "注意:分布式文件异常!", TIPS_DURATION_TIME);
- return;
- }
- String distributedFilePath = getContext().getDistributedDir().getPath() + File.separator + fileName;
-
- File fr = new File(distributedFilePath);
- in = new FileInputStream(fileDescriptor);
- out = new FileOutputStream(fr);
- byte[] buffer = new byte[CACHE_SIZE];
- int count = 0;
- LogUtil.info(TAG, "START WRITING");
- while ((count = in.read(buffer)) != IO_END_LEN) {
- out.write(buffer, 0, count);
- }
- out.close();
- LogUtil.info(TAG, "STOP WRITING");
- }
- } catch (DataAbilityRemoteException | IOException e) {
- LogUtil.error(TAG, "initPublicPictureFile exception");
- } finally {
- try {
- if (in != null) {
- in.close();
- }
- if (out != null) {
- out.close();
- }
- } catch (IOException e) {
- LogUtil.error(TAG, "io exception");
- }
- }
- }
复制代码在点击附件按钮获取图片时,则获取分布式路径下的文件列表,代码如下:
- rootView.findComponentById(ResourceTable.Id_open_dir)
- .setClickedListener(
- c -> {
- // 防止多次点击一直弹窗
- if (fileDialog != null && fileDialog.isShowing()) {
- return;
- }
-
- // 先获取文件并上传到共享库distributedir
- if (mDialogDataList.size() < 1) {
- mDialogDataList.clear();
- // 获取设备的分布式文件
- List tempListRemotes = DeviceUtils.getFile(this);
- mDialogDataList.addAll(tempListRemotes);
- }
- // 弹窗
- showDialog(getContext());
- });
- public void showDialog(Context context) {
- Component rootView = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_dialog_picture, null, false);
- ListContainer listContainer = (ListContainer) rootView.findComponentById(ResourceTable.Id_list_container_pic);
- if (mRecycleItemProvider == null) {
- mRecycleItemProvider =
- new ListComponentAdapter<String>(
- getContext(), mDialogDataList, ResourceTable.Layout_dialog_picture_item) {
- @Override
- public void onBindViewHolder(CommentViewHolder commonViewHolder, String item, int position) {
- commonViewHolder
- .getTextView(ResourceTable.Id_item_desc)
- .setText(item.substring(item.lastIndexOf(File.separator) + 1));
- FileInputStream fileInputStream = null;
- try {
- fileInputStream = new FileInputStream(item);
- ImageSource source = ImageSource.create(fileInputStream, null);
- commonViewHolder
- .getImageView(ResourceTable.Id_image)
- .setPixelMap(source.createPixelmap(0, null));
- } catch (FileNotFoundException e) {
- LogUtil.error(TAG, "showDialog() FileNotFound Exception");
- }
- }
- };
- } else {
- mRecycleItemProvider.notifyDataChanged();
- }
-
- clickOnAttachment(listContainer);
- fileDialog = new CommonDialog(context);
- fileDialog.setSize(MATCH_PARENT, MATCH_PARENT);
- fileDialog.setAlignment(LayoutAlignment.BOTTOM);
- fileDialog.setAutoClosable(true);
- fileDialog.setContentCustomComponent(rootView);
- fileDialog.show();
- }
-
- private void clickOnAttachment(ListContainer listContainer) {
- listContainer.setItemProvider(mRecycleItemProvider);
- listContainer.setItemClickedListener(
- (listContainer1, component, i, l) -> {
- if (mAttachmentDataList.contains(mDialogDataList.get(i))) {
- WidgetHelper.showTips(this, "此附件已添加!", TIPS_DURATION_TIME);
- } else {
- mAttachmentDataList.add(mDialogDataList.get(i));
- mAttachmentProvider.notifyDataChanged();
- fileDialog.destroy();
- }
- });
- }
复制代码 10. 恭喜你 目前你已经成功完成了Codelab并且学到了:
11. 参考