注意,本问题的解决方案需要对原生打包熟练,并且能编写简单原生代码(如有过编写原生插件经验)
另,ios只要正确设置Background Modes和对应权限申请描述即可,本文只关心安卓端
问题描述:
watchPosition监听位置信息,每间隔一段时间获取位置信息上传,实现后台实时定位,当app退到后台或锁屏一段时间(通常几分钟)后,便获取不到位置信息了。
解决方案:(以百度SDK为例)
第一种:
根据百度官方建议:http://lbsyun.baidu.com/index.php?title=android-locsdk/guide/addition-func/android8-notice,我们可以自行编写一个原生插件,如:
package com.XX.XXX.H5PlusPlugin;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import io.dcloud.common.DHInterface.IWebview;
import io.dcloud.common.DHInterface.StandardFeature;
import io.dcloud.common.util.JSUtil;
import com.XX.XXX.util.NotificationUtils;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.BDLocation;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.text.ParsePosition;
public class BaiduLocation extends StandardFeature {
public String callBackID = "";
public Activity curActivity;
public IWebview curWebview;
private LocationClient mClient;
private NotificationUtils mNotificationUtils;
private Notification notification;
public void onStart(Context pContext, Bundle pSavedInstanceState, String[] pRuntimeArgs) {
/**
* 如果需要在应用启动时进行初始化,可以继承这个方法,并在properties.xml文件的service节点添加扩展插件的注册即可触发onStart方法
* */
}
public void watchPosition(IWebview pWebview, JSONArray array) {
curWebview = pWebview;
curActivity = pWebview.getActivity();
// 原生代码中获取JS层传递的参数,
// 参数的获取顺序与JS层传递的顺序一致
callBackID = array.optString(0);
//String verifyToken = array.optString(1);
// 定位初始化
mClient = new LocationClient(pWebview.getContext());
LocationClientOption mOption = new LocationClientOption();
mOption.setScanSpan(10000);
mOption.setCoorType("bd09ll");
mOption.setIsNeedAddress(true);
mOption.setOpenGps(true);
mClient.setLocOption(mOption);
//mClient.registerLocationListener(myLocationListener);
mClient.registerLocationListener(new BDAbstractLocationListener() {
public void onReceiveLocation(BDLocation bdLocation) {
JSONObject json=makeJSON(bdLocation,"bd09ll");
//结果返回给js层
JSUtil.execCallback(curWebview, callBackID, json.toString(), JSUtil.OK, true,true);
}
});
//设置后台定位
//android8.0及以上使用NotificationUtils
if (Build.VERSION.SDK_INT >= 26) {
mNotificationUtils = new NotificationUtils(pWebview.getContext());
Notification.Builder builder2 = mNotificationUtils.getAndroidChannelNotification
("适配android 8限制后台定位功能", "正在后台定位");
notification = builder2.build();
} else {
//获取一个Notification构造器
Notification.Builder builder = new Notification.Builder(curActivity);
Intent nfIntent = new Intent(curActivity, curActivity.getClass());
builder.setContentIntent(PendingIntent.
getActivity(curActivity, 0, nfIntent, 0)) // 设置PendingIntent
.setContentTitle("适配android 8限制后台定位功能") // 设置下拉列表里的标题
.setSmallIcon(android.R.drawable.btn_star) // 设置状态栏内的小图标
.setContentText("正在后台定位") // 设置上下文内容
.setWhen(System.currentTimeMillis()); // 设置该通知发生的时间
notification = builder.build(); // 获取构建好的Notification
}
notification.defaults = Notification.DEFAULT_SOUND; //设置为默认的声音
mClient.enableLocInForeground(1, notification);
mClient.start();
}
private JSONObject makeJSON(BDLocation pLoc, String coordsType) {
JSONObject json = null;
try {
json = new JSONObject();
json.put("latitude", pLoc.getLatitude());
json.put("longitude", pLoc.getLongitude());
json.put("altitude", pLoc.getAltitude());
json.put("accuracy", pLoc.getRadius());
json.put("altitudeAccuracy", 0);
json.put("heading", pLoc.getDirection());
json.put("velocity", pLoc.getSpeed());
json.put("coordsType", coordsType);
try {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ParsePosition pos = new ParsePosition(0);
Date strtodate = formatter.parse(pLoc.getTime(), pos);
json.put("timestamp", strtodate.getTime());
} catch (Exception e) {
e.printStackTrace();
json.put("timestamp", pLoc.getTime());
}
json.put("address", pLoc.getAddrStr());
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
}
其中com.XX.XXX.util.NotificationUtils是自行编写的一个生成通知的类,在百度定位sdk的demo工程中可以拷贝。
由于dcloud已经引入过百度SDK(baidu-libs-release.aar),我们不需要再添加引用。
按原生插件配置文档配置好之后,编写好js插件,然后在html页面调用即可,如
plus.BaiduLocation.watchPosition(function(p){
$('#div_position').prepend('<p>'+moment().format('HH:mm:ss')+'</p><p>'+JSON.stringify(p)+'</p>');
});
第二种:
直接修改DCloud官方H5+的watchPosition方法。
该方法在geolocation-baidu-release.aar中,利用jd-ui等工具反编译导出java文件,然后新建工程,修改代码重新编译替换.class即可。(注意新建工程需要引用依赖的包lib.5plus.base-release.aar、baidu-libs-release.aar)
修改的代码就是按百度官方建议的,加上前台服务通知,调用enableLocInForeground开启前台定位,如:
package io.dcloud.js.geolocation.baidu;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.BDLocation;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import io.dcloud.common.DHInterface.FeatureMessageDispatcher;
import io.dcloud.common.DHInterface.IEventCallback;
import io.dcloud.common.DHInterface.IWebview;
import io.dcloud.common.adapter.ui.AdaFrameView;
import io.dcloud.common.adapter.util.AndroidResources;
import io.dcloud.common.adapter.util.Logger;
import io.dcloud.common.adapter.util.SP;
import io.dcloud.common.util.JSUtil;
import io.dcloud.common.util.NetTool;
import io.dcloud.common.util.PdrUtil;
import io.dcloud.common.util.StringUtil;
import io.dcloud.js.geolocation.GeoManagerBase;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
public class BaiduGeoManager
extends GeoManagerBase
{
public static final String TAG = BaiduGeoManager.class.getSimpleName();
boolean hasAppkey = false;
boolean isGeocode = true;
boolean isStreamApp = false;
static BaiduGeoManager mInstance;
LocationClient mClient = null;
LocationClientOption mOption = null;
HashMap<String, LocationClient> mContinuousMap = new HashMap<>();
HashMap<String, LocationClient> mSingleTimeMap = new HashMap<>();
private NotificationManager mManager;
public static final String ANDROID_CHANNEL_NAME = "ANDROID CHANNEL";
private Notification notification;
public Activity curActivity;
public BaiduGeoManager(Context pContext) {
super(pContext);
this.hasAppkey = !PdrUtil.isEmpty(AndroidResources.getMetaValue("com.baidu.lbsapi.API_KEY"));
}
public static BaiduGeoManager getInstance(Context pContext) {
pContext = pContext.getApplicationContext();
if (mInstance != null) {
return mInstance;
}
mInstance = new BaiduGeoManager(pContext);
return mInstance;
}
public String execute(IWebview pWebViewImpl, String pActionName, String[] pJsArgs) {
curActivity=pWebViewImpl.getActivity();
String result = "";
try {
this.isStreamApp = pWebViewImpl.obtainApp().isStreamApp();
String t = (pJsArgs.length > 7) ? pJsArgs[6] : "null";
int timeout = Integer.MAX_VALUE;
if (!"null".equals(t)) {
timeout = Integer.parseInt(t);
}
String intervals = (pJsArgs.length > 8) ? pJsArgs[7] : "5000";
int interval = 5000;
if (!intervals.equals("null")) {
interval = Integer.parseInt(intervals);
if (interval < 1000) {
interval = 1000;
}
}
if (pActionName.startsWith("getCurrentPosition")) {
this.isGeocode = Boolean.parseBoolean(pJsArgs[5]);
boolean _enableHighAccuracy = Boolean.parseBoolean(pJsArgs[1]);
boolean isNotWgs84 = !PdrUtil.isEquals("wgs84", pJsArgs[3]);
if (isNotWgs84) {
startLocating(pWebViewImpl, pJsArgs[0], null, _enableHighAccuracy, timeout, -1, pActionName.endsWith("DLGEO"), pJsArgs[3], false);
} else {
String _json = StringUtil.format("{code:%d,message:'%s'}", new Object[] { Integer.valueOf(17), isNotWgs84 ? "指定的provider不存在或无效" : "only support gcj02|bd09|bd09ll" });
JSUtil.execCallback(pWebViewImpl, pJsArgs[0], _json, JSUtil.ERROR, true, false);
}
} else if (pActionName.startsWith("watchPosition")) {
this.isGeocode = Boolean.parseBoolean(pJsArgs[5]);
boolean _enableHighAccuracy = Boolean.parseBoolean(pJsArgs[2]);
pWebViewImpl.obtainFrameView().addFrameViewListener(new IEventCallback()
{
public Object onCallBack(String pEventType, Object pArgs) {
if ((PdrUtil.isEquals(pEventType, "window_close") || PdrUtil.isEquals(pEventType, "close")) && pArgs instanceof IWebview) {
BaiduGeoManager.this.stopContinuousLocating();
((AdaFrameView)((IWebview)pArgs).obtainFrameView()).removeFrameViewListener(this);
}
return null;
}
});
boolean isNotWgs84 = !PdrUtil.isEquals("wgs84", pJsArgs[3]);
if (isNotWgs84) {
startLocating(pWebViewImpl, pJsArgs[0], pJsArgs[1], _enableHighAccuracy, timeout, interval, pActionName.endsWith("DLGEO"), pJsArgs[3], true);
} else {
String _json = StringUtil.format("{code:%d,message:'%s'}", new Object[] { Integer.valueOf(17), isNotWgs84 ? "指定的provider不存在或无效" : "only support gcj02|bd09|bd09ll" });
JSUtil.execCallback(pWebViewImpl, pJsArgs[0], _json, JSUtil.ERROR, true, false);
}
} else if (pActionName.startsWith("clearWatch")) {
this.keySet.remove(pJsArgs[0]);
LocationClient tClient=(LocationClient)this.mContinuousMap.remove(pJsArgs[0]);
tClient.disableLocInForeground(true);
tClient.stop();
}
return result;
} catch (Exception e) {
Logger.e(TAG, "e.getMessage()==" + e.getMessage());
return result;
}
}
public void startLocating(final IWebview pWebViewImpl, final String pCallbackId, String key, boolean enableHighAccuracy, int timeOut, int intervals, final boolean isDLGeo, final String coordsType, final boolean continuous) {
if (this.hasAppkey) {
this.mClient = new LocationClient(pWebViewImpl.getContext());
this.mOption = new LocationClientOption();
if (PdrUtil.isEmpty(key)) {
this.mOption.setScanSpan(0);
this.mSingleTimeMap.put(pCallbackId, this.mClient);
} else {
this.mOption.setScanSpan(intervals);
this.mOption.setLocationNotify(true);
this.keySet.add(key);
this.mContinuousMap.put(key, this.mClient);
}
if (NetTool.isNetworkAvailable(this.mContext)) {
if (enableHighAccuracy) {
this.mOption.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
} else {
this.mOption.setLocationMode(LocationClientOption.LocationMode.Battery_Saving);
}
this.mOption.setTimeOut(timeOut);
} else {
this.mOption.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
if (Integer.MAX_VALUE == timeOut) {
this.mOption.setTimeOut(3000);
} else {
this.mOption.setTimeOut(timeOut);
}
}
this.mOption.setIsNeedAddress(this.isGeocode);
this.mOption.setCoorType(getCoorType(coordsType));
this.mClient.setLocOption(this.mOption);
this.mClient.registerLocationListener(new BDAbstractLocationListener()
{
public void onReceiveLocation(BDLocation bdLocation) {
if (bdLocation.getAddress() != null) {
FeatureMessageDispatcher.dispatchMessage("record_address", (bdLocation.getAddress() != null) ? (bdLocation.getAddress()).address : null);
}
Logger.e(BaiduGeoManager.TAG, "onReceiveLocation bdLocation==" + bdLocation.toString());
BaiduGeoManager.this.callBack2Front(pWebViewImpl, pCallbackId, bdLocation, BaiduGeoManager.this.getCoorType(coordsType), isDLGeo, continuous);
}
});
if(continuous) {
//设置后台定位
if (Build.VERSION.SDK_INT >= 26) {
createChannels();
Notification.Builder builder2 = getAndroidChannelNotification("适配android 8限制后台定位功能", "正在后台定位");
notification = builder2.build();
} else {
//获取一个Notification构造器
Notification.Builder builder = new Notification.Builder(curActivity);
Intent nfIntent = new Intent(curActivity, curActivity.getClass());
builder.setContentIntent(PendingIntent.
getActivity(curActivity, 0, nfIntent, 0)) // 设置PendingIntent
.setContentTitle("适配android 8限制后台定位功能") // 设置下拉列表里的标题
.setSmallIcon(android.R.drawable.btn_star) // 设置状态栏内的小图标
.setContentText("正在后台定位") // 设置上下文内容
.setWhen(System.currentTimeMillis()); // 设置该通知发生的时间
notification = builder.build(); // 获取构建好的Notification
}
notification.defaults = Notification.DEFAULT_SOUND; //设置为默认的声音
this.mClient.enableLocInForeground(1,notification);
}
this.mClient.start();
} else {
String _json = StringUtil.format("{code:%d,message:'%s'}", new Object[] { Integer.valueOf(16), "has not baidu appkey" });
JSUtil.execCallback(pWebViewImpl, pCallbackId, _json, JSUtil.ERROR, true, false);
}
}
public void createChannels() {
// create android channel
NotificationChannel androidChannel = new NotificationChannel(curActivity.getPackageName()+".BDLocation",
ANDROID_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
// Sets whether notifications posted to this channel should display notification lights
androidChannel.enableLights(true);
// Sets whether notification posted to this channel should vibrate.
androidChannel.enableVibration(true);
// Sets the notification light color for notifications posted to this channel
androidChannel.setLightColor(Color.GREEN);
// Sets whether notifications posted to this channel appear on the lockscreen or not
androidChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
getManager().createNotificationChannel(androidChannel);
}
private NotificationManager getManager() {
if (mManager == null) {
mManager = (NotificationManager) curActivity.getSystemService(Context.NOTIFICATION_SERVICE);
}
return mManager;
}
public Notification.Builder getAndroidChannelNotification(String title, String body) {
return new Notification.Builder(curActivity.getApplicationContext(), curActivity.getPackageName()+".BDLocation")
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(android.R.drawable.btn_star)
.setAutoCancel(true);
}
private void stopContinuousLocating() {
for (Map.Entry<String, LocationClient> entry : this.mContinuousMap.entrySet()) {
System.out.println("key= " + (String)entry.getKey() + " and value= " + entry.getValue());
if (!PdrUtil.isEmpty(entry.getValue())) {
LocationClient tClient=(LocationClient)entry.getValue();
tClient.disableLocInForeground(true);
tClient.stop();
}
}
}
private void callBack2Front(IWebview mWebview, String mCallbackId, BDLocation location, String CoordsType, boolean isDLGeo, boolean continuous) {
if (!continuous &&
!PdrUtil.isEmpty(this.mSingleTimeMap.get(mCallbackId))) {
((LocationClient)this.mSingleTimeMap.get(mCallbackId)).stop();
}
JSONObject _json = makeJSON(location, CoordsType);
if (_json == null) {
geoDataError(mWebview, mCallbackId, isDLGeo, continuous);
} else {
callback(mWebview, mCallbackId, _json.toString(), JSUtil.OK, true, isDLGeo, continuous);
}
}
public void callback(IWebview webview, String callId, String json, int code, boolean isJson, boolean isDLGeo, boolean continuous) {
if (isDLGeo) {
JSUtil.execGEOCallback(webview, callId, json, code, isJson, continuous);
} else {
JSUtil.execCallback(webview, callId, json, code, isJson, continuous);
}
}
private JSONObject makeJSON(BDLocation pLoc, String coordsType) {
JSONObject json = null;
try {
json = new JSONObject();
json.put("latitude", pLoc.getLatitude());
json.put("longitude", pLoc.getLongitude());
json.put("altitude", pLoc.getAltitude());
json.put("accuracy", pLoc.getRadius());
json.put("altitudeAccuracy", 0);
json.put("heading", pLoc.getDirection());
json.put("velocity", pLoc.getSpeed());
json.put("coordsType", coordsType);
try {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ParsePosition pos = new ParsePosition(0);
Date strtodate = formatter.parse(pLoc.getTime(), pos);
json.put("timestamp", strtodate.getTime());
} catch (Exception e) {
e.printStackTrace();
json.put("timestamp", pLoc.getTime());
}
if (this.isGeocode) {
JSONObject address = new JSONObject();
json.put("address", address);
address.put("country", pLoc.getCountry());
address.put("province", pLoc.getProvince());
address.put("city", pLoc.getCity());
address.put("district", pLoc.getDistrict());
address.put("street", pLoc.getStreet());
address.put("streetNum", pLoc.getStreetNumber());
address.put("poiName", (pLoc.getPoiList() != null && pLoc.getPoiList().size() > 0) ? pLoc.getPoiList().get(0) : null);
address.put("postalCode", null);
address.put("cityCode", pLoc.getCityCode());
json.put("addresses", pLoc.getAddrStr());
}
} catch (JSONException e) {
e.printStackTrace();
}
saveGeoData(pLoc, coordsType);
return json;
}
private String getCoorType(String coorType) {
if (PdrUtil.isEquals(coorType, "bd09ll"))
return "bd09ll";
if (PdrUtil.isEquals(coorType, "bd09")) {
return "bd09";
}
return "gcj02";
}
public void onDestroy() {
for (Map.Entry<String, LocationClient> entry : this.mContinuousMap.entrySet()) {
if (!PdrUtil.isEmpty(entry.getValue())) {
((LocationClient)entry.getValue()).stop();
}
}
this.mContinuousMap.clear();
for (Map.Entry<String, LocationClient> entry : this.mSingleTimeMap.entrySet()) {
if (!PdrUtil.isEmpty(entry.getValue())) {
((LocationClient)entry.getValue()).stop();
}
}
this.mSingleTimeMap.clear();
}
private void saveGeoData(BDLocation pLoc, String coordsType) {
if (!this.isStreamApp) {
JSONObject jsonObject = new JSONObject();
JSONObject coordsJson = new JSONObject();
try {
coordsJson.put("latitude", pLoc.getLatitude());
coordsJson.put("longitude", pLoc.getLongitude());
jsonObject.put("coords", coordsJson);
jsonObject.put("coordsType", coordsType);
if (this.isGeocode) {
jsonObject.put("addresses", pLoc.getAddrStr());
}
SharedPreferences startSp = SP.getOrCreateBundle("start_statistics_data");
SP.setBundleData(startSp, "geo_data", jsonObject.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void geoDataError(IWebview pWebViewImpl, String pCallbackId, boolean isDLGeo, boolean continuous) {
String err = StringUtil.format("{code:%d,message:'%s'}", new Object[] { Integer.valueOf(40), "定位异常"});
if (isDLGeo) {
JSUtil.execGEOCallback(pWebViewImpl, pCallbackId, err, JSUtil.ERROR, true, continuous);
} else {
JSUtil.execCallback(pWebViewImpl, pCallbackId, err, JSUtil.ERROR, true, continuous);
}
}
}
总结:
以上代码只是测试功能,写得简单且不规范。其实都只是加入百度SDK已经实现的功能代码。
经打包安装到手机(目前只在华为Novaz5)测试,退到后台锁屏几十分钟再打开查看记录,一直能连续获取到位置信息。
另外,要将手机管家中对该app去掉“自动管理”,并打开“允许后台活动”开关
最后,强烈建议官方修改watchPosition方法(如加个生成前台服务通知的参数?)!!
2 个评论
要回复文章请先登录或注册
z***@163.com
8***@qq.com