深度集成使用高德地图功能--猎鹰(1) 记录轨迹篇

本文适用对象:已继承了猎鹰功能的朋友及使用期间遇到困难的朋友。

背景

前段日子公司要求上线一个功能,具有导航,记录轨迹,查看历史行程等功能的一个模块,我用过百度地图,但是公司继承的是高德地图,索性直接用高德,其实我对百度观感也极差,理由就是–作恶多端。
功能是要做的,时间也很急,12月上旬左右说的需求,还没有UI图,要求年底前上线,我们大概7个移动端开发人员,全都上了,我主要负责的就是记点,记轨迹,查轨迹的功能,其中遇到了很多很多坑,也算是一种提升吧。


过程&难点

过程

技术选型预研阶段,与后台考虑过是否自己上传点位,报告位置,一开始是这么选的,后来感觉对于流量还有并发等问题,也是碰巧遇到了高德的猎鹰,研究了下,就直接用人家的接口了,总比花自己的流量要实惠。
于是乎我就负责了猎鹰的继承,然后还要帮助同事写一点接口,本文没有关于视图层的东西。

难点

遇到了挺多的难点,其实继承第三方并不困难,困难的就在于,第三方有可能文档不全,这是个问题。
我在期间遇到了挺多难题,总结一下吧。
记录轨迹的难题有:

  • 记录点位不全
  • 内存泄漏的问题
  • 8.0以上系统后台定位被限制住

闲话不多说,直接上代码。


实现

依赖

1
2
3
4
implementation 'com.amap.api:navi-3dmap:latest.integration'
implementation 'com.amap.api:search:latest.integration'
implementation 'org.greenrobot:eventbus:3.1.1'
implementation files('libs/AMapTrack_1.1.0_AMapLocation_4.8.0_20191210.jar')

继承猎鹰功能必须用jar包形式, jar包里面顺道包含了定位的sdk,所以不用在二次依赖定位的sdk ,不然会报错。
信息传递使用的是EventBus。

实体类

实体类如下,用于被EventBus传递信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TrackInfo {

private long terminalId;
private long trackId;
private String message;
private boolean canUse;

public boolean isCanUse() {
return canUse;
}

public void setCanUse(boolean canUse) {
this.canUse = canUse;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}



public long getTerminalId() {
return terminalId;
}

public void setTerminalId(long terminalId) {
this.terminalId = terminalId;
}

public long getTrackId() {
return trackId;
}

public void setTrackId(long trackId) {
this.trackId = trackId;
}


}

轨迹服务

官方已经给了启动顺序,我简化了下,根据他的demo也改了下,启动顺序是这样的:

  1. 新建TerminalId,使用QueryTerminalRequest,传入serviceId、terminalName,如果得到terminalId不为0,去往第3步;为0,去第2步。
  2. 因为terminalId=0,所以是一个新的设备,调用AddTerminalRequest,传入serviceId、terminalName,得到terminalId。
  3. 根据上一步的回调得到terminalId,调用AddTrackRequest,传入serviceId、terminalId,得到trackId。
  4. 三个id都得到后,可以根据官方api,调用 client.startTrack()方法,启动轨迹。
  5. 启动轨迹后,在成功的回调里,调用client.startGather()方法,启动收集点位。

几步代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
* 创建路线
*
* @param serviceId
* @param terminalName
* @param trackId
*/
public void createTrack(final long serviceId, final String terminalName, final long trackId) {
this.serviceId = serviceId;
this.trackId = trackId;
client.queryTerminal(new QueryTerminalRequest(serviceId, terminalName), new SimpleOnTrackListener() {
@Override
public void onQueryTerminalCallback(QueryTerminalResponse queryTerminalResponse) {
if (queryTerminalResponse.isSuccess()) {
if (queryTerminalResponse.getTid() <= 0) {//新用户去申请terminalId
addTerminal(serviceId, terminalName, trackId);
} else {
long terminalId = queryTerminalResponse.getTid();//老用户直接获取terminalId
if (trackId != 0) {//此处为断点续传//如果有trackId,则直接返回成功,去startGather
if (trackDoSthListener != null) {
trackDoSthListener.onCreateTrackSuccess(serviceId, terminalId, trackId);
} else {
onTrackListenerNull();
}
} else {//为0为新的
createTrack(serviceId, terminalId);
}
}
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(queryTerminalResponse.getErrorMsg(), queryTerminalResponse.getErrorCode(), "查询Terminal");
} else {
onTrackListenerNull();
}
}
}
});
}

/**
* 新建terminal
* @param serviceId
* @param terminalName
* @param trackId
*/
private void addTerminal(final long serviceId, String terminalName, final long trackId) {
client.addTerminal(new AddTerminalRequest(terminalName, serviceId), new SimpleOnTrackListener() {
@Override
public void onCreateTerminalCallback(AddTerminalResponse addTerminalResponse) {
if (addTerminalResponse.isSuccess()) {
long terminalId = addTerminalResponse.getTid();
TrackFactory.this.terminalId = terminalId;
if (trackId != 0) {
trackDoSthListener.onCreateTrackSuccess(serviceId, terminalId, trackId);
} else {
createTrack(serviceId, terminalId);
}
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(addTerminalResponse.getErrorMsg(), addTerminalResponse.getErrorCode(), "新建Terminal");
} else {
onTrackListenerNull();
}
}
}
});
}

/**
* 重写方法
*
* @param serviceId
* @param terminalId
*/
private void createTrack(final long serviceId, final long terminalId) {
this.terminalId = terminalId;
client.addTrack(new AddTrackRequest(serviceId, terminalId), new SimpleOnTrackListener() {
@Override
public void onAddTrackCallback(AddTrackResponse addTrackResponse) {
if (addTrackResponse.isSuccess()) {
trackId = addTrackResponse.getTrid();
if (trackDoSthListener != null) {
trackDoSthListener.onCreateTrackSuccess(serviceId, terminalId, trackId);
}else {
onTrackListenerNull();
}
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(addTrackResponse.getErrorMsg(), addTrackResponse.getErrorCode(), "新建Track");
}else {
onTrackListenerNull();
}
}
}
});
}

如果收集完毕,轨迹停止顺序为下:

  1. 调用官方api,client.stopGather(),停止收集点位。
  2. 得到停止收集点位成功的回调里,调用client.stopTrack(),停止定位服务。

值得注意的是,开始的时候是先启动track,在启动gather。而停止的时候正好是反着的,先停止gather,在停止track,不要搞反。

三个Id分别为:serviceId,terminalId,trackId,是核心的东西,需要传给后台服务器,供以后查询使用。
猎鹰生命周期如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 启动猎鹰生命周期回调
*/
private void newListener() {
listener = new OnTrackLifecycleListener() {
@Override
public void onBindServiceCallback(int i, String s) {

}

@Override
public void onStartGatherCallback(int i, String s) {
if (i == ErrorCode.TrackListen.START_GATHER_SUCEE ||
i == ErrorCode.TrackListen.START_GATHER_ALREADY_STARTED) {
if (trackDoSthListener != null) {
trackDoSthListener.onStartSuccess(serviceId, terminalId, trackId);
} else {
onTrackListenerNull();
}
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(s, 0, "定位开始收集失败");
} else {
onTrackListenerNull();
}
}
}

@Override
public void onStartTrackCallback(int i, String s) {
if (i == ErrorCode.TrackListen.START_TRACK_SUCEE ||
i == ErrorCode.TrackListen.START_TRACK_SUCEE_NO_NETWORK ||
i == ErrorCode.TrackListen.START_TRACK_ALREADY_STARTED) {
// 服务启动成功,继续开启收集上报
client.startGather(this);
//ToastUtil.getInstance()._short(context, "开始路程成功");

} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(s, 0, "Track服务启动失败");
} else {
onTrackListenerNull();
}

}
}

@Override
public void onStopGatherCallback(int i, String s) {
if (i == ErrorCode.TrackListen.STOP_GATHER_SUCCE) {
TrackParam tp = new TrackParam(serviceId, terminalId);
client.stopTrack(tp, listener);
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(s, i, "定位停止采集失败");
} else {
onTrackListenerNull();
}
}
}

@Override
public void onStopTrackCallback(int i, String s) {
if (i == ErrorCode.TrackListen.STOP_TRACK_SUCCE) {
// 成功停止
if (trackDoSthListener != null) {
trackDoSthListener.onStopSuccess();
} else {
onTrackListenerNull();
}
} else {
if (trackDoSthListener != null) {
trackDoSthListener.onDoingFailed(s, i, "路程停止失败");
} else {
onTrackListenerNull();
}
}
}
};
}

上面大概就是猎鹰功能的核心代码,很多都没写全,本想着开一篇就够了,但是感觉篇幅太长,就算了。

前台服务

猎鹰功能本身是自己带着service的,但是如果一直处在后台状态,会被杀死,尤其是Android 8.0以后,限制住了很多权限,其中就有后台定位功能。
猎鹰推荐8.0系统启动一个 ForcegroundService,新出的前台进程,也可以给他们的api传一个notification,让他们自己启动前台进程,但是有一个问题,就是他们的接口已经过时了,不能用了,于是我研究了下,就自己写了一个前台Service来确保猎鹰可以正常取点。
我在其中做了个media Player来确保尽可能不被后台杀死。
在单例里面用四大组件的上下文一定要特别注意,最好传getApplicationContext(),因为Application的上下文是能贯穿整个app的,我一开始因为失误,再单例里面传了Service的上下文,导致service一直无法被回收,导致内存泄漏。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
* ************************************************************
* 文件:CarManageService.java 模块:app 项目:oemp-android
* 当前修改时间:2019年12月25日 09:57:10
* 上次修改时间:2019年12月25日 09:57:10
* 作者:QingHeyang
* QingHeyang版权所有
* ************************************************************
*/

package com.qhy.trackdemo.service;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;


import com.qhy.trackdemo.MainActivity;
import com.qhy.trackdemo.R;

import org.greenrobot.eventbus.EventBus;

public class TrackDemoService extends Service {


private static final String CHANNEL_ID_SERVICE_RUNNING = "CHANNEL_ID_SERVICE_RUNNING";

private long serviceId;
private long terminalId, trackId;
private String terminalName;
private Notification notification;
private MediaPlayer bgmediaPlayer;
private Intent intent;

@Override
public void onCreate() {
super.onCreate();
Notification notification = registerNotification();
this.notification = notification;
// BaseApplication.getRefWatcher().watch(this);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && this.intent == null) {
this.intent = intent;
startGather(intent);
}
return START_REDELIVER_INTENT;
}

/**
* 注册用于8.0以后的手机
*
* @return
*/
private Notification registerNotification() {
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID_SERVICE_RUNNING
, "猎鹰服务"
, NotificationManager.IMPORTANCE_LOW);
nm.createNotificationChannel(channel);
builder = new Notification.Builder(this, CHANNEL_ID_SERVICE_RUNNING);
} else {
builder = new Notification.Builder(this);
}
Intent nfIntent = new Intent(this, MainActivity.class);//此处的Activity为前台进程的Notification点击回调页面
nfIntent.putExtra("fromService", true);
Bitmap bitmap = ((BitmapDrawable) getResources().getDrawable(R.mipmap.ic_launcher)).getBitmap();//此处icon需要根据自己需求来
builder.setContentIntent(PendingIntent.getActivity(this,
0,
nfIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(bitmap)
.setContentTitle("您的有未完成的行程进行中...")
.setContentText("点击回到行程");
Notification notification = builder.build();
return notification;
}

/**
* 开始收集
*
* @param intent
*/
public void startGather(Intent intent) {
terminalName = intent.getStringExtra("terminalName");
trackId = intent.getLongExtra("trackId", 0);
serviceId = intent.getLongExtra("serviceId", 0);

/*Notification notification = registerNotification();
startForeground((int) System.currentTimeMillis(),notification);*/

TrackFactory.getInstance(getApplicationContext()).setOnTrackDoSthListener(new TrackFactory.OnTrackDoSthListener() {
@Override
public void onCreateTrackSuccess(long serviceId, long terminalId, long trackId) {
TrackDemoService.this.terminalId = terminalId;
TrackDemoService.this.trackId = trackId;
TrackFactory.getInstance(TrackDemoService.this.getApplicationContext()).startGather(serviceId, terminalId, trackId);
}

@Override
public void onStartSuccess(long serviceId, long terminalId, long trackId) {
TrackInfo info = new TrackInfo();
info.setTrackId(trackId);
info.setTerminalId(terminalId);
info.setCanUse(true);
EventBus.getDefault().post(info);
startForeground((int) System.currentTimeMillis(), notification);
}

@Override
public void onStopSuccess() {
}

@Override
public void onDoingFailed(String msg, long code, String from) {
TrackInfo info = new TrackInfo();
info.setMessage(msg + "\n" + from);
info.setCanUse(false);
EventBus.getDefault().post(info);
}
});
TrackFactory.getInstance(getApplicationContext()).createTrack(serviceId, terminalName, trackId);
playMusic();
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* 此处播放无声音乐,用来不被系统杀死
*/
private void playMusic() {
if (bgmediaPlayer == null) {
bgmediaPlayer = MediaPlayer.create(this, 0);
bgmediaPlayer.start();
bgmediaPlayer.setLooping(true);
bgmediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
bgmediaPlayer.start();
bgmediaPlayer.setLooping(true);
}
});
}
}

@Override
public void onDestroy() {
if (bgmediaPlayer != null) {
bgmediaPlayer.stop();
bgmediaPlayer = null;
}
TrackFactory.getInstance(getApplicationContext()).stopGather(serviceId, terminalId);
TrackDemoService.this.stopForeground(true);
super.onDestroy();
}

}

电池白名单

还有一个问题就是,在8.0以后,存在了电池白名单的问题,如果不申请,后台会被限制活动,这里给出方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 判断用户是否在电池白名单
*
* @param context
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public static boolean inWhiteList(Context context) {
boolean isIgnoring = false;
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powerManager != null) {
isIgnoring = powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
}
return isIgnoring;
}

/**
* 请求电池白名单
* @param context
* @return
*/
public static Intent doRequestWhiteList(Context context) {
Intent intent = new Intent();
try {
String packageName = context.getPackageName();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
} catch (Exception e) {
e.printStackTrace();
}
return intent;
}

启动服务

回头再写…

请我喝杯咖啡吧~

支付宝
微信