开发步骤
完成 环境搭建,在DevEco Studio中,选择手机设备,Empty Feature Ability(Java)模板创建项目,在项目自动创建的MainAbility中实现IAbilityContinuation接口。
public class MainAbility extends Ability implements IAbilityContinuation { private static final int DOMAIN_ID = 0xD001100; private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, DOMAIN_ID, "MainAbility"); @Override public void onStart(Intent intent) { super.onStart(intent); super.setMainRoute(MainAbilitySlice.class.getName()); } // 为了方便演示,不在Ability实现流转逻辑,具体逻辑在AbilitySlice中实现 @Override public boolean onStartContinuation() { HiLog.info(LABEL_LOG, "onStartContinuation called"); return true; } @Override public boolean onSaveData(IntentParams saveData) { HiLog.info(LABEL_LOG, "onSaveData called"); return true; } @Override public boolean onRestoreData(IntentParams restoreData) { HiLog.info(LABEL_LOG, "onRestoreData called"); return true; } @Override public void onCompleteContinuation(int result) { HiLog.info(LABEL_LOG, "onCompleteContinuation called"); } }
在AbilitySlice中实现一个用于控制基础功能的页面,以下演示代码逻辑都将在AbilitySlice中实现,代码示例如下:
public class MainAbilitySlice extends AbilitySlice { private static final int DOMAIN_ID = 0xD000F00; private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, DOMAIN_ID, "MainAbilitySlice"); @Override public void onStart(Intent intent) { super.onStart(intent); // 开发者可以自行进行界面设计 // 为按钮设置统一的背景色 // 例如通过PositionLayout可以实现简单界面 PositionLayout layout = new PositionLayout(this); LayoutConfig config = new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT); layout.setLayoutConfig(config); ShapeElement buttonBg = new ShapeElement(); buttonBg.setRgbColor(new RgbColor(0, 125, 255)); super.setUIContent(layout); } @Override public void onInactive() { super.onInactive(); } @Override public void onActive() { super.onActive(); } @Override public void onBackground() { super.onBackground(); } @Override public void onForeground(Intent intent) { super.onForeground(intent); } @Override public void onStop() { super.onStop(); } }
在FA对应的config.json中声明多设备协同访问的权限:ohos.permission.DISTRIBUTED_DATASYNC。在config.json中的配置如下:
{ "module": { "reqPermissions": [ { "name": "ohos.permission.DISTRIBUTED_DATASYNC", "reason": "need", "usedScene": { "ability": [ "MainAbility" ], "when": "inuse" } } ], ... } ... }
此外,还需要在FA的onStart()中,调用requestPermissionsFromUser()方法向用户申请权限,代码示例如下:
public class MainAbility extends Ability implements IAbilityContinuation { @Override public void onStart(Intent intent) { super.onStart(intent); // 开发者显示声明需要使用的权限 requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"}, 0); ... } ... }
设置流转任务管理服务回调函数,注册流转任务管理服务,管理流转的目标设备,同时需要在流转结束时解注册流转任务管理服务。
public class MainAbilitySlice extends AbilitySlice { // 当前应用包名 private String BUNDLE_NAME = "XXX.XXX.XXX"; // 流转应用包名 private String REMOTE_BUNDLE_NAME = "XXX.XXX.XXX"; // 流转FA名称 private String REMOTE_FA_NAME = "XXX.XXX.XXX.XXXAbility"; // 流转PA名称 private String REMOTE_PA_NAME = "XXX.XXX.XXX.XXXAbility"; // 注册流转任务管理服务后返回的Ability token private int abilityToken; // 用户在设备列表中选择设备后返回的设备ID private String selectDeviceId; // 用户是否已发起可拉回流转流程 private boolean isReversibly = false; // 获取流转任务管理服务管理类 private IContinuationRegisterManager continuationRegisterManager; // 设置初始化分布式环境的回调 private IInitCallback iInitCallback = new IInitCallback() { @Override public void onInitSuccess(String deviceId) { HiLog.info(LABEL_LOG, "device id success: " + deviceId); } @Override public void onInitFailure(String deviceId, int errorCode) { HiLog.info(LABEL_LOG, "device id failed: " + deviceId + "errorCode: " + errorCode); } }; // 设置流转任务管理服务设备状态变更的回调 private IContinuationDeviceCallback callback = new IContinuationDeviceCallback() { @Override public void onConnected(ContinuationDeviceInfo deviceInfo) { // 在用户选择设备后设置设备ID selectDeviceId = deviceInfo.getDeviceId(); try { // 初始化分布式环境 DeviceManager.initDistributedEnvironment(selectDeviceId, iInitCallback); } catch (RemoteException e) { HiLog.info(LABEL_LOG, "initDistributedEnvironment failed"); } //更新选择设备后的流转状态 continuationRegisterManager.updateConnectStatus(abilityToken, selectDeviceId, DeviceConnectState.CONNECTED.getState(), null); } @Override public void onDisconnected(String deviceId) { } }; // 设置注册流转任务管理服务回调 private RequestCallback requestCallback = new RequestCallback() { @Override public void onResult(int result) { abilityToken = result; } }; ... @Override public void onStart(Intent intent) { ... continuationRegisterManager = getContinuationRegisterManager(); } @Override public void onStop() { super.onStop(); // 解注册流转任务管理服务 continuationRegisterManager.unregister(abilityToken, null); // 断开流转任务管理服务连接 continuationRegisterManager.disconnect(); }
为不同功能设置相应的控制按钮。
// 建议开发者按照自己的界面进行按钮设计,示例代码仅供参考 private static final int OFFSET_X = 100; private static final int OFFSET_Y = 100; private static final int ADD_OFFSET_Y = 150; private static final int BUTTON_WIDTH = 800; private static final int BUTTON_HEIGHT = 100; private static final int TEXT_SIZE = 50; private int offsetY = 0; private Button btnShowDeviceList; private Button btnStartRemote; private Button btnStopRemote; private Button btnConnectRemotePA; private Button btnControlRemotePA; private Button btnDisconnectRemotePA; private Button createButton(String text, ShapeElement buttonBg) { Button button = new Button(this); button.setContentPosition(OFFSET_X, OFFSET_Y + offsetY); offsetY += ADD_OFFSET_Y; button.setWidth(BUTTON_WIDTH); button.setHeight(BUTTON_HEIGHT); button.setTextSize(TEXT_SIZE); button.setTextColor(Color.YELLOW); button.setText(text); button.setBackground(buttonBg); return button; } // 按照顺序在PositionLayout中依次添加按钮的示例 private void addComponents(PositionLayout linear, ShapeElement buttonBg) { // 构建显示注册流转任务管理服务的按钮 Button btnRegister = createButton("register", buttonBg); btnRegister.setClickedListener(mRegisterListener); linear.addComponent(btnRegister); // 构建显示设备列表的按钮 btnShowDeviceList = createButton("ShowDeviceList", buttonBg); btnShowDeviceList.setClickedListener(mShowDeviceListListener); linear.addComponent(btnShowDeviceList); // 构建远程启动FA/PA的按钮 btnStartRemote = createButton("StartRemote", buttonBg); btnStartRemote.setClickedListener(mStartRemoteListener); linear.addComponent(btnStartRemote); // 构建远程关闭PA的按钮 btnStopRemote = createButton("StopRemote", buttonBg); btnStopRemote.setClickedListener(mStopRemoteListener); linear.addComponent(btnStopRemote); // 构建连接远程PA的按钮 btnConnectRemotePA = createButton("ConnectRemotePA", buttonBg); btnConnectRemotePA.setClickedListener(mConnectRemotePAListener); linear.addComponent(btnConnectRemotePA); // 构建控制连接PA的按钮 btnControlRemotePA = createButton("ControlRemotePA", buttonBg); btnControlRemotePA.setClickedListener(mControlPAListener); linear.addComponent(btnControlRemotePA); // 构建与远程PA断开连接的按钮 btnDisconnectRemotePA = createButton("DisconnectRemotePA", buttonBg); btnDisconnectRemotePA.setClickedListener(mDisconnectRemotePAListener); linear.addComponent(btnDisconnectRemotePA); } @Override public void onStart(Intent intent) { ... //添加功能按钮布局 addComponents(layout, buttonBg); super.setUIContent(layout); }
注册流转任务管理服务。
// 注册流转任务管理服务 private Component.ClickedListener mRegisterListener = new Component.ClickedListener() { @Override public void onClick(Component arg0) { HiLog.info(LABEL_LOG, "register call."); //增加过滤条件 ExtraParams params = new ExtraParams(); String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE}; params.setDevType(devTypes); String jsonParams = "{'filter':{'commonFilter':{'system':{'harmonyVersion':'2.0.0'},'groupType':'1|256','curComType': 0x00030004,'faFilter':'{\"localVersionCode\":1,\"localMinCompatibleVersionCode\":2,\"targetBundleName\": \"com.xxx.yyy\"}'}},'transferScene':0,'remoteAuthenticationDescription': '拉起HiVision扫描弹框描述','remoteAuthenticationPicture':''}"; params.setJsonParams(jsonParams); continuationRegisterManager.register(BUNDLE_NAME, params, callback, requestCallback); } };
通过流转任务管理服务提供的showDeviceList()接口获取选择设备列表,用户选择设备后在IContinuationDeviceCallback回调中获取设备ID。
// 显示设备列表,获取设备信息 private ClickedListener mShowDeviceListListener = new ClickedListener() { @Override public void onClick(Component arg0) { // 设置过滤设备类型 ExtraParams params = new ExtraParams(); String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE}; params.setDevType(devTypes); String jsonParams = "{'filter':{'commonFilter':{'system':{'harmonyVersion':'2.0.0'},'groupType':'1|256','curComType': 0x00030004,'faFilter':'{\"localVersionCode\":1,\"localMinCompatibleVersionCode\":2,\"targetBundleName\": \"com.xxx.yyy\"}'}},'transferScene':0,'remoteAuthenticationDescription': '拉起HiVision扫描弹框描述','remoteAuthenticationPicture':''}"; params.setJsonParams(jsonParams); // 显示选择设备列表 continuationRegisterManager.showDeviceList(abilityToken, params, null); } };
为启动远程FA/PA的按钮设置点击回调,实现启动FA/PA和关闭远程PA的能力。
// 启动远程FA/PA private ClickedListener mStartRemoteListener = new ClickedListener() { @Override public void onClick(Component arg0) { if (selectDeviceId != null) { // 通过showDeviceList获取指定目标设备deviceId // 指定待启动FA/PA的bundleName和abilityName // 设置分布式标记,表明当前涉及分布式能力 Operation operation = new Intent.OperationBuilder() .withDeviceId(selectDeviceId) .withBundleName(REMOTE_BUNDLE_NAME) .withAbilityName(REMOTE_FA_NAME) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); Intent startIntent = new Intent(); startIntent.setOperation(operation); // 通过AbilitySlice包含的startAbility接口实现跨设备启动FA/PA startAbility(startIntent); } else { btnStartRemote.setText("StartRemote selectDeviceId is null"); } } }; // 关闭远程PA private ClickedListener mStopRemoteListener = new ClickedListener() { @Override public void onClick(Component arg0) { if (selectDeviceId != null) { // 通过showDeviceList获取指定目标设备deviceId // 指定待关闭PA的bundleName和abilityName // 设置分布式标记,表明当前涉及分布式能力 Operation operation = new Intent.OperationBuilder() .withDeviceId(selectDeviceId) .withBundleName(REMOTE_BUNDLE_NAME) .withAbilityName(REMOTE_PA_NAME) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); Intent stopIntent = new Intent(); stopIntent.setOperation(operation); // 通过AbilitySlice包含的stopAbility接口实现跨设备关闭PA stopAbility(stopIntent); } else { btnStopRemote.setText("StopRemote selectDeviceId is null"); } } };
需要注意,目标FA/PA需要在config.json中设置“visible”为true。visible标签表示Ability是否可以被其他应用调用,默认为false,即只允许同应用(同appid)访问;如需被其他应用访问,需要将其设置为true,同时建议在目标FA/PA中添加自定义权限,控制访问范围,防止被其他应用随意访问。
在config.json中的配置如下:
{ "module": { "abilities": [ { ... "visible": true ... } ] ... } ... }
设备A连接设备B侧的PA,利用连接关系调用该PA执行特定任务,以及断开连接。
// 当连接完成时,用来提供管理已连接PA的能力 private MyRemoteProxy mProxy = null; // 用于管理连接关系 private IAbilityConnection mConn = new IAbilityConnection() { @Override public void onAbilityConnectDone(ElementName element, IRemoteObject remote, int resultCode) { // 跨设备PA连接完成后,会返回一个序列化的IRemoteObject对象 // 通过该对象得到控制远端服务的代理 mProxy = new MyRemoteProxy(remote); btnConnectRemotePA.setText("connectRemoteAbility done"); } @Override public void onAbilityDisconnectDone(ElementName element, int resultCode) { // 当已连接的远端PA异常关闭时,会触发该回调 // 支持开发者按照返回的错误信息进行PA生命周期管理 disconnectAbility(mConn); } };
仅通过启动/关闭两种方式对PA进行调度无法应对需长期交互的场景,因此,系统向开发者提供了跨设备PA连接及断开连接的能力。为了对已连接PA进行管理,开发者需要实现一个满足IAbilityConnection接口的连接状态检测实例,通过该实例可以对连接及断开连接完成时设置具体的处理逻辑,例如:获取控制对端PA的代理等。进一步为了使用该代理跨设备调度PA,开发者需要在本地及对端分别实现对外接口一致的代理。一个具备加法能力的代理示例如下:
// 以连接提供加法计算能力的PA为例。为了提供跨设备连接能力,需要在本地发起连接侧和对端被连接侧分别实现代理 // 发起连接侧的代理示例如下: public class MyRemoteProxy implements IRemoteBroker { private static final int ERR_OK = 0; private static final int COMMAND_PLUS = IRemoteObject.MIN_TRANSACTION_ID; private static final String DESCRIPTOR = "com.XXX.DESCRIPTOR"; private final IRemoteObject remote; public MyRemoteProxy(IRemoteObject remote) { this.remote = remote; } @Override public IRemoteObject asObject() { return remote; } public int plus(int a, int b) throws RemoteException { MessageParcel data = MessageParcel.obtain(); MessageParcel reply = MessageParcel.obtain(); // option不同的取值,决定采用同步或异步方式跨设备控制PA // 本例需要同步获取对端PA执行加法的结果,因此采用同步的方式,即MessageOption.TF_SYNC // 具体MessageOption的设置,可参考相关API文档 MessageOption option = new MessageOption(MessageOption.TF_SYNC); data.writeInterfaceToken(DESCRIPTOR); data.writeInt(a); data.writeInt(b); try { remote.sendRequest(COMMAND_PLUS, data, reply, option); int errCode = reply.readInt(); if (errCode != ERR_OK) { throw new RemoteException(); } int result = reply.readInt(); return result; } finally { data.reclaim(); reply.reclaim(); } } }
此外,对端待连接的PA需要实现对应的客户端,代码示例如下所示:
// 以计算加法为例,对端实现的客户端如下 public class MyRemote extends RemoteObject implements IRemoteBroker{ private static final int ERR_OK = 0; private static final int ERROR = -1; private static final int COMMAND_PLUS = IRemoteObject.MIN_TRANSACTION_ID; private static final String DESCRIPTOR = "com.XXX.DESCRIPTOR"; public MyRemote() { super("MyService_Remote"); } @Override public IRemoteObject asObject() { return this; } @Override public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { String token = data.readInterfaceToken(); if (!DESCRIPTOR.equals(token)) { reply.writeInt(ERROR); return false; } if (code != COMMAND_PLUS) { reply.writeInt(ERROR); return false; } int value1 = data.readInt(); int value2 = data.readInt(); int sum = value1 + value2; reply.writeInt(ERR_OK); reply.writeInt(sum); return true; } }
对端除了要实现如上所述的客户端外,待连接的PA还需要作如下修改:
// 为了返回给连接方可调用的代理,需要在该PA中实例化客户端,例如作为该PA的成员变量 private MyRemote remote = new MyRemote(); // 当该PA接收到连接请求时,即将该客户端转化为代理返回给连接发起侧 @Override protected IRemoteObject onConnect(Intent intent) { super.onConnect(intent); return remote.asObject(); }
创建远程连接目标PA的步骤可参考创建Service。需要注意,目标PA需要在config.json中设置“visible”为true。visible标签表示Ability是否可以被其他应用调用,默认为false,即只允许同应用(同appid)访问;如需被其他应用访问,需要将其设置为true,同时建议在目标PA中添加自定义权限,控制访问范围,防止被其他应用随意访问。
在config.json中的配置如下:
{ "module": { "abilities": [ { ... "visible": true ... } ] ... } ... }
完成上述步骤后,可以通过点击事件实现连接、利用连接关系控制PA以及断开连接等行为,代码示例如下:
// 连接远程PA private ClickedListener mConnectRemotePAListener = new ClickedListener() { @Override public void onClick(Component arg0) { if (selectDeviceId != null) { // 指定待连接PA的bundleName和abilityName // 设置分布式标记,表明当前涉及分布式能力 Operation operation = new Intent.OperationBuilder() .withDeviceId(selectDeviceId) .withBundleName(REMOTE_BUNDLE_NAME) .withAbilityName(REMOTE_PA_NAME) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); Intent connectPAIntent = new Intent(); connectPAIntent.setOperation(operation); // 通过AbilitySlice包含的connectAbility接口实现跨设备连接PA connectAbility(connectPAIntent, mConn); } } }; // 控制已连接PA执行加法 private ClickedListener mControlPAListener = new ClickedListener() { @Override public void onClick(Component arg0) { if (mProxy != null) { int ret = -1; try { ret = mProxy.plus(10, 20); } catch (RemoteException e) { HiLog.error(LABEL_LOG, "ControlRemotePA error"); } btnControlRemotePA.setText("ControlRemotePA result = " + ret); } } }; // 与远程PA断开连接 private ClickedListener mDisconnectRemotePAListener = new ClickedListener() { @Override public void onClick(Component arg0) { // 按钮复位 btnConnectRemotePA.setText("ConnectRemotePA"); btnControlRemotePA.setText("ControlRemotePA"); disconnectAbility(mConn); } };
说明
通过连接/断开连接远程PA,与跨设备PA建立长期的管理关系。例如在本例中,通过连接关系得到远程PA的控制代理后,实现跨设备计算加法并将结果返回到本地显示。在实际开发中,开发者可以根据需要实现多种分布式场景,例如:跨设备位置/电量等信息的采集、跨设备计算资源互助等。