一、效果展示
二、项目介绍
\qquad本项目以 ArkUI(JS)开发界面,利用 JS FA 调用 JAVA PA 的接口功能,通过 JAVA 端拉取本地相册,选择图片后返回,并将图片复制到 JS IMAGE 组件可访问的目录下以及将图片路径返回至 JS 端。最终实现 JS 界面展示本地相册图片的功能。
\qquad项目提供两种方案,Internal Ability 以及 LocalParticleAbility ,两种方案思路基本一致,最终效果也都一样。
\qquad通过这个项目来和大家分享我目前使用 JS FA 调用 JAVA PA 功能的经验以及 JS 展示相册照片的思路,同时也希望各位能指出我代码中的不足。项目中部分代码来源于各教程。
三、代码结构展示
Java 端
- getPhotoInternalAbility:Internal Ability 方式与 JS 端交互。
- getPhotoLocalParticleAbility:LocalParticleAbility 方式与 JS 端交互。
- Utils:工具类,保存照片。
- MainAbility:主 Ability。
- Js 端:基本展示页面(index.hml、index.css、index.js)。
四、关键问题
4.1 权限问题
由于要访问用户媒体文件,需要申请媒体文件相关权限。
config.json 权限配置:
"reqPermissions": [
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
},
{
"name": "ohos.permission.MEDIA_LOCATION"
}
]
可选择添加动态申请权限,添加则有权限提示。
// 动态申请权限
private void requestPermissions() {
String[] permissions =
{
SystemPermission.MEDIA_LOCATION,
SystemPermission.WRITE_MEDIA,
SystemPermission.READ_MEDIA,
};
List<String> permissionFiltered =
Arrays.stream(permissions)
.filter(permission -> verifySelfPermission(permission)
!= IBundleManager.PERMISSION_GRANTED).collect(Collectors.toList());
requestPermissionsFromUser(permissionFiltered.toArray(new String[permissionFiltered.size()]), 0);
}
4.2 页面跳转问题
与正常跳转其它 Ability 页面相同,我们需要通过 Intent 来指定启动目标。
此处我们根据需要选择图片的能力来构建 intent 并传入 startAbility,以达到跳转本地相册的目的。
Intent intent = new Intent();
Operation opt=new Intent.OperationBuilder()
.withAction(IntentConstants.ACTION_CHOOSE)
.build();
intent.setOperation(opt);
intent.addFlags(Intent.FLAG_NOT_OHOS_COMPONENT);
intent.setType("image/*");
abilityContext.startAbility(intent,code); // 此处 code 随意
关键点在于 abilityContext 的获取,很容易我们可以得知 startAbility 是抽象类 AbilityContext 中的方法,MainAbility 继承自 AceAbility ,而 AceAbility 的父类 Ability 则是 AbilityContext 的具体实现类。
我们操作的主线程(UI 线程)上下文应当是由 MainAbility 管理,此时我们需要从一个 Ability 跳转至另一个 Ability,很明显 abilityContext 的获取与 MainAbility 有关。
通过浏览官方文档不难发现,Internal Ability 以及 LocalParticleAbility 的实现均与 MainAbility 有着密切关系,这两种方式都存在注册与注销的方法,所需参数不正是 MainAbility ?
4.2.1 Internal Ability 方式
getPhotoInternalAbility:
private AbilityContext abilityContext;
/**
* Internal ability 注册接口。
*/
public static void register(AbilityContext abilityContext) {
instance = new getPhotoInternalAbility();
instance.onRegister(abilityContext);
}
private void onRegister(AbilityContext abilityContext) {
this.abilityContext = abilityContext;
}
MainAbility:
@Override
public void onStart(Intent intent) {
getPhotoInternalAbility.register(this);
super.onStart(intent);
}
4.2.2 LocalParticleAbility
getPhotoLocalParticleAbility:
// 重写注册方法即可
private AbilityContext abilityContext;
@Override
public void register(AceAbility ability) {
abilityContext = ability;
LocalParticleAbility.super.register(ability);
}
MainAbility:
@Override
public void onStart(Intent intent) {
super.onStart(intent);
getPhotoLocalParticleAbility.getInstance().register(this);
}
4.2.3 JS 端
JS 侧只需简单调用 JAVA 有关跳转页面的代码即可。
例:
makeAction(bundleName, abilityName, code, abilityType, data) {
const action = {};
action.bundleName = bundleName;
action.abilityName = abilityName;
action.messageCode = code;
action.abilityType = abilityType;
action.data = data;
action.syncOption = 0;
return action;
},
async testGetPhotoInternalAbility() {
const action = this.makeAction('cn.crcrc.arkui_example',
'cn.crcrc.arkui_example.ability.getPhotoInternalAbility', 1001, 1, {});
const result = await FeatureAbility.callAbility(action);
console.info(result)
if(result != null){
this.avatarURL = result
}
},
testGetPhotoLocalParticleAbility(){
this.javaInterface = createLocalParticleAbility('cn.crcrc.arkui_example.ability.getPhotoLocalParticleAbility');
this.javaInterface.getPhotoUri().then(result => {
console.info(result);
if(result != null){
this.avatarURL = result
}
}, error => {
console.error('testGetPhotoLocalParticleAbility error');
});
}
4.3 返回值问题
AbilityContext.startAbility 的结果是可以通过 Ability 的回调方法 onAbilityResult 来获得。通过以上分析,显而易见,由于是通过 MainAbility 来 startAbility 的,那么 onAbilityResult 自然是需在 MainAbility 重写以获得结果。
此时便要解决将相册返回值传递给 Internal Ability 或 LocalParticleAbility,进而返回所得图片地址给 JS 端的问题。
此处我选择通过有序公共事件来传播消息。基本思路即在 MainAbility 的 onAbilityResult 中发布事件,附带相册图片 Uri 值;在 Internal Ability 或 LocalParticleAbility 中订阅事件,得到 Uri 值后通过 Utils 类将图片经自定义处理后存入 JS IMAGE 组件可访问的地址中。
MainAbility:
/*
* 发布有序的公共事件
*/
private void orderlyEventPublish(String data) {
HiLog.info(LABEL, "发布有序公共事件开始");
//1.构建一个Intent对象,包含了自定义的事件的标识符
Intent intent = new Intent();
Operation oper = new Intent.OperationBuilder().withAction("cn.crcrc.arkui_example.event") //就是自定义的公共事件的标识
.build();
intent.setOperation(oper);
//2.构建CommonEventData对象
CommonEventData commonEventData = new CommonEventData(intent);
//仅仅只有有序的公共事件,才能携带的两个专用属性,可选的参数,不是必须的
commonEventData.setData(data);
commonEventData.setCode(1001);
//配置公共事件的对应权限
CommonEventPublishInfo publishInfo = new CommonEventPublishInfo();
publishInfo.setOrdered(true);
//3.核心的发布事件的动作,发布的公共事件,有序的公共事件
try {
CommonEventManager.publishCommonEvent(commonEventData, publishInfo);
HiLog.info(LABEL, "发布有序公共事件完成");
} catch (RemoteException e) {
e.printStackTrace();
}
}
/**
* 跳转回调
*/
@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
super.onAbilityResult(requestCode, resultCode, resultData);
try {
System.out.println("APP LOG resultData:" + resultData.getUriString());
// 通过发布有序公共事件传递信息
orderlyEventPublish(resultData.getUriString());
} catch (Exception e) {
orderlyEventPublish("fail");
e.printStackTrace();
}
}
Internal Ability 或 LocalParticleAbility 中订阅事件,例:
/**
* 订阅事件
*/
private void subscribeCommonEvent() {
if(!isSubscribe){
HiLog.info(LABEL,"订阅开始:");
//1.构建MatchingSkills对象
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent("cn.crcrc.arkui_example.event"); //订阅自定义的公共事件
//2.构建订阅信息对象
CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
//3.构建订阅者对象
subscriber = new CommonEventSubscriber(subscribeInfo){
// 回调方法
@Override
public void onReceiveEvent(CommonEventData commonEventData) {
HiLog.info(LABEL,"已接收公共事件");
Boolean setImaDataResult = false;
if(commonEventData.getData() != null)
{
System.out.println("接收到数据:" + commonEventData.getData());
if(!commonEventData.getData().equals("fail"))
{
Uri uri = Uri.parse(commonEventData.getData());
long l = System.currentTimeMillis();
String fileName = String.valueOf(l);
setImaDataResult = utils.setImaData(uri,fileName);
photoUriString = "internal://app/" + String.valueOf(l) + ".jpg";
HiLog.info(LABEL,"js 访问图片路径:" + photoUriString);
}
latch.countDown();
}else {
HiLog.info(LABEL,"已接收公共事件,但数据为空");
System.out.println("APP LOG 已接收公共事件,但数据为空");
}
// 目前 onReceiveEvent 只能在 ui 主线程上执行
// 耗时任务派发到子线程异步执行,保证不阻塞 ui 线程
final AsyncCommonEventResult result = goAsyncCommonEvent();
Boolean finalSetImaDataResult = setImaDataResult;
handler.postTask(new Runnable() {
@Override
public void run() {
if(finalSetImaDataResult){
HiLog.info(LABEL,"进行数据库操作");
// 存入数据库
}
HiLog.info(LABEL,"数据库操作完成");
result.finishCommonEvent();//结束事件
}
});
}
};
//4.订阅公共事件的核心动作
try {
CommonEventManager.subscribeCommonEvent(subscriber);
isSubscribe = true;
} catch (RemoteException e) {
e.printStackTrace();
}
}else{
HiLog.info(LABEL,"公共事件不能重复订阅");
}
}
可将订阅以及取消订阅动作分别放入注册与注销动作中,以 LocalParticleAbility 为例:
@Override
public void register(AceAbility ability) {
abilityContext = ability;
LocalParticleAbility.super.register(ability);
utils = new Utils(abilityContext); // 得到实例
// 注册公共事件
subscribeCommonEvent();
}
@Override
public void deregister(AceAbility ability) {
abilityContext = null;
LocalParticleAbility.super.deregister(ability);
// 取消订阅
try{
CommonEventManager.unsubscribeCommonEvent(subscriber);
}catch(RemoteException e) {
HiLog.error(LABEL,"Exception occurred during unsubscribeCommonEvent invocation.");
}
}
注意:
- JS 调用的 JAVA 端方法会在订阅事件中获取到图片地址之前返回,导致 JS 端获得空值。故此处我采取 CountDownLatch 方法以同步传递数据。
- 初始化订阅事件时,其中的回调方法会执行一次,故其中要加判断条件,防止 latch.countDown() 出错。
- 由于可能不选择图片直接返回,导致 onAbilityResult 的 resultData 为空,故 resultData.getUriString 应 catch 异常。
其中解决 JAVA 端提前返回值应该有其它解决方法,比如用 LocalParticleAbility 的 CallBack 方法开线程等图片返回值,并将其异步返回。或者 JAVA 端先将其存储下来, JS 端调 JAVA PA 相关函数查询对应键值即可。
五、调试日志
六、对比与总结
编写代码时,很容易体会到 LocalParticleAbility 方式较 Internal Ability 方便,尤其是 JS 端与 JAVA 端参数传递上。
LocalParticleAbility 方式就好像 JS 端直接调用其中方法实现功能一样,而 Internal Ability 需要在 onRemoteRequest 根据传递的 code 判断执行哪个方法。
此项目再结合下 JAVA 或 JS 端数据库功能,即可实现如下效果:
项目中有关图片转存的函数实现,查看上传的相应文件即可。
其实 JS 端亦可拉起图片选择能力,但需要返回值的对应方法要 API7 才支持。
var str = {
"want": {
"type": "image/*",
"action": "android.intent.action.GET_CONTENT",
"flags": wantConstant.Flags.FLAG_NOT_OHOS_COMPONENT
},
};
featureAbility.startAbilityForResult(str, (error, data) => {
if (error) {
console.error('Operation failed. Cause: ' + error);
return;
}
console.info('Operation succeeded: ' + data);
});