@josuelmm/cordova-background-geolocation 3.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +4 -0
- package/CHANGELOG.md +313 -0
- package/CLAUDE.md +56 -0
- package/HISTORY.md +124 -0
- package/README.md +198 -6
- package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +90 -0
- package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +362 -1
- package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +153 -0
- package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +27 -11
- package/android/common/src/main/java/com/marianhello/bgloc/Config.java +268 -0
- package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +86 -26
- package/android/common/src/main/java/com/marianhello/bgloc/PluginDelegate.java +26 -0
- package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +48 -5
- package/android/common/src/main/java/com/marianhello/bgloc/data/SessionLocationDAO.java +18 -0
- package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +8 -1
- package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java +74 -0
- package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java +169 -0
- package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
- package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
- package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
- package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
- package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
- package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
- package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +310 -7
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
- package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
- package/android/dependencies.gradle +0 -3
- package/angular/background-geolocation-events.ts +21 -0
- package/angular/background-geolocation.service.ts +91 -0
- package/angular/dist/background-geolocation-events.d.ts +18 -1
- package/angular/dist/background-geolocation.service.d.ts +40 -0
- package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
- package/angular/dist/esm2022/background-geolocation.service.mjs +47 -1
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +67 -0
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
- package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.h +4 -0
- package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +352 -1
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +26 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +421 -15
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
- package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
- package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
- package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
- package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +12 -3
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +102 -44
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.h +29 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.m +31 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.h +25 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.m +153 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
- package/package.json +36 -1
- package/plugin.xml +26 -8
- package/www/BackgroundGeolocation.d.ts +559 -3
- package/www/BackgroundGeolocation.js +78 -1
- package/RELEASE.MD +0 -16
package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java
CHANGED
|
@@ -8,6 +8,7 @@ import android.util.Log;
|
|
|
8
8
|
|
|
9
9
|
import com.marianhello.bgloc.data.sqlite.SQLiteLocationContract.LocationEntry;
|
|
10
10
|
import com.marianhello.bgloc.data.sqlite.SQLiteConfigurationContract.ConfigurationEntry;
|
|
11
|
+
import com.marianhello.bgloc.data.sqlite.SQLiteSessionContract.SessionEntry;
|
|
11
12
|
|
|
12
13
|
import java.util.ArrayList;
|
|
13
14
|
|
|
@@ -21,7 +22,7 @@ import static com.marianhello.bgloc.data.sqlite.SQLiteLocationContract.LocationE
|
|
|
21
22
|
public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
|
|
22
23
|
private static final String TAG = SQLiteOpenHelper.class.getName();
|
|
23
24
|
public static final String SQLITE_DATABASE_NAME = "cordova_bg_geolocation.db";
|
|
24
|
-
public static final int DATABASE_VERSION =
|
|
25
|
+
public static final int DATABASE_VERSION = 20;
|
|
25
26
|
|
|
26
27
|
public static final String TEXT_TYPE = " TEXT";
|
|
27
28
|
public static final String INTEGER_TYPE = " INTEGER";
|
|
@@ -66,6 +67,8 @@ public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
|
|
|
66
67
|
execAndLogSql(db, SQL_CREATE_CONFIG_TABLE);
|
|
67
68
|
execAndLogSql(db, SQL_CREATE_LOCATION_TABLE_TIME_IDX);
|
|
68
69
|
execAndLogSql(db, SQL_CREATE_LOCATION_TABLE_BATCH_ID_IDX);
|
|
70
|
+
execAndLogSql(db, SessionEntry.SQL_CREATE_SESSION_TABLE);
|
|
71
|
+
execAndLogSql(db, SessionEntry.SQL_CREATE_SESSION_TABLE_TIME_IDX);
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
@Override
|
|
@@ -137,6 +140,9 @@ public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
|
|
|
137
140
|
" ADD COLUMN " + ConfigurationEntry.COLUMN_NAME_SHOW_TIME + INTEGER_TYPE);
|
|
138
141
|
alterSql.add("ALTER TABLE " + ConfigurationEntry.TABLE_NAME +
|
|
139
142
|
" ADD COLUMN " + ConfigurationEntry.COLUMN_NAME_SHOW_DISTANCE + INTEGER_TYPE);
|
|
143
|
+
case 19:
|
|
144
|
+
alterSql.add(SessionEntry.SQL_CREATE_SESSION_TABLE);
|
|
145
|
+
alterSql.add(SessionEntry.SQL_CREATE_SESSION_TABLE_TIME_IDX);
|
|
140
146
|
|
|
141
147
|
break; // DO NOT FORGET TO MOVE DOWN BREAK ON DB UPGRADE!!!
|
|
142
148
|
default:
|
|
@@ -154,6 +160,7 @@ public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
|
|
|
154
160
|
// we don't support db downgrade yet, instead we drop table and start over
|
|
155
161
|
execAndLogSql(db, SQL_DROP_LOCATION_TABLE);
|
|
156
162
|
execAndLogSql(db, SQL_DROP_CONFIG_TABLE);
|
|
163
|
+
execAndLogSql(db, SessionEntry.SQL_DROP_SESSION_TABLE);
|
|
157
164
|
onCreate(db);
|
|
158
165
|
}
|
|
159
166
|
|
package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package com.marianhello.bgloc.data.sqlite;
|
|
2
|
+
|
|
3
|
+
import android.provider.BaseColumns;
|
|
4
|
+
|
|
5
|
+
import static com.marianhello.bgloc.data.sqlite.SQLiteOpenHelper.COMMA_SEP;
|
|
6
|
+
import static com.marianhello.bgloc.data.sqlite.SQLiteOpenHelper.INTEGER_TYPE;
|
|
7
|
+
import static com.marianhello.bgloc.data.sqlite.SQLiteOpenHelper.REAL_TYPE;
|
|
8
|
+
import static com.marianhello.bgloc.data.sqlite.SQLiteOpenHelper.TEXT_TYPE;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Contract for the "session" location table.
|
|
12
|
+
* Stores all locations for the current recording session (route).
|
|
13
|
+
* Cleared on startSession() and clearSession(); not cleared when sync succeeds.
|
|
14
|
+
*/
|
|
15
|
+
public final class SQLiteSessionContract {
|
|
16
|
+
|
|
17
|
+
public SQLiteSessionContract() {}
|
|
18
|
+
|
|
19
|
+
public static abstract class SessionEntry implements BaseColumns {
|
|
20
|
+
public static final String TABLE_NAME = "location_session";
|
|
21
|
+
public static final String COLUMN_NAME_NULLABLE = "NULLHACK";
|
|
22
|
+
public static final String COLUMN_NAME_TIME = "time";
|
|
23
|
+
public static final String COLUMN_NAME_ACCURACY = "accuracy";
|
|
24
|
+
public static final String COLUMN_NAME_VERTICAL_ACCURACY = "vertical_accuracy";
|
|
25
|
+
public static final String COLUMN_NAME_SPEED = "speed";
|
|
26
|
+
public static final String COLUMN_NAME_BEARING = "bearing";
|
|
27
|
+
public static final String COLUMN_NAME_ALTITUDE = "altitude";
|
|
28
|
+
public static final String COLUMN_NAME_LATITUDE = "latitude";
|
|
29
|
+
public static final String COLUMN_NAME_LONGITUDE = "longitude";
|
|
30
|
+
public static final String COLUMN_NAME_RADIUS = "radius";
|
|
31
|
+
public static final String COLUMN_NAME_HAS_ACCURACY = "has_accuracy";
|
|
32
|
+
public static final String COLUMN_NAME_HAS_VERTICAL_ACCURACY = "has_vertical_accuracy";
|
|
33
|
+
public static final String COLUMN_NAME_HAS_SPEED = "has_speed";
|
|
34
|
+
public static final String COLUMN_NAME_HAS_BEARING = "has_bearing";
|
|
35
|
+
public static final String COLUMN_NAME_HAS_ALTITUDE = "has_altitude";
|
|
36
|
+
public static final String COLUMN_NAME_HAS_RADIUS = "has_radius";
|
|
37
|
+
public static final String COLUMN_NAME_PROVIDER = "provider";
|
|
38
|
+
public static final String COLUMN_NAME_LOCATION_PROVIDER = "service_provider";
|
|
39
|
+
public static final String COLUMN_NAME_STATUS = "valid";
|
|
40
|
+
public static final String COLUMN_NAME_BATCH_START_MILLIS = "batch_start";
|
|
41
|
+
public static final String COLUMN_NAME_MOCK_FLAGS = "mock_flags";
|
|
42
|
+
|
|
43
|
+
public static final String SQL_CREATE_SESSION_TABLE =
|
|
44
|
+
"CREATE TABLE " + SessionEntry.TABLE_NAME + " (" +
|
|
45
|
+
SessionEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
|
46
|
+
SessionEntry.COLUMN_NAME_TIME + INTEGER_TYPE + COMMA_SEP +
|
|
47
|
+
SessionEntry.COLUMN_NAME_ACCURACY + REAL_TYPE + COMMA_SEP +
|
|
48
|
+
SessionEntry.COLUMN_NAME_VERTICAL_ACCURACY + REAL_TYPE + COMMA_SEP +
|
|
49
|
+
SessionEntry.COLUMN_NAME_SPEED + REAL_TYPE + COMMA_SEP +
|
|
50
|
+
SessionEntry.COLUMN_NAME_BEARING + REAL_TYPE + COMMA_SEP +
|
|
51
|
+
SessionEntry.COLUMN_NAME_ALTITUDE + REAL_TYPE + COMMA_SEP +
|
|
52
|
+
SessionEntry.COLUMN_NAME_LATITUDE + REAL_TYPE + COMMA_SEP +
|
|
53
|
+
SessionEntry.COLUMN_NAME_LONGITUDE + REAL_TYPE + COMMA_SEP +
|
|
54
|
+
SessionEntry.COLUMN_NAME_RADIUS + REAL_TYPE + COMMA_SEP +
|
|
55
|
+
SessionEntry.COLUMN_NAME_HAS_ACCURACY + INTEGER_TYPE + COMMA_SEP +
|
|
56
|
+
SessionEntry.COLUMN_NAME_HAS_VERTICAL_ACCURACY + INTEGER_TYPE + COMMA_SEP +
|
|
57
|
+
SessionEntry.COLUMN_NAME_HAS_SPEED + INTEGER_TYPE + COMMA_SEP +
|
|
58
|
+
SessionEntry.COLUMN_NAME_HAS_BEARING + INTEGER_TYPE + COMMA_SEP +
|
|
59
|
+
SessionEntry.COLUMN_NAME_HAS_ALTITUDE + INTEGER_TYPE + COMMA_SEP +
|
|
60
|
+
SessionEntry.COLUMN_NAME_HAS_RADIUS + INTEGER_TYPE + COMMA_SEP +
|
|
61
|
+
SessionEntry.COLUMN_NAME_PROVIDER + TEXT_TYPE + COMMA_SEP +
|
|
62
|
+
SessionEntry.COLUMN_NAME_LOCATION_PROVIDER + INTEGER_TYPE + COMMA_SEP +
|
|
63
|
+
SessionEntry.COLUMN_NAME_STATUS + INTEGER_TYPE + COMMA_SEP +
|
|
64
|
+
SessionEntry.COLUMN_NAME_BATCH_START_MILLIS + INTEGER_TYPE + COMMA_SEP +
|
|
65
|
+
SessionEntry.COLUMN_NAME_MOCK_FLAGS + INTEGER_TYPE +
|
|
66
|
+
" )";
|
|
67
|
+
|
|
68
|
+
public static final String SQL_DROP_SESSION_TABLE =
|
|
69
|
+
"DROP TABLE IF EXISTS " + SessionEntry.TABLE_NAME;
|
|
70
|
+
|
|
71
|
+
public static final String SQL_CREATE_SESSION_TABLE_TIME_IDX =
|
|
72
|
+
"CREATE INDEX session_time_idx ON " + SessionEntry.TABLE_NAME + " (" + SessionEntry.COLUMN_NAME_TIME + ")";
|
|
73
|
+
}
|
|
74
|
+
}
|
package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package com.marianhello.bgloc.data.sqlite;
|
|
2
|
+
|
|
3
|
+
import android.content.ContentValues;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.database.Cursor;
|
|
6
|
+
import android.database.sqlite.SQLiteDatabase;
|
|
7
|
+
import com.marianhello.bgloc.data.BackgroundLocation;
|
|
8
|
+
import com.marianhello.bgloc.data.SessionLocationDAO;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collection;
|
|
12
|
+
|
|
13
|
+
public class SQLiteSessionLocationDAO implements SessionLocationDAO {
|
|
14
|
+
|
|
15
|
+
private static final String PREFS_NAME = "bgloc_session";
|
|
16
|
+
private static final String KEY_SESSION_ACTIVE = "session_active";
|
|
17
|
+
|
|
18
|
+
private final SQLiteDatabase db;
|
|
19
|
+
private final Context context;
|
|
20
|
+
|
|
21
|
+
public SQLiteSessionLocationDAO(Context context) {
|
|
22
|
+
this.context = context.getApplicationContext();
|
|
23
|
+
SQLiteOpenHelper helper = SQLiteOpenHelper.getHelper(this.context);
|
|
24
|
+
this.db = helper.getWritableDatabase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Override
|
|
28
|
+
public void startSession() {
|
|
29
|
+
db.delete(SQLiteSessionContract.SessionEntry.TABLE_NAME, null, null);
|
|
30
|
+
setSessionActive(true);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
public void clearSession() {
|
|
35
|
+
db.delete(SQLiteSessionContract.SessionEntry.TABLE_NAME, null, null);
|
|
36
|
+
setSessionActive(false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Override
|
|
40
|
+
public boolean isSessionActive() {
|
|
41
|
+
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
42
|
+
.getBoolean(KEY_SESSION_ACTIVE, false);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private void setSessionActive(boolean active) {
|
|
46
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
47
|
+
.edit()
|
|
48
|
+
.putBoolean(KEY_SESSION_ACTIVE, active)
|
|
49
|
+
.apply();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Override
|
|
53
|
+
public void persistSessionLocation(BackgroundLocation location) {
|
|
54
|
+
if (!isSessionActive() || location == null) return;
|
|
55
|
+
ContentValues values = getContentValues(location);
|
|
56
|
+
db.insertOrThrow(SQLiteSessionContract.SessionEntry.TABLE_NAME,
|
|
57
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_NULLABLE, values);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Override
|
|
61
|
+
public Collection<BackgroundLocation> getSessionLocations() {
|
|
62
|
+
Collection<BackgroundLocation> locations = new ArrayList<>();
|
|
63
|
+
String orderBy = SQLiteSessionContract.SessionEntry.COLUMN_NAME_TIME + " ASC";
|
|
64
|
+
Cursor cursor = null;
|
|
65
|
+
try {
|
|
66
|
+
cursor = db.query(
|
|
67
|
+
SQLiteSessionContract.SessionEntry.TABLE_NAME,
|
|
68
|
+
queryColumns(),
|
|
69
|
+
null, null, null, null, orderBy);
|
|
70
|
+
while (cursor.moveToNext()) {
|
|
71
|
+
locations.add(hydrate(cursor));
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
if (cursor != null) cursor.close();
|
|
75
|
+
}
|
|
76
|
+
return locations;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Override
|
|
80
|
+
public int getSessionLocationsCount() {
|
|
81
|
+
Cursor cursor = null;
|
|
82
|
+
try {
|
|
83
|
+
cursor = db.rawQuery("SELECT COUNT(*) FROM " + SQLiteSessionContract.SessionEntry.TABLE_NAME, null);
|
|
84
|
+
return cursor.moveToFirst() ? cursor.getInt(0) : 0;
|
|
85
|
+
} finally {
|
|
86
|
+
if (cursor != null) cursor.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private BackgroundLocation hydrate(Cursor c) {
|
|
91
|
+
BackgroundLocation l = new BackgroundLocation(c.getString(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_PROVIDER)));
|
|
92
|
+
l.setTime(c.getLong(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_TIME)));
|
|
93
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ACCURACY)) == 1) {
|
|
94
|
+
l.setAccuracy(c.getFloat(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_ACCURACY)));
|
|
95
|
+
}
|
|
96
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_VERTICAL_ACCURACY)) == 1) {
|
|
97
|
+
l.setVerticalAccuracy(c.getFloat(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_VERTICAL_ACCURACY)));
|
|
98
|
+
}
|
|
99
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_SPEED)) == 1) {
|
|
100
|
+
l.setSpeed(c.getFloat(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_SPEED)));
|
|
101
|
+
}
|
|
102
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_BEARING)) == 1) {
|
|
103
|
+
l.setBearing(c.getFloat(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_BEARING)));
|
|
104
|
+
}
|
|
105
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ALTITUDE)) == 1) {
|
|
106
|
+
l.setAltitude(c.getDouble(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_ALTITUDE)));
|
|
107
|
+
}
|
|
108
|
+
if (c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_RADIUS)) == 1) {
|
|
109
|
+
l.setRadius(c.getFloat(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_RADIUS)));
|
|
110
|
+
}
|
|
111
|
+
l.setLatitude(c.getDouble(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LATITUDE)));
|
|
112
|
+
l.setLongitude(c.getDouble(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LONGITUDE)));
|
|
113
|
+
l.setLocationProvider(c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LOCATION_PROVIDER)));
|
|
114
|
+
l.setLocationId(c.getLong(c.getColumnIndex(SQLiteSessionContract.SessionEntry._ID)));
|
|
115
|
+
l.setMockFlags(c.getInt(c.getColumnIndex(SQLiteSessionContract.SessionEntry.COLUMN_NAME_MOCK_FLAGS)));
|
|
116
|
+
return l;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private ContentValues getContentValues(BackgroundLocation l) {
|
|
120
|
+
ContentValues values = new ContentValues();
|
|
121
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_PROVIDER, l.getProvider());
|
|
122
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_TIME, l.getTime());
|
|
123
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_ACCURACY, l.getAccuracy());
|
|
124
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_VERTICAL_ACCURACY, l.getVerticalAccuracy());
|
|
125
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_SPEED, l.getSpeed());
|
|
126
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_BEARING, l.getBearing());
|
|
127
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_ALTITUDE, l.getAltitude());
|
|
128
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_RADIUS, l.getRadius());
|
|
129
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LATITUDE, l.getLatitude());
|
|
130
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LONGITUDE, l.getLongitude());
|
|
131
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ACCURACY, l.hasAccuracy() ? 1 : 0);
|
|
132
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_VERTICAL_ACCURACY, l.hasVerticalAccuracy() ? 1 : 0);
|
|
133
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_SPEED, l.hasSpeed() ? 1 : 0);
|
|
134
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_BEARING, l.hasBearing() ? 1 : 0);
|
|
135
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ALTITUDE, l.hasAltitude() ? 1 : 0);
|
|
136
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_RADIUS, l.hasRadius() ? 1 : 0);
|
|
137
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_LOCATION_PROVIDER, l.getLocationProvider());
|
|
138
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_STATUS, 0);
|
|
139
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_BATCH_START_MILLIS, 0L);
|
|
140
|
+
values.put(SQLiteSessionContract.SessionEntry.COLUMN_NAME_MOCK_FLAGS, l.getMockFlags());
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private String[] queryColumns() {
|
|
145
|
+
return new String[]{
|
|
146
|
+
SQLiteSessionContract.SessionEntry._ID,
|
|
147
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_PROVIDER,
|
|
148
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_TIME,
|
|
149
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_ACCURACY,
|
|
150
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_VERTICAL_ACCURACY,
|
|
151
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_SPEED,
|
|
152
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_BEARING,
|
|
153
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_ALTITUDE,
|
|
154
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_RADIUS,
|
|
155
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_LATITUDE,
|
|
156
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_LONGITUDE,
|
|
157
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ACCURACY,
|
|
158
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_VERTICAL_ACCURACY,
|
|
159
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_SPEED,
|
|
160
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_BEARING,
|
|
161
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_ALTITUDE,
|
|
162
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_HAS_RADIUS,
|
|
163
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_LOCATION_PROVIDER,
|
|
164
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_STATUS,
|
|
165
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_BATCH_START_MILLIS,
|
|
166
|
+
SQLiteSessionContract.SessionEntry.COLUMN_NAME_MOCK_FLAGS
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
package com.marianhello.bgloc.driving;
|
|
2
|
+
|
|
3
|
+
import com.marianhello.bgloc.data.BackgroundLocation;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* v4.0 Phase 6 — Driver insights state machine (GPS-only).
|
|
7
|
+
*
|
|
8
|
+
* Pure-Java helper, no Android imports. Hosted by {@code LocationServiceImpl}, which
|
|
9
|
+
* feeds it every received location and surfaces emitted events via the plugin
|
|
10
|
+
* {@code MSG_ON_*} broadcast pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Heuristics:
|
|
13
|
+
* moving = speed > minMovingSpeed
|
|
14
|
+
* stopped = !moving for stoppedDuration ms
|
|
15
|
+
* tripStart = stopped → moving with speed >= minTripSpeed sustained for minTripDuration ms
|
|
16
|
+
* tripEnd = moving → stopped (after a tripStart)
|
|
17
|
+
* speeding = first crossing above speedLimit (km/h); rearms on drop below
|
|
18
|
+
*
|
|
19
|
+
* Sensor-fusion events (hardBrake / sharpTurn / possibleCrash) are intentionally NOT
|
|
20
|
+
* implemented in this class. They require linear acceleration + gyroscope sampling and
|
|
21
|
+
* are planned for v4.1 in a separate {@code SensorFusionDetector}.
|
|
22
|
+
*/
|
|
23
|
+
public class DrivingEventsDetector {
|
|
24
|
+
|
|
25
|
+
public interface Listener {
|
|
26
|
+
void onMoving(BackgroundLocation location);
|
|
27
|
+
void onStopped(BackgroundLocation location);
|
|
28
|
+
void onTripStart(BackgroundLocation location);
|
|
29
|
+
/** distance in meters, durationMs in milliseconds. */
|
|
30
|
+
void onTripEnd(BackgroundLocation location, double distance, long durationMs);
|
|
31
|
+
void onSpeeding(BackgroundLocation location, double speedKmh, double limitKmh);
|
|
32
|
+
void onProviderChange(String provider);
|
|
33
|
+
// v4.1 GPS-derived driving events
|
|
34
|
+
/** GPS-derived deceleration (m/s²). Negative number, more negative = harder brake. */
|
|
35
|
+
void onHardBrake(BackgroundLocation location, double decelMps2);
|
|
36
|
+
/** GPS-derived acceleration (m/s²). */
|
|
37
|
+
void onRapidAcceleration(BackgroundLocation location, double accelMps2);
|
|
38
|
+
/** Bearing change rate (deg/s). */
|
|
39
|
+
void onSharpTurn(BackgroundLocation location, double degPerSec);
|
|
40
|
+
/** Velocity drop in km/h within {@code crashWindowMs} while tripActive. */
|
|
41
|
+
void onPossibleCrash(BackgroundLocation location, double velocityDropKmh);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static class Config {
|
|
45
|
+
public boolean enabled = false;
|
|
46
|
+
public double speedLimitKmh = 0; // 0 disables speeding
|
|
47
|
+
public double minMovingSpeedMps = 1.0; // ~3.6 km/h
|
|
48
|
+
public long stoppedDurationMs = 60_000;
|
|
49
|
+
public double minTripSpeedMps = 3.0; // ~10.8 km/h
|
|
50
|
+
public long minTripDurationMs = 30_000;
|
|
51
|
+
// v4.1 GPS-derived driving events. 0 disables each one.
|
|
52
|
+
public double hardBrakeMps2 = 3.5; // -m/s² threshold (positive value)
|
|
53
|
+
public double rapidAccelMps2 = 3.5; // m/s² threshold
|
|
54
|
+
public double sharpTurnDegPerSec = 30; // deg/sec, requires speed > 5 m/s
|
|
55
|
+
public double crashImpactKmh = 25; // velocity drop within crashWindow
|
|
56
|
+
public long crashWindowMs = 2_000; // window to evaluate the velocity drop
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private final Listener listener;
|
|
60
|
+
private Config cfg = new Config();
|
|
61
|
+
|
|
62
|
+
// State
|
|
63
|
+
private boolean isMoving = false;
|
|
64
|
+
private boolean tripActive = false;
|
|
65
|
+
private long tripStartedAt = 0;
|
|
66
|
+
private double tripDistanceMeters = 0;
|
|
67
|
+
private double tripStartLat, tripStartLon;
|
|
68
|
+
private boolean hasTripStartCoord = false;
|
|
69
|
+
|
|
70
|
+
private long aboveTripSpeedSince = 0; // first sample with speed >= minTripSpeed
|
|
71
|
+
private long belowMovingSinceMs = 0; // first sample with speed < minMovingSpeed
|
|
72
|
+
|
|
73
|
+
private boolean wasSpeeding = false;
|
|
74
|
+
private String lastProvider;
|
|
75
|
+
|
|
76
|
+
private double prevLat, prevLon;
|
|
77
|
+
private boolean hasPrev = false;
|
|
78
|
+
|
|
79
|
+
// v4.1 GPS-derived deltas
|
|
80
|
+
private double prevSpeedMps = 0.0;
|
|
81
|
+
private long prevSpeedAt = 0L;
|
|
82
|
+
private double prevBearingDeg = 0.0;
|
|
83
|
+
private boolean hasPrevBearing = false;
|
|
84
|
+
private long prevBearingAt = 0L;
|
|
85
|
+
/** Cooldown so we don't refire the same event on every fix in a sustained brake. */
|
|
86
|
+
private long lastHardBrakeAt = 0L, lastRapidAccelAt = 0L, lastSharpTurnAt = 0L, lastCrashAt = 0L;
|
|
87
|
+
private static final long DRIVING_EVENT_COOLDOWN_MS = 4_000L;
|
|
88
|
+
|
|
89
|
+
public DrivingEventsDetector(Listener listener) {
|
|
90
|
+
this.listener = listener;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public synchronized void setConfig(Config c) {
|
|
94
|
+
if (c != null) this.cfg = c;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reset internal state. Called when service stops. */
|
|
98
|
+
public synchronized void reset() {
|
|
99
|
+
prevSpeedMps = 0.0;
|
|
100
|
+
prevSpeedAt = 0L;
|
|
101
|
+
prevBearingDeg = 0.0;
|
|
102
|
+
hasPrevBearing = false;
|
|
103
|
+
prevBearingAt = 0L;
|
|
104
|
+
lastHardBrakeAt = lastRapidAccelAt = lastSharpTurnAt = lastCrashAt = 0L;
|
|
105
|
+
isMoving = false;
|
|
106
|
+
tripActive = false;
|
|
107
|
+
tripStartedAt = 0;
|
|
108
|
+
tripDistanceMeters = 0;
|
|
109
|
+
hasTripStartCoord = false;
|
|
110
|
+
aboveTripSpeedSince = 0;
|
|
111
|
+
belowMovingSinceMs = 0;
|
|
112
|
+
wasSpeeding = false;
|
|
113
|
+
lastProvider = null;
|
|
114
|
+
hasPrev = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public synchronized void onLocation(BackgroundLocation loc) {
|
|
118
|
+
if (!cfg.enabled || loc == null) return;
|
|
119
|
+
long now = System.currentTimeMillis();
|
|
120
|
+
double speed = loc.hasSpeed() ? loc.getSpeed() : 0.0;
|
|
121
|
+
|
|
122
|
+
// Provider change
|
|
123
|
+
String provider = loc.getProvider();
|
|
124
|
+
if (provider != null && !provider.equals(lastProvider)) {
|
|
125
|
+
lastProvider = provider;
|
|
126
|
+
if (listener != null) listener.onProviderChange(provider);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Distance accumulator (very simple: planar haversine approximation via
|
|
130
|
+
// Location.distanceBetween is not used here to keep this class platform-free;
|
|
131
|
+
// consumer can swap to BackgroundLocation.distanceTo in onTripEnd if desired).
|
|
132
|
+
double curLat = loc.getLatitude();
|
|
133
|
+
double curLon = loc.getLongitude();
|
|
134
|
+
if (hasPrev && tripActive) {
|
|
135
|
+
tripDistanceMeters += haversineMeters(prevLat, prevLon, curLat, curLon);
|
|
136
|
+
}
|
|
137
|
+
prevLat = curLat;
|
|
138
|
+
prevLon = curLon;
|
|
139
|
+
hasPrev = true;
|
|
140
|
+
|
|
141
|
+
// Moving / stopped state
|
|
142
|
+
boolean nowMoving = speed >= cfg.minMovingSpeedMps;
|
|
143
|
+
if (nowMoving) {
|
|
144
|
+
belowMovingSinceMs = 0;
|
|
145
|
+
if (!isMoving) {
|
|
146
|
+
isMoving = true;
|
|
147
|
+
if (listener != null) listener.onMoving(loc);
|
|
148
|
+
}
|
|
149
|
+
// Trip start arming
|
|
150
|
+
if (!tripActive) {
|
|
151
|
+
if (speed >= cfg.minTripSpeedMps) {
|
|
152
|
+
if (aboveTripSpeedSince == 0) aboveTripSpeedSince = now;
|
|
153
|
+
if (now - aboveTripSpeedSince >= cfg.minTripDurationMs) {
|
|
154
|
+
tripActive = true;
|
|
155
|
+
tripStartedAt = now;
|
|
156
|
+
tripDistanceMeters = 0;
|
|
157
|
+
tripStartLat = curLat;
|
|
158
|
+
tripStartLon = curLon;
|
|
159
|
+
hasTripStartCoord = true;
|
|
160
|
+
if (listener != null) listener.onTripStart(loc);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
aboveTripSpeedSince = 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
aboveTripSpeedSince = 0;
|
|
168
|
+
if (belowMovingSinceMs == 0) belowMovingSinceMs = now;
|
|
169
|
+
if (isMoving && (now - belowMovingSinceMs) >= cfg.stoppedDurationMs) {
|
|
170
|
+
isMoving = false;
|
|
171
|
+
if (listener != null) listener.onStopped(loc);
|
|
172
|
+
if (tripActive) {
|
|
173
|
+
long durMs = now - tripStartedAt;
|
|
174
|
+
double dist = tripDistanceMeters;
|
|
175
|
+
tripActive = false;
|
|
176
|
+
if (listener != null) listener.onTripEnd(loc, dist, durMs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Speeding (km/h)
|
|
182
|
+
if (cfg.speedLimitKmh > 0) {
|
|
183
|
+
double kmh = speed * 3.6;
|
|
184
|
+
if (kmh > cfg.speedLimitKmh) {
|
|
185
|
+
if (!wasSpeeding) {
|
|
186
|
+
wasSpeeding = true;
|
|
187
|
+
if (listener != null) listener.onSpeeding(loc, kmh, cfg.speedLimitKmh);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Rearm: emit again on next crossing.
|
|
191
|
+
wasSpeeding = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// v4.1 GPS-derived driving events (only meaningful during an active trip)
|
|
196
|
+
if (tripActive && prevSpeedAt > 0) {
|
|
197
|
+
long dtMs = now - prevSpeedAt;
|
|
198
|
+
if (dtMs > 0 && dtMs <= 5_000) {
|
|
199
|
+
double dt = dtMs / 1000.0;
|
|
200
|
+
double dv = speed - prevSpeedMps; // m/s
|
|
201
|
+
double accel = dv / dt; // m/s²
|
|
202
|
+
|
|
203
|
+
if (cfg.hardBrakeMps2 > 0
|
|
204
|
+
&& accel <= -cfg.hardBrakeMps2
|
|
205
|
+
&& (now - lastHardBrakeAt) >= DRIVING_EVENT_COOLDOWN_MS) {
|
|
206
|
+
lastHardBrakeAt = now;
|
|
207
|
+
if (listener != null) listener.onHardBrake(loc, accel);
|
|
208
|
+
}
|
|
209
|
+
if (cfg.rapidAccelMps2 > 0
|
|
210
|
+
&& accel >= cfg.rapidAccelMps2
|
|
211
|
+
&& (now - lastRapidAccelAt) >= DRIVING_EVENT_COOLDOWN_MS) {
|
|
212
|
+
lastRapidAccelAt = now;
|
|
213
|
+
if (listener != null) listener.onRapidAcceleration(loc, accel);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Possible crash: sustained velocity drop greater than crashImpactKmh in <= crashWindow.
|
|
217
|
+
if (cfg.crashImpactKmh > 0 && dtMs <= cfg.crashWindowMs) {
|
|
218
|
+
double dropKmh = (prevSpeedMps - speed) * 3.6; // positive when slowing down
|
|
219
|
+
if (dropKmh >= cfg.crashImpactKmh
|
|
220
|
+
&& speed < 1.5 // ended near stop
|
|
221
|
+
&& prevSpeedMps * 3.6 >= cfg.crashImpactKmh
|
|
222
|
+
&& (now - lastCrashAt) >= DRIVING_EVENT_COOLDOWN_MS) {
|
|
223
|
+
lastCrashAt = now;
|
|
224
|
+
if (listener != null) listener.onPossibleCrash(loc, dropKmh);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Sharp turn (bearing change rate) — requires meaningful speed to avoid GPS jitter.
|
|
231
|
+
if (cfg.sharpTurnDegPerSec > 0 && loc.hasBearing() && speed >= 5.0 && hasPrevBearing) {
|
|
232
|
+
long dtMs = now - prevBearingAt;
|
|
233
|
+
if (dtMs > 0 && dtMs <= 5_000) {
|
|
234
|
+
double bearing = loc.getBearing();
|
|
235
|
+
double diff = Math.abs(bearing - prevBearingDeg);
|
|
236
|
+
if (diff > 180) diff = 360 - diff;
|
|
237
|
+
double rate = diff * 1000.0 / dtMs;
|
|
238
|
+
if (rate >= cfg.sharpTurnDegPerSec
|
|
239
|
+
&& (now - lastSharpTurnAt) >= DRIVING_EVENT_COOLDOWN_MS) {
|
|
240
|
+
lastSharpTurnAt = now;
|
|
241
|
+
if (listener != null) listener.onSharpTurn(loc, rate);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
prevBearingDeg = loc.getBearing();
|
|
245
|
+
prevBearingAt = now;
|
|
246
|
+
} else if (loc.hasBearing()) {
|
|
247
|
+
prevBearingDeg = loc.getBearing();
|
|
248
|
+
prevBearingAt = now;
|
|
249
|
+
hasPrevBearing = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
prevSpeedMps = speed;
|
|
253
|
+
prevSpeedAt = now;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private static double haversineMeters(double lat1, double lon1, double lat2, double lon2) {
|
|
257
|
+
double R = 6371000.0;
|
|
258
|
+
double dLat = Math.toRadians(lat2 - lat1);
|
|
259
|
+
double dLon = Math.toRadians(lon2 - lon1);
|
|
260
|
+
double a = Math.sin(dLat/2) * Math.sin(dLat/2)
|
|
261
|
+
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
|
262
|
+
* Math.sin(dLon/2) * Math.sin(dLon/2);
|
|
263
|
+
return 2 * R * Math.asin(Math.sqrt(a));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.marianhello.bgloc.http;
|
|
2
|
+
|
|
3
|
+
import com.marianhello.bgloc.data.BackgroundLocation;
|
|
4
|
+
|
|
5
|
+
import java.text.SimpleDateFormat;
|
|
6
|
+
import java.util.Date;
|
|
7
|
+
import java.util.HashMap;
|
|
8
|
+
import java.util.Locale;
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
import java.util.TimeZone;
|
|
11
|
+
import java.util.regex.Matcher;
|
|
12
|
+
import java.util.regex.Pattern;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves placeholders like {lat}, {lon}, {timestamp_iso}, {device_id}, ...
|
|
16
|
+
* in a URL template using a single BackgroundLocation and an optional queryParams map.
|
|
17
|
+
*
|
|
18
|
+
* Placeholders not found in the location/queryParams are left as-is so that
|
|
19
|
+
* partial templates (e.g. only static keys for batch mode) keep working.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* String url = UrlTemplateResolver.resolve(template, location, queryParams);
|
|
23
|
+
* // location may be null for batch mode (only queryParams keys are resolved).
|
|
24
|
+
*/
|
|
25
|
+
public final class UrlTemplateResolver {
|
|
26
|
+
|
|
27
|
+
private static final Pattern PLACEHOLDER = Pattern.compile("\\{([a-zA-Z0-9_]+)\\}");
|
|
28
|
+
|
|
29
|
+
private UrlTemplateResolver() { /* no instances */ }
|
|
30
|
+
|
|
31
|
+
public static String resolve(String template, BackgroundLocation location, Map<String, ?> queryParams) {
|
|
32
|
+
if (template == null || template.isEmpty()) return template;
|
|
33
|
+
Map<String, String> ctx = buildContext(location, queryParams);
|
|
34
|
+
Matcher m = PLACEHOLDER.matcher(template);
|
|
35
|
+
StringBuffer sb = new StringBuffer(template.length());
|
|
36
|
+
while (m.find()) {
|
|
37
|
+
String key = m.group(1);
|
|
38
|
+
String value = ctx.get(key);
|
|
39
|
+
if (value != null) {
|
|
40
|
+
m.appendReplacement(sb, Matcher.quoteReplacement(urlEncode(value)));
|
|
41
|
+
} else {
|
|
42
|
+
// Leave placeholder as-is if no value available.
|
|
43
|
+
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(0)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
m.appendTail(sb);
|
|
47
|
+
return sb.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public static boolean hasPlaceholders(String template) {
|
|
51
|
+
if (template == null) return false;
|
|
52
|
+
return PLACEHOLDER.matcher(template).find();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static Map<String, String> buildContext(BackgroundLocation loc, Map<String, ?> queryParams) {
|
|
56
|
+
Map<String, String> ctx = new HashMap<String, String>();
|
|
57
|
+
|
|
58
|
+
// queryParams first so location-derived values can override if user wants
|
|
59
|
+
if (queryParams != null) {
|
|
60
|
+
for (Map.Entry<String, ?> e : queryParams.entrySet()) {
|
|
61
|
+
if (e.getValue() != null) {
|
|
62
|
+
ctx.put(e.getKey(), String.valueOf(e.getValue()));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (loc != null) {
|
|
68
|
+
ctx.put("latitude", String.valueOf(loc.getLatitude()));
|
|
69
|
+
ctx.put("longitude", String.valueOf(loc.getLongitude()));
|
|
70
|
+
ctx.put("lat", String.valueOf(loc.getLatitude()));
|
|
71
|
+
ctx.put("lon", String.valueOf(loc.getLongitude()));
|
|
72
|
+
|
|
73
|
+
long timeMs = loc.getTime();
|
|
74
|
+
ctx.put("time", String.valueOf(timeMs));
|
|
75
|
+
ctx.put("timestamp", String.valueOf(timeMs));
|
|
76
|
+
ctx.put("timestamp_iso", isoUtc(timeMs));
|
|
77
|
+
|
|
78
|
+
if (loc.hasSpeed()) ctx.put("speed", String.valueOf(loc.getSpeed()));
|
|
79
|
+
if (loc.hasAltitude()) ctx.put("altitude", String.valueOf(loc.getAltitude()));
|
|
80
|
+
if (loc.hasBearing()) ctx.put("bearing", String.valueOf(loc.getBearing()));
|
|
81
|
+
if (loc.hasAccuracy()) ctx.put("accuracy", String.valueOf(loc.getAccuracy()));
|
|
82
|
+
|
|
83
|
+
if (loc.getProvider() != null) ctx.put("provider", loc.getProvider());
|
|
84
|
+
// is_moving derived from speed when available (>0.5 m/s ~ walking pace).
|
|
85
|
+
if (loc.hasSpeed()) {
|
|
86
|
+
ctx.put("is_moving", loc.getSpeed() > 0.5f ? "true" : "false");
|
|
87
|
+
}
|
|
88
|
+
// {activity} is not produced by BackgroundLocation by default; the user can supply
|
|
89
|
+
// a value via queryParams (already populated above).
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return ctx;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* URL-encode a placeholder value for use in URL paths and query strings.
|
|
97
|
+
* Spaces become %20, not + (which is form-encoding).
|
|
98
|
+
*/
|
|
99
|
+
private static String urlEncode(String s) {
|
|
100
|
+
if (s == null) return "";
|
|
101
|
+
try {
|
|
102
|
+
String enc = java.net.URLEncoder.encode(s, "UTF-8");
|
|
103
|
+
// URLEncoder uses application/x-www-form-urlencoded; convert "+" back to "%20" for URL safety.
|
|
104
|
+
return enc.replace("+", "%20");
|
|
105
|
+
} catch (Exception e) {
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static String isoUtc(long ms) {
|
|
111
|
+
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
|
112
|
+
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
113
|
+
return sdf.format(new Date(ms));
|
|
114
|
+
}
|
|
115
|
+
}
|