1. Continuously locate collection points
Background continuous positioning mainly refers to the example given by Autonavi’s official website. There are mainly the following points:
1. Locate the LocationService and create a guard Service, LocationHelperService. If the Service fails, the guard invokes the LocationService.
package com.yxc.barchart.map.location.service;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import com.yxc.barchart.map.location.util.Utils;
public class LocationHelperService extends Service {
private Utils.CloseServiceReceiver mCloseReceiver;
@Override
public void onCreate(a) {
super.onCreate();
startBind();
mCloseReceiver = new Utils.CloseServiceReceiver(this);
registerReceiver(mCloseReceiver, Utils.getCloseServiceFilter());
}
@Override
public void onDestroy(a) {
if(mInnerConnection ! =null) {
unbindService(mInnerConnection);
mInnerConnection = null;
}
if(mCloseReceiver ! =null) {
unregisterReceiver(mCloseReceiver);
mCloseReceiver = null;
}
super.onDestroy();
}
private ServiceConnection mInnerConnection;
private void startBind(a) {
final String locationServiceName = "com.yxc.barchart.map.location.service.LocationService";
mInnerConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Intent intent = new Intent();
intent.setAction(locationServiceName);
startService(Utils.getExplicitIntent(getApplicationContext(), intent));
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ILocationServiceAIDL l = ILocationServiceAIDL.Stub.asInterface(service);
try {
l.onFinishBind();
} catch(RemoteException e) { e.printStackTrace(); }}}; Intent intent =new Intent();
intent.setAction(locationServiceName);
bindService(Utils.getExplicitIntent(getApplicationContext(), intent), mInnerConnection, Service.BIND_AUTO_CREATE);
}
private HelperBinder mBinder;
private class HelperBinder extends ILocationHelperServiceAIDL.Stub{
@Override
public void onFinishBind(int notiId) throws RemoteException {
startForeground(notiId, Utils.buildNotification(LocationHelperService.this.getApplicationContext()));
stopForeground(true); }}@Nullable
@Override
public IBinder onBind(Intent intent) {
if (mBinder == null) {
mBinder = new HelperBinder();
}
returnmBinder; }}Copy the code
Since LocationService, LocationHelperService, and the main process are different processes, AIDL is required to communicate between processes.
2. The LocationService needs to trigger the notification to increase the process priority
/** * Trigger to increase the process priority with notification */
protected void applyNotiKeepMech(a) {
startForeground(NOTI_ID, Utils.buildNotification(getBaseContext()));
startBindHelperService();
}
Copy the code
Keep and other sports apps are also in the same situation. If they are not added to the mobile phone with Android8.0 system, the home button is in the background, and the positioning listener always returns the longitude and latitude of the last Location in the cache.
3. The screen turns off the power screen
Check in location services for network outages caused by screen breaks, and if so, try lighting up the screen. At the same time, in order to avoid frequent lighting, the minimum time interval is set (can be modified according to requirements). If the screen is not disconnected, you do not need to light up the screen.
AMapLocationListener locationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
// Send a notification of the result
sendLocationBroadcast(aMapLocation);
// Determine whether to handle the disconnection of the screen
if(! mIsWifiCloseable) {return;
}
// Pass the location result and the device state to the mWifiAutoCloseDelegate
if (aMapLocation.getErrorCode() == AMapLocation.LOCATION_SUCCESS) {
/ /...
} else {
/ /...}}private void sendLocationBroadcast(AMapLocation aMapLocation) {
// Record the message and send a broadcast...}};/** Handle the logic of wifi disconnection after a screen break */
public class WifiAutoCloseDelegate implements IWifiAutoCloseDelegate {
/** * Add it according to the background data. This is only for Xiaomi phones *@param context
* @return* /
@Override
public boolean isUseful(Context context) {
/ /...
}
/** The service can be killed, so when the service is initialized, initialize the related parameter */
@Override
public void initOnServiceStarted(Context context) {
/ /...
}
/** The locating success message is processed */s
@Override
public void onLocateSuccess(Context context, boolean isScreenOn, boolean isMobileable) {
/ /...
}
/** Process information about locating failure. If you need to wake up the screen, try */
@Override
public void onLocateFail(Context context, int errorCode, boolean isScreenOn, boolean isWifiable) {
/ /...}}Copy the code
When the screen is lit, it is limited by the minimum interval
/**
* 唤醒屏幕
*/
public void wakeUpScreen(final Context context) {
try {
acquirePowerLock(context, PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_DIM_WAKE_LOCK);
} catch (Exception e) {
throwe; }}/** * Get WaveLock of PowerManager according to levelAndFlags * use worker thread to get lock so as not to block main thread *@param context
* @param levelAndFlags
*/
private void acquirePowerLock(final Context context, final int levelAndFlags) {
if (context == null) {
throw new NullPointerException("when invoke aquirePowerLock , context is null which is unacceptable");
}
long currentMills = System.currentTimeMillis();
if (currentMills - mLastWakupTime < mMinWakupInterval) {
return;
}
mLastWakupTime = currentMills;
if (mInnerThreadFactory == null) {
mInnerThreadFactory = new InnerThreadFactory();
}
mInnerThreadFactory.newThread(new Runnable() {
@Override
public void run(a) {
if (pm == null) {
pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
}
if(pmLock ! =null) { // release
pmLock.release();
pmLock = null;
}
pmLock = pm.newWakeLock(levelAndFlags, "MyTag");
pmLock.acquire();
pmLock.release();
}
}).start();
}
Copy the code
There are several permissions involved:
<! -- Allows applications to access CellID or WiFi hotspots to get rough location -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Copy the code
6.0 need to handle dynamic permissions by yourself.
2. Store Location collection point
1. Process the points collected by GPS
AMapLocationListener locationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
// sometimes the return point (0,0) needs to be excluded
if (aMapLocation.getLatitude() == 0f || aMapLocation.getLongitude() <= 0.001 f) {
return;
}
If the distance remains unchanged or the change is too small, it is regarded as the same point. Instead of inter the new point, change the endTime and duration of the current point.
double itemDistance = LocationComputeUtil.getDistance(aMapLocation, lastSaveLocation);
if (lastSaveLocation == null && aMapLocation.getLatitude() > 0f) {
// Insert the first point of the record into the database
lastSaveLocation = aMapLocation;
} else if (itemDistance > 1.0 f) {
resetIntervalTimes(0);/ / the new point
lastSaveLocation = aMapLocation;
} else {Update endTime; // Do not store new data.
long timestamp = lastSaveLocation.getTime();
long endTime = System.currentTimeMillis();// Todo needs to consider the difference between the positioning time and the system time.
long duration = endTime - timestamp;
resetIntervalTimes(duration);
}
sendLocationBroadcast(aMapLocation);
// Send a notification of the result. }Copy the code
If the distance remains unchanged or the change is too small, it is regarded as the same point. Instead of inter the new point, change the endTime and duration of the current point.
The first point is stored directly. The data structure that stores the original point:
Timestamp | endTime | duration | Latitude | Longitude | speed | itemDistance | distance | locationStr | recordType | recordId | milePost |
---|---|---|---|---|---|---|---|---|---|---|---|
RecordType: indicates the movement recordType
RecordId: movement recordId (find all points under the same Id and recordType and plot the movement track)
ItemDistance refers to the distance to the following memory point, distance refers to the distance to the starting point, which can be used to mark milestones, milePost refers to mark milestones. EndTime and duration can be used to indicate the duration of the position.
LocationStr: contains important fields such as latitude and longitude of the current point, which can be reversed into AMapLocation.
The resetIntervalTimes(duration) method has different policies depending on the duration: for example, adjust the frequency of Location collection for Location, or stop LocationService at a certain time
//LocationService
private long intervalTime = LocationConstants.DEFAULT_INTERVAL_TIME;
private void resetIntervalTimes(long duration) {
if (duration >= 60 * 60 * 1000) {// Stop your service for 90 minutes, and should also close the daemon
onDestroy();
return;
}
int intervalTimes = LocationComputeUtil.computeIntervalTimes(duration);
intervalTime = intervalTimes * LocationConstants.DEFAULT_INTERVAL_TIME;
mLocationOption.setInterval(intervalTime);
mLocationClient.setLocationOption(mLocationOption);
}
public static int computeIntervalTimes(long duration) {
long timeMin = 60 * 1000;
if (duration > timeMin) {
return 2;
} else if (duration > 4 * timeMin) {
return 3;
} else if (duration > 10 * timeMin) {
return 5;
}
return 1;
}
Copy the code
In this case, it is possible to stopService a duration greater than a certain time value, and then re-invoke GPS positioning by gyroscope, pedometer, and other means.
The LocalLocationService collects DB inserts and updates from remote1 and broadcasts them to LocalLocationService. When the LocalLocationService receives a broadcast, the data is stored:
//LocalLocationService
public void onLocationChanged(AMapLocation aMapLocation) {
if (aMapLocation.getLatitude() == 0f || aMapLocation.getLongitude() <= 0.001 f) {
return;
}
// Calculate the current registration point's distance to a savepoint as itemDistance. When lastSaveLocation is null, itemDistance is 0.
double itemDistance = LocationComputeUtil.getDistance(aMapLocation, lastSaveLocation);
if (lastSaveLocation == null && aMapLocation.getLatitude() > 0f) {
// Insert the first point of the record into the database
Log.d("LocationService"."The first point...");
Toast.makeText(LocalLocationService.this."Service first insert Point", Toast.LENGTH_SHORT).show();
LocationDBHelper.deleteRecordLocationList(recordType, recordId);
String locationStr = LocationComputeUtil.amapLocationToString(aMapLocation);
double distance = 0;
double milePost = 0;
RecordLocation recordLocation = RecordLocation.createLocation(aMapLocation, recordId, recordType, itemDistance, distance, locationStr, milePost);
// Insert the first point directly into the database
LocationDBHelper.insertRecordLocation(recordLocation);
Log.d("LocationService"."first insert recordLocation:" + recordLocation.toString());
sendEventbus(aMapLocation, recordLocation);
// Update Location to lastSaveLocation for next calculation when there is an insert operation.
lastSaveLocation = aMapLocation;
lastRecordLocation = recordLocation;
} else if (itemDistance > 1.0 f) {
Toast.makeText(LocalLocationService.this."save Point:" + aMapLocation.getLatitude(), Toast.LENGTH_SHORT).show();
String locationStr = LocationComputeUtil.amapLocationToString(aMapLocation);
if(lastRecordLocation ! =null) {
// Cumulatively calculate the distance value of the current point based on the last saved Location value.
double distance = lastRecordLocation.distance + itemDistance;
double milePost = 0;
if (distance >= mMilePost){// When it is just above the milestone point, record it as a milestone point, modify the milestone value, and wait for the next milestone point to be recorded.
milePost = mMilePost;
mMilePost += LocationConstants.MILE_POST_ONE_KILOMETRE;
}
RecordLocation recordLocation = RecordLocation.createLocation(aMapLocation, recordId, recordType,itemDistance, distance, locationStr, milePost);
// The time of the current Location is calculated with the endTime of the previous point and itemDistance to obtain the Speed value of the current point.
long time = (aMapLocation.getTime() - lastRecordLocation.endTime)/1000;
float speed = (float) (itemDistance * 1.0 f/time);
recordLocation.speed = speed;
lastRecordLocation = recordLocation;
//insert
LocationDBHelper.insertRecordLocation(recordLocation);
// Modify lastSaveLocation endTime, duration.
// long lastSaveLocationEndTime = aMapLocation.getTime();
//long lastSaveLocationDuration = aMapLocation.getTime() - lastSaveLocation.getTime(); //LocationDBHelper.updateRecordLocation(lastSaveLocation.getTime(),lastSaveLocationEndTime,
//lastSaveLocationDuration);
sendEventbus(aMapLocation, recordLocation);
Log.d("LocationService"."insert recordLocation:" + recordLocation.toString());
}
lastSaveLocation = aMapLocation;
} else {Update endTime; // Do not store new data.
Toast.makeText(LocalLocationService.this."update Point:" + aMapLocation.getLatitude(), Toast.LENGTH_SHORT).show();
long timestamp = lastSaveLocation.getTime();
long endTime = System.currentTimeMillis();// Todo needs to consider the difference between the positioning time and the system time.
long duration = endTime - timestamp;
// Update LastSaveLocation endTime, duration valuesLocationDBHelper.updateRecordLocation(timestamp, endTime, duration); }}Copy the code
LocalLocationService, like LocationService, has three cases:
1. Create RecordLocation for the first time and then insert into the database
- Insert a new point when Location changes; Calculate the new changed Location point ItemDistance, distance, speed, and see if it is a milestone based on the saved lastSaveLocation, and write the milePost field. Refer to the above code comments for detailed operations.
- If the range is small, it is considered that the current point changes endTime and duration.
When new data is inserted, the UI is updated and the RecordLocation is passed through EventBus
private void sendEventbus(AMapLocation aMapLocation, RecordLocation recordLocation) {
// Send eventBus instead
EventBus.getDefault().post(new LocationEvent(aMapLocation, recordLocation));
}
Copy the code
3. Draw RecordPath
When the LocationActivity receives an EventBus, it can use the data from the EventBus or draw points from the DB in real time.
//eventBus receives data from LocalService
@Subscribe
public void onLocationSaved(LocationEvent locationEvent){
if(locationEvent.mapLocation ! =null){ onLocationChanged(locationEvent.mapLocation, locationEvent.recordLocation); }}Copy the code
Here I take points from the database, query TIMESTAMP greaterThan to obtain the LocationList, and then draw the track.
/** * Location result callback **@paramAmapLocation location information class */
public void onLocationChanged(AMapLocation amapLocation, RecordLocation sendRecordLocation) {
if(amapLocation ! =null && amapLocation.getErrorCode() == 0) {
if(lastLocation ! =null) {
long timestamp = lastLocation.getTime();
// Get some from the database
List<RecordLocation> locationList
= LocationDBHelper.getLateLocationList(recordId, timestamp);
record.addPointList(locationList);
for (int i = 0; i < locationList.size(); i++) {
RecordLocation recordLocation = locationList.get(i);
AMapLocation aMapLocation=LocationComputeUtil.parseLocation(recordLocation.locationStr);
LatLng myLocation = new LatLng(aMapLocation.getLatitude(), aMapLocation.getLongitude());
mPolyOptions.add(myLocation);
}
redRawLine();
} else {// Draw the first point directly
LatLng myLocation = new LatLng(amapLocation.getLatitude(), amapLocation.getLongitude());
mAMap.moveCamera(CameraUpdateFactory.changeLatLng(myLocation));
if (btn.isChecked()) {
Log.d("Location"."record " + myLocation);
record.addPoint(sendRecordLocation);
mPolyOptions.add(myLocation);
redRawLine();
}
}
lastLocation = amapLocation;
} else {
String errText = "Location failed," + amapLocation.getErrorCode() + ":"
+ amapLocation.getErrorInfo();
Log.e("AmapErr", errText); }}Copy the code
Plot real time trajectory
/** * real time trajectory drawing line */
private void redRawLine(a) {
if (mPolyOptions.getPoints().size() > 1) {
if(mPolyline ! =null) {
mPolyline.setPoints(mPolyOptions.getPoints());
} else{ mPolyline = mAMap.addPolyline(mPolyOptions); }}}Copy the code
Save the path to the Model Record and store it in the database:
if(! TextUtils.isEmpty(recordId)){// Query the original LocationList by recordType, recordId
List<RecordLocation> locationList = LocationDBHelper.getLocationList(recordType,recordId);
saveRecord(locationList);
}
// Query all points, then generate Record object, insert Record object into DB
protected void saveRecord(List<RecordLocation> list) {
if(list ! =null && list.size() > 0) {
RecordLocation firstLocation = list.get(0);
RecordLocation lastLocation = list.get(list.size() - 1);
double distance = lastLocation.distance;
long duration = getDuration(firstLocation, lastLocation);
String averageSpeed = getAverage(distance, duration);
String pathLineStr = LocationComputeUtil.getPathLineStr(list);
String dateStr = TimeDateUtil.getDateStrMinSecond(firstLocation.getTimestamp());
Record record = Record.createRecord(recordType, Double.toString(distance),Long.toString(duration), averageSpeed, pathLineStr,firstLocation.locationStr,lastLocation.locationStr, dateStr);
LocationDBHelper.insertRecord(record);
} else {
Toast.makeText(LocationActivity.this."No path recorded.", Toast.LENGTH_SHORT).show(); }}private long getDuration(RecordLocation firstLocation, RecordLocation lastLocation) {
return (lastLocation.getEndTime() - firstLocation.getTimestamp()) / 1000;
}
private String getAverage(doubledistancelong duration) {
return String.valueOf(distance/duration);
}
Copy the code
Record is a Model related to the estimated path, which contains all the data of Location points. It is transformed from the LocationList queried by Gson conversion database. There are some problems encountered in the development process: I’m using a Realm database, and the RecordLocationProxy object is being queried from the database, not the RecordLocation object. Therefore, I DeepClone RecordLocationProxy directly to RecordLocation, and then here is a List, processing each field of each object, the efficiency is not very good, I haven’t come up with a good solution for the moment.
The generated Gson object will also need to filter RealmObject fields. This article will not focus on RealmObject database. Write a separate introduction if you can find a solution to the above problems.
public class Record extends RealmObject {
@PrimaryKey
public int id;
public int recordType;
public String distance;
public String duration;
public String speed;// This is averageSpeed
public String pathLine;// Gson string for all points.
@Ignore
private AMapLocation mStartPoint;
@Ignore
private AMapLocation mEndPoint;
public String startPoint;// The important field string for the start point, corresponding to the LocationStr for RecordLocation
public String endPoint;// The important field string for the end point corresponds to the LocationStr for RecordLocation
public String date;// Start point corresponds to dateStr of Timestamp
@Ignore
public List<RecordLocation> mPathLocationList = new ArrayList<>();
public Record(a) {}public static Record createRecord(int recordType, String distance, String duration, String speed, String pathLine, String startPoint,String endPoint, String date) {
Record record = new Record();
record.recordType = recordType;
record.distance = distance;
record.duration = duration;
record.speed = speed;
record.pathLine = pathLine;
record.startPoint = startPoint;
record.endPoint = endPoint;
record.date = date;
returnrecord; }... }Copy the code
RecordLocation The data structure of a single point, corresponding to the data table described above:
public class RecordLocation extends RealmObject {
@PrimaryKey
public long timestamp;/ / timestamp
public long endTime;Endtime-timestamp = duration
public long duration;
public double longitude;/ / precision
public double latitude;/ / dimensions
public float speed;// The speed of a single point is used to underline different colors
public double itemDistance;// The distance from the previous point
public double distance;// The distance from the starting point
public String recordId;// Motion record id(for aggregate query)
public int recordType;// Type of exercise: running, cycling, driving.
public String locationStr;// Contains AMapLocation fields
public double milePost;/ / milestone
public RecordLocation(a) {}public static RecordLocation copyLocation(RecordLocation originalLocation){
RecordLocation recordLocation = new RecordLocation();
recordLocation.timestamp = originalLocation.getTimestamp();
recordLocation.endTime = originalLocation.getEndTime();
recordLocation.duration = originalLocation.getDuration();
recordLocation.latitude = originalLocation.getLatitude();
recordLocation.longitude = originalLocation.getLongitude();
recordLocation.speed = originalLocation.getSpeed();
recordLocation.recordId = originalLocation.getRecordId();
recordLocation.recordType = originalLocation.getRecordType();
recordLocation.itemDistance = originalLocation.getItemDistance();
recordLocation.distance = originalLocation.getDistance();
recordLocation.locationStr = originalLocation.getLocationStr();
recordLocation.milePost=originalLocation.getMilePost();
return recordLocation;
}
public static RecordLocation createLocation(AMapLocation location, String recordId,
int recordType, double itemDistance,
double distance, String locationStr, double milePost){
RecordLocation recordLocation = new RecordLocation();
recordLocation.timestamp = location.getTime();
recordLocation.endTime = recordLocation.timestamp;
recordLocation.duration = 0;
recordLocation.latitude = location.getLatitude();
recordLocation.longitude = location.getLongitude();
recordLocation.speed = location.getSpeed();
recordLocation.recordId = recordId;
recordLocation.recordType = recordType;
recordLocation.itemDistance = itemDistance;
recordLocation.distance = distance;
recordLocation.locationStr = locationStr;
recordLocation.milePost = milePost;
return recordLocation;
}
@Override
public String toString(a) {
return "RecordLocation{" +
"timestamp=" + timestamp +
", endTime=" + endTime +
", duration=" + duration +
", longitude=" + longitude +
", latitude=" + latitude +
", speed=" + speed +
", itemDistance=" + itemDistance +
", distance=" + distance +
", recordId='" + recordId + '\' ' +
", recordType=" + recordType +
", locationStr='" + locationStr + '\' ' +
", milePost=" + milePost +
'} '; }... }Copy the code
conclusion
Data collection, storage, drawing process is not complex, the testing process is cumbersome, need to collect GPS points outside; Debugging up relatively speaking is not very convenient, before the day after Android8.0, the screen can not be continuously caused by the problem; As well as the collection of data, the original data can not be seen at a glance and many other problems that hinder the process have been solved with unremitting efforts. Reaml Browser is used to view database files **. Remal files using a Realm tool. Here I set the Realm of Config storing files directly to the SD card, but there’s a permissions problem is, RealmConfig configuration specifies the SD card read and write is in Application in onCreate, above 6.0 dynamic authority had time to apply for, Again, I’ll leave this to Realm to explore separately.
Finally, attach a map of the dot, friends who need source code can leave a message to contact me, temporarily Github needs to sort out to give out.