From a3b8b424efb0d95d87825793fba02226530586e0 Mon Sep 17 00:00:00 2001
From: cagryinside
Date: Fri, 21 Jun 2019 17:43:46 -0700
Subject: [PATCH] WIP Fix #875 - Implement user-travel-behavior tracking data
collection feature
---
build.gradle | 6 +-
gradle.properties | 2 +-
gradle/wrapper/gradle-wrapper.properties | 4 +-
onebusaway-android/build.gradle | 10 +-
onebusaway-android/google-services.json | 167 +-------
.../src/main/AndroidManifest.xml | 12 +
.../onebusaway/android/app/Application.java | 17 +
.../directions/realtime/RealtimeService.java | 13 +-
.../android/directions/tasks/TripRequest.java | 7 +-
.../android/io/elements/ObaRegion.java | 5 +
.../android/io/elements/ObaRegionElement.java | 12 +-
.../io/request/ObaArrivalInfoResponse.java | 10 +
.../android/io/request/RequestBase.java | 4 +
.../android/provider/ObaContract.java | 8 +-
.../android/provider/ObaProvider.java | 34 +-
.../travelbehavior/TravelBehaviorManager.java | 360 ++++++++++++++++++
.../constants/TravelBehaviorConstants.java | 69 ++++
...ravelBehaviorFileSaverExecutorManager.java | 56 +++
.../ArrivalAndDepartureDataSaverTask.java | 132 +++++++
.../DestinationReminderDataSaverTask.java | 134 +++++++
.../io/task/TripPlanDataSaverTask.java | 125 ++++++
...ArrivalsAndDeparturesDataReaderWorker.java | 101 +++++
.../DestinationReminderReaderWorker.java | 88 +++++
...gisterTravelBehaviorParticipantWorker.java | 103 +++++
.../io/worker/TripPlanDataReaderWorker.java | 100 +++++
.../model/ArrivalAndDepartureInfo.java | 105 +++++
.../model/DestinationReminderInfo.java | 73 ++++
.../model/ObaArrivalInfoPojo.java | 335 ++++++++++++++++
.../model/TravelBehaviorInfo.java | 112 ++++++
.../travelbehavior/model/TripPlanInfo.java | 77 ++++
.../receiver/LocationBroadcastReceiver.java | 45 +++
.../RecognitionBroadcastReceiver.java | 125 ++++++
.../receiver/TransitionBroadcastReceiver.java | 226 +++++++++++
.../utils/TravelBehaviorFirebaseIOUtils.java | 114 ++++++
.../utils/TravelBehaviorUtils.java | 88 +++++
.../android/ui/ArrivalsListFragment.java | 66 ++--
.../android/ui/ArrivalsListLoader.java | 12 +-
.../onebusaway/android/ui/HomeActivity.java | 95 +++--
.../android/ui/PreferencesActivity.java | 27 ++
.../android/ui/TripDetailsListFragment.java | 4 +
.../android/ui/TripPlanActivity.java | 37 +-
.../onebusaway/android/util/RegionUtils.java | 11 +-
.../raw/travel_behavior_informed_consent.html | 135 +++++++
.../src/main/res/values/donottranslate.xml | 4 +
.../src/main/res/values/strings.xml | 23 ++
.../src/main/res/xml/preferences.xml | 5 +
46 files changed, 3019 insertions(+), 279 deletions(-)
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/TravelBehaviorManager.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/constants/TravelBehaviorConstants.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/TravelBehaviorFileSaverExecutorManager.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/ArrivalAndDepartureDataSaverTask.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/DestinationReminderDataSaverTask.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/TripPlanDataSaverTask.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/ArrivalsAndDeparturesDataReaderWorker.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/DestinationReminderReaderWorker.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/RegisterTravelBehaviorParticipantWorker.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/TripPlanDataReaderWorker.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ArrivalAndDepartureInfo.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/DestinationReminderInfo.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ObaArrivalInfoPojo.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TravelBehaviorInfo.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TripPlanInfo.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/LocationBroadcastReceiver.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/RecognitionBroadcastReceiver.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/TransitionBroadcastReceiver.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorFirebaseIOUtils.java
create mode 100644 onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorUtils.java
create mode 100644 onebusaway-android/src/main/res/raw/travel_behavior_informed_consent.html
diff --git a/build.gradle b/build.gradle
index 94e3a8605..08841f759 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,9 +37,9 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.1'
- classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.0'
- classpath 'com.google.gms:google-services:4.1.0'
+ classpath 'com.android.tools.build:gradle:3.4.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.31'
+ classpath 'com.google.gms:google-services:4.2.0'
}
}
diff --git a/gradle.properties b/gradle.properties
index 0b7d878b0..d6c1b3e53 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,4 +15,4 @@
#
android.enableJetifier=true
-android.useAndroidX=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 23e754060..77286c839 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jan 25 09:21:55 EST 2019
+#Fri Jun 21 16:46:28 PDT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/onebusaway-android/build.gradle b/onebusaway-android/build.gradle
index f20668394..59f9137d3 100644
--- a/onebusaway-android/build.gradle
+++ b/onebusaway-android/build.gradle
@@ -45,7 +45,7 @@ android {
dexOptions {
preDexLibraries true
}
- compileSdkVersion 28
+ compileSdkVersion 29
buildToolsVersion '28.0.3'
defaultConfig {
@@ -124,6 +124,7 @@ android {
buildConfigField "String", "FIXED_REGION_PAYMENT_ANDROID_APP_ID", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_TITLE", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_BODY", "null"
+ buildConfigField "boolean", "FIXED_TRAVEL_BEHAVIOR_DATA_COLLECTION", "false"
}
/**
@@ -168,6 +169,7 @@ android {
buildConfigField "String", "FIXED_REGION_PAYMENT_ANDROID_APP_ID", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_TITLE", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_BODY", "null"
+ buildConfigField "boolean", "FIXED_TRAVEL_BEHAVIOR_DATA_COLLECTION", "false"
}
agencyY {
@@ -208,6 +210,7 @@ android {
buildConfigField "String", "FIXED_REGION_PAYMENT_ANDROID_APP_ID", "\"co.bytemark.hart\""
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_TITLE", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_BODY", "null"
+ buildConfigField "boolean", "FIXED_TRAVEL_BEHAVIOR_DATA_COLLECTION", "false"
}
yrt {
// "YRT - York Region Transit - Canada" rebranding - https://play.google.com/store/apps/details?id=can.yrt.onebusaway
@@ -246,6 +249,7 @@ android {
buildConfigField "String", "FIXED_REGION_PAYMENT_ANDROID_APP_ID", "\"co.bytemark.york\""
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_TITLE", "null"
buildConfigField "String", "FIXED_REGION_PAYMENT_WARNING_BODY", "null"
+ buildConfigField "boolean", "FIXED_TRAVEL_BEHAVIOR_DATA_COLLECTION", "false"
}
/**
* Add more rebranding flavors here...
@@ -443,6 +447,10 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.1.0'
// WorkManager (Java only)
implementation 'androidx.work:work-runtime:2.0.0'
+ implementation "androidx.concurrent:concurrent-futures:1.0.0-beta01"
+ implementation "androidx.concurrent:concurrent-listenablefuture:1.0.0-beta01"
+ implementation "androidx.concurrent:concurrent-listenablefuture-callback:1.0.0-beta01"
+
}
apply plugin:'com.google.gms.google-services'
diff --git a/onebusaway-android/google-services.json b/onebusaway-android/google-services.json
index dd6dbccfe..88c4d4d6a 100644
--- a/onebusaway-android/google-services.json
+++ b/onebusaway-android/google-services.json
@@ -1,186 +1,35 @@
{
"project_info": {
- "project_number": "1083736323923",
- "firebase_url": "https://api-project-1083736323923.firebaseio.com",
- "project_id": "api-project-1083736323923",
- "storage_bucket": "api-project-1083736323923.appspot.com"
+ "project_number": "893400419714",
+ "firebase_url": "https://oba-user-behavior.firebaseio.com",
+ "project_id": "oba-user-behavior",
+ "storage_bucket": "oba-user-behavior.appspot.com"
},
"client": [
{
"client_info": {
- "mobilesdk_app_id": "1:1083736323923:android:75ea84a920f3dd26",
- "android_client_info": {
- "package_name": "can.yrt.onebusaway"
- }
- },
- "oauth_client": [
- {
- "client_id": "1083736323923-u9a12phc7p9s7kmlh6aerpdvu6cbjagg.apps.googleusercontent.com",
- "client_type": 1,
- "android_info": {
- "package_name": "can.yrt.onebusaway",
- "certificate_hash": "d8d368095abbdf638318a74cce3f5f3c35ab34b2"
- }
- },
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- }
- ],
- "api_key": [
- {
- "current_key": "AIzaSyAKhirldbMm7knvxq2lFoYiNxSCOesvnwk"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": [
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- },
- {
- "client_id": "1083736323923-a3abfviki4fcbm734vbfceejcivrvbv2.apps.googleusercontent.com",
- "client_type": 2,
- "ios_info": {
- "bundle_id": "org.onebusaway.iphone",
- "app_store_id": "329380089"
- }
- }
- ]
- }
- }
- },
- {
- "client_info": {
- "mobilesdk_app_id": "1:1083736323923:android:e2ba0de76d8a138b",
+ "mobilesdk_app_id": "1:893400419714:android:e2ba0de76d8a138b",
"android_client_info": {
"package_name": "com.joulespersecond.seattlebusbot"
}
},
"oauth_client": [
{
- "client_id": "1083736323923-0u4ns2rkmbar7taqetottco4oe0q36d5.apps.googleusercontent.com",
- "client_type": 1,
- "android_info": {
- "package_name": "com.joulespersecond.seattlebusbot",
- "certificate_hash": "650f90e1ea8abee695865f3abaac9af968a99007"
- }
- },
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
+ "client_id": "893400419714-do2ctm9psfdqt368vk5bsmr6la62sai3.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
- "current_key": "AIzaSyAKhirldbMm7knvxq2lFoYiNxSCOesvnwk"
+ "current_key": "AIzaSyBHl2hYUA8P6jd0DFo6jENr6Ty-Gladl_4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
+ "client_id": "893400419714-do2ctm9psfdqt368vk5bsmr6la62sai3.apps.googleusercontent.com",
"client_type": 3
- },
- {
- "client_id": "1083736323923-a3abfviki4fcbm734vbfceejcivrvbv2.apps.googleusercontent.com",
- "client_type": 2,
- "ios_info": {
- "bundle_id": "org.onebusaway.iphone",
- "app_store_id": "329380089"
- }
- }
- ]
- }
- }
- },
- {
- "client_info": {
- "mobilesdk_app_id": "1:1083736323923:android:0afb35895aeb0afe",
- "android_client_info": {
- "package_name": "org.agencyx.android"
- }
- },
- "oauth_client": [
- {
- "client_id": "1083736323923-1db56fvaq7nt417bofvnf81hq1uc42e3.apps.googleusercontent.com",
- "client_type": 1,
- "android_info": {
- "package_name": "org.agencyx.android",
- "certificate_hash": "650f90e1ea8abee695865f3abaac9af968a99007"
- }
- },
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- }
- ],
- "api_key": [
- {
- "current_key": "AIzaSyAKhirldbMm7knvxq2lFoYiNxSCOesvnwk"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": [
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- },
- {
- "client_id": "1083736323923-a3abfviki4fcbm734vbfceejcivrvbv2.apps.googleusercontent.com",
- "client_type": 2,
- "ios_info": {
- "bundle_id": "org.onebusaway.iphone",
- "app_store_id": "329380089"
- }
- }
- ]
- }
- }
- },
- {
- "client_info": {
- "mobilesdk_app_id": "1:1083736323923:android:5315b0708d7ce060",
- "android_client_info": {
- "package_name": "org.agencyy.android"
- }
- },
- "oauth_client": [
- {
- "client_id": "1083736323923-8e01enil38smqtoc0fisqu57u57e26ie.apps.googleusercontent.com",
- "client_type": 1,
- "android_info": {
- "package_name": "org.agencyy.android",
- "certificate_hash": "650f90e1ea8abee695865f3abaac9af968a99007"
- }
- },
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- }
- ],
- "api_key": [
- {
- "current_key": "AIzaSyAKhirldbMm7knvxq2lFoYiNxSCOesvnwk"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": [
- {
- "client_id": "1083736323923-r9sj5jprq479c089eol4shq6q026tlv7.apps.googleusercontent.com",
- "client_type": 3
- },
- {
- "client_id": "1083736323923-a3abfviki4fcbm734vbfceejcivrvbv2.apps.googleusercontent.com",
- "client_type": 2,
- "ios_info": {
- "bundle_id": "org.onebusaway.iphone",
- "app_store_id": "329380089"
- }
}
]
}
diff --git a/onebusaway-android/src/main/AndroidManifest.xml b/onebusaway-android/src/main/AndroidManifest.xml
index 29a3f83e1..d4abb6027 100644
--- a/onebusaway-android/src/main/AndroidManifest.xml
+++ b/onebusaway-android/src/main/AndroidManifest.xml
@@ -29,6 +29,7 @@
+
@@ -37,6 +38,7 @@
+
+
+
+
+
+
+
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java b/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java
index 835852c76..9bfa154d5 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java
@@ -27,6 +27,7 @@
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
+import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
@@ -46,6 +47,7 @@
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.provider.ObaContract;
import org.onebusaway.android.report.ui.util.SocialReportHandler;
+import org.onebusaway.android.travelbehavior.TravelBehaviorManager;
import org.onebusaway.android.ui.social.SocialAppProfile;
import org.onebusaway.android.ui.social.SocialNavigationDrawerHandler;
import org.onebusaway.android.util.BuildFlavorUtils;
@@ -125,6 +127,8 @@ public void onStart(@NonNull LifecycleOwner owner) {
reportAnalytics();
createNotificationChannels();
+
+ TravelBehaviorManager.startCollectingData(getApplicationContext());
}
/**
@@ -623,4 +627,17 @@ private void createNotificationChannels() {
}
}
+ public static Boolean isIgnoringBatteryOptimizations(Context applicationContext) {
+ PowerManager pm = (PowerManager) applicationContext.getSystemService(Context.POWER_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ pm.isIgnoringBatteryOptimizations(applicationContext.getPackageName())) {
+ return true;
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return null;
+ }
+
+ return false;
+ }
}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/directions/realtime/RealtimeService.java b/onebusaway-android/src/main/java/org/onebusaway/android/directions/realtime/RealtimeService.java
index 3fa4f1179..222dc5cd4 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/directions/realtime/RealtimeService.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/directions/realtime/RealtimeService.java
@@ -23,6 +23,7 @@
import org.onebusaway.android.directions.util.TripRequestBuilder;
import org.opentripplanner.api.model.Itinerary;
import org.opentripplanner.api.model.Leg;
+import org.opentripplanner.api.model.TripPlan;
import android.app.Activity;
import android.app.AlarmManager;
@@ -165,16 +166,16 @@ private void checkForItineraryChange(final Class extends Activity> source, fin
TripRequest.Callback callback = new TripRequest.Callback() {
@Override
- public void onTripRequestComplete(List itineraries, String url) {
- if (itineraries == null || itineraries.isEmpty()) {
+ public void onTripRequestComplete(TripPlan tripPlan, String url) {
+ if (tripPlan == null || tripPlan.itineraries == null || tripPlan.itineraries.isEmpty()) {
onTripRequestFailure(-1, null);
return;
}
// Check each itinerary. Notify user if our *current* itinerary doesn't exist
// or has a lower rank.
- for (int i = 0; i < itineraries.size(); i++) {
- ItineraryDescription other = new ItineraryDescription(itineraries.get(i));
+ for (int i = 0; i < tripPlan.itineraries.size(); i++) {
+ ItineraryDescription other = new ItineraryDescription(tripPlan.itineraries.get(i));
if (itineraryDescription.itineraryMatches(other)) {
@@ -187,7 +188,7 @@ public void onTripRequestComplete(List itineraries, String url) {
(delay > 0) ? R.string.trip_plan_delay
: R.string.trip_plan_early,
R.string.trip_plan_notification_new_plan_text,
- source, builder.getBundle(), itineraries);
+ source, builder.getBundle(), tripPlan.itineraries);
disableListenForTripUpdates();
return;
}
@@ -203,7 +204,7 @@ public void onTripRequestComplete(List itineraries, String url) {
showNotification(itineraryDescription,
R.string.trip_plan_notification_new_plan_title,
R.string.trip_plan_notification_new_plan_text, source,
- builder.getBundle(), itineraries);
+ builder.getBundle(), tripPlan.itineraries);
disableListenForTripUpdates();
}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/directions/tasks/TripRequest.java b/onebusaway-android/src/main/java/org/onebusaway/android/directions/tasks/TripRequest.java
index e6c452b7b..a982ad1ec 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/directions/tasks/TripRequest.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/directions/tasks/TripRequest.java
@@ -18,7 +18,7 @@
import org.onebusaway.android.app.Application;
import org.onebusaway.android.directions.util.JacksonConfig;
-import org.opentripplanner.api.model.Itinerary;
+import org.opentripplanner.api.model.TripPlan;
import org.opentripplanner.api.ws.Message;
import org.opentripplanner.api.ws.Request;
import org.opentripplanner.api.ws.Response;
@@ -35,7 +35,6 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.List;
/**
* AsyncTask that invokes a trip planning request to the OTP Server
@@ -48,7 +47,7 @@
public class TripRequest extends AsyncTask {
public interface Callback {
- void onTripRequestComplete(List itineraries, String url);
+ void onTripRequestComplete(TripPlan tripPlan, String url);
void onTripRequestFailure(int errorCode, String url);
}
@@ -108,7 +107,7 @@ protected void onPostExecute(Long result) {
if (mResponse != null && mResponse.getPlan() != null
&& mResponse.getPlan().getItinerary().get(0) != null) {
- mCallback.onTripRequestComplete(mResponse.getPlan().getItinerary(), mRequestUrl);
+ mCallback.onTripRequestComplete(mResponse.getPlan(), mRequestUrl);
} else {
Log.e(TAG, "Error retrieving routing from OTP server: " + mResponse);
int errorCode = -1;
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegion.java b/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegion.java
index d5b452672..044e0029d 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegion.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegion.java
@@ -153,4 +153,9 @@ public interface Open311Server {
* @return The body text of a warning dialog that should be shown to the user the first time they select the fare payment option, or empty if no warning should be shown to the user.
*/
String getPaymentWarningBody();
+
+ /**
+ * @return true if the region allows travel behavior data collection feature.
+ */
+ boolean isTravelBehaviorDataCollectionEnabled();
}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegionElement.java b/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegionElement.java
index 5242fad43..8e2ca5eb7 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegionElement.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/io/elements/ObaRegionElement.java
@@ -179,6 +179,8 @@ public String toString() {
private final String paymentWarningBody;
+ private final boolean travelBehaviorDataCollectionEnabled;
+
ObaRegionElement() {
id = 0;
regionName = "";
@@ -202,6 +204,7 @@ public String toString() {
paymentAndroidAppId = null;
paymentWarningTitle = null;
paymentWarningBody = null;
+ travelBehaviorDataCollectionEnabled = false;
}
public ObaRegionElement(long id,
@@ -225,7 +228,8 @@ public ObaRegionElement(long id,
boolean supportsEmbeddedSocial,
String paymentAndroidAppId,
String paymentWarningTitle,
- String paymentWarningBody) {
+ String paymentWarningBody,
+ boolean travelBehaviorDataCollectionEnabled) {
this.id = id;
this.regionName = name;
this.active = active;
@@ -248,6 +252,7 @@ public ObaRegionElement(long id,
this.paymentAndroidAppId = paymentAndroidAppId;
this.paymentWarningTitle = paymentWarningTitle;
this.paymentWarningBody = paymentWarningBody;
+ this.travelBehaviorDataCollectionEnabled = travelBehaviorDataCollectionEnabled;
}
@Override
@@ -360,6 +365,11 @@ public String getPaymentWarningBody() {
return paymentWarningBody;
}
+ @Override
+ public boolean isTravelBehaviorDataCollectionEnabled() {
+ return travelBehaviorDataCollectionEnabled;
+ }
+
@Override
public int hashCode() {
final int prime = 31;
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/io/request/ObaArrivalInfoResponse.java b/onebusaway-android/src/main/java/org/onebusaway/android/io/request/ObaArrivalInfoResponse.java
index 629aa3824..eea1cbb6c 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/io/request/ObaArrivalInfoResponse.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/io/request/ObaArrivalInfoResponse.java
@@ -61,6 +61,8 @@ private static final class Data {
private final Data data;
+ private String mUrl;
+
ObaArrivalInfoResponse() {
data = Data.EMPTY_OBJECT;
}
@@ -96,4 +98,12 @@ public List getSituations() {
public ObaReferences getRefs() {
return data.references;
}
+
+ public void setUrl(String url) {
+ mUrl = url;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/io/request/RequestBase.java b/onebusaway-android/src/main/java/org/onebusaway/android/io/request/RequestBase.java
index 819842e47..aeda425bd 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/io/request/RequestBase.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/io/request/RequestBase.java
@@ -53,6 +53,10 @@ protected RequestBase(Uri uri, String postData) {
mPostData = postData;
}
+ public Uri getUri() {
+ return mUri;
+ }
+
public static class BuilderBase {
protected static final String BASE_PATH = "api/where";
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaContract.java b/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaContract.java
index 54a4d8d98..18de28d7c 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaContract.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaContract.java
@@ -459,6 +459,8 @@ protected interface RegionsColumns {
*
*/
public static final String PAYMENT_WARNING_BODY = "payment_warning_body";
+
+ public static final String TRAVEL_BEHAVIOR_DATA_COLLECTION = "travel_behavior_data_collection";
}
protected interface RegionBoundsColumns {
@@ -1342,7 +1344,8 @@ public static ObaRegion get(ContentResolver cr, int id) {
SUPPORTS_EMBEDDED_SOCIAL,
PAYMENT_ANDROID_APP_ID,
PAYMENT_WARNING_TITLE,
- PAYMENT_WARNING_BODY
+ PAYMENT_WARNING_BODY,
+ TRAVEL_BEHAVIOR_DATA_COLLECTION
};
Cursor c = cr.query(buildUri((int) id), PROJECTION, null, null, null);
@@ -1373,7 +1376,8 @@ public static ObaRegion get(ContentResolver cr, int id) {
c.getInt(15) > 0, // Supports Embedded Social
c.getString(16), // Payment Android App ID
c.getString(17), // Payment Warning Title
- c.getString(18) // Payment Warning Body
+ c.getString(18), // Payment Warning Body
+ c.getInt(19) > 0 // travel behavior data collection
);
} finally {
c.close();
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaProvider.java b/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaProvider.java
index 3dd5541ad..0d882dccf 100644
--- a/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaProvider.java
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/provider/ObaProvider.java
@@ -49,7 +49,7 @@ public class ObaProvider extends ContentProvider {
private class OpenHelper extends SQLiteOpenHelper {
- private static final int DATABASE_VERSION = 29;
+ private static final int DATABASE_VERSION = 30;
public OpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -284,19 +284,25 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
++oldVersion;
}
if (oldVersion == 28) {
- db.execSQL(
- "CREATE TABLE " +
- ObaContract.NavStops.PATH + " (" +
- ObaContract.NavStops._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
- + ObaContract.NavStops.NAV_ID + " VARCHAR NOT NULL, "
- + ObaContract.NavStops.START_TIME + " INTEGER NOT NULL, "
- + ObaContract.NavStops.TRIP_ID + " VARCHAR NOT NULL, "
- + ObaContract.NavStops.DESTINATION_ID + " VARCHAR NOT NULL, "
- + ObaContract.NavStops.BEFORE_ID + " VARCHAR NOT NULL, "
- + ObaContract.NavStops.SEQUENCE + " INTEGER NOT NULL, "
- + ObaContract.NavStops.ACTIVE + " INTEGER NOT NULL " +
- ");"
+ db.execSQL(
+ "CREATE TABLE " +
+ ObaContract.NavStops.PATH + " (" +
+ ObaContract.NavStops._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + ObaContract.NavStops.NAV_ID + " VARCHAR NOT NULL, "
+ + ObaContract.NavStops.START_TIME + " INTEGER NOT NULL, "
+ + ObaContract.NavStops.TRIP_ID + " VARCHAR NOT NULL, "
+ + ObaContract.NavStops.DESTINATION_ID + " VARCHAR NOT NULL, "
+ + ObaContract.NavStops.BEFORE_ID + " VARCHAR NOT NULL, "
+ + ObaContract.NavStops.SEQUENCE + " INTEGER NOT NULL, "
+ + ObaContract.NavStops.ACTIVE + " INTEGER NOT NULL " +
+ ");"
);
+ ++oldVersion;
+ }
+
+ if (oldVersion == 29) {
+ db.execSQL("ALTER TABLE " + ObaContract.Regions.PATH +
+ " ADD COLUMN " + ObaContract.Regions.TRAVEL_BEHAVIOR_DATA_COLLECTION + " INTEGER");
}
}
@@ -563,6 +569,8 @@ private void dropTables(SQLiteDatabase db) {
.put(ObaContract.Regions.PAYMENT_WARNING_TITLE, ObaContract.Regions.PAYMENT_WARNING_TITLE);
sRegionsProjectionMap
.put(ObaContract.Regions.PAYMENT_WARNING_BODY, ObaContract.Regions.PAYMENT_WARNING_BODY);
+ sRegionsProjectionMap
+ .put(ObaContract.Regions.TRAVEL_BEHAVIOR_DATA_COLLECTION, ObaContract.Regions.TRAVEL_BEHAVIOR_DATA_COLLECTION);
sRegionBoundsProjectionMap = new HashMap();
sRegionBoundsProjectionMap.put(ObaContract.RegionBounds._ID, ObaContract.RegionBounds._ID);
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/TravelBehaviorManager.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/TravelBehaviorManager.java
new file mode 100644
index 000000000..2ee1b74ce
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/TravelBehaviorManager.java
@@ -0,0 +1,360 @@
+package org.onebusaway.android.travelbehavior;
+
+import com.google.android.gms.location.ActivityRecognition;
+import com.google.android.gms.location.ActivityTransition;
+import com.google.android.gms.location.ActivityTransitionRequest;
+import com.google.android.gms.location.DetectedActivity;
+import com.google.android.gms.tasks.Task;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+import org.onebusaway.android.R;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.io.elements.ObaArrivalInfo;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.io.TravelBehaviorFileSaverExecutorManager;
+import org.onebusaway.android.travelbehavior.io.task.ArrivalAndDepartureDataSaverTask;
+import org.onebusaway.android.travelbehavior.io.task.DestinationReminderDataSaverTask;
+import org.onebusaway.android.travelbehavior.io.task.TripPlanDataSaverTask;
+import org.onebusaway.android.travelbehavior.io.worker.RegisterTravelBehaviorParticipantWorker;
+import org.onebusaway.android.travelbehavior.receiver.TransitionBroadcastReceiver;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorUtils;
+import org.onebusaway.android.util.PermissionUtils;
+import org.onebusaway.android.util.PreferenceUtils;
+import org.opentripplanner.api.model.TripPlan;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.Html;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+public class TravelBehaviorManager {
+
+ private static final String TAG = "TravelBehaviorManager";
+
+ private Context mActivityContext;
+
+ private Context mApplicationContext;
+
+ public TravelBehaviorManager(Context activityContext, Context applicationContext) {
+ mActivityContext = activityContext;
+ mApplicationContext = applicationContext;
+ }
+
+ public void registerTravelBehaviorParticipant() {
+ registerTravelBehaviorParticipant(false);
+ }
+
+ public void registerTravelBehaviorParticipant(boolean forceStart) {
+ boolean isUserOptOut = PreferenceUtils.getBoolean(TravelBehaviorConstants.USER_OPT_OUT,
+ false);
+ if (forceStart) isUserOptOut = false;
+ // If user opt out or Global switch is off then do nothing
+ if (!TravelBehaviorUtils.isTravelBehaviorActiveInRegion() || isUserOptOut) {
+ stopCollectingData();
+ return;
+ }
+
+ boolean isUserOptIn = PreferenceUtils.getBoolean(TravelBehaviorConstants.USER_OPT_IN,
+ false);
+
+ // If the user not opt in yet
+ if (!isUserOptIn) {
+ showParticipationDialog();
+ }
+ }
+
+ private void showParticipationDialog() {
+ new AlertDialog.Builder(mActivityContext)
+ .setMessage(R.string.travel_behavior_opt_in_message)
+ .setTitle(R.string.travel_behavior_opt_in_title)
+ .setIcon(createIcon())
+ .setCancelable(false)
+ .setPositiveButton(R.string.travel_behavior_dialog_yes,
+ (dialog, which) -> {
+ showAgeDialog();
+ })
+ .setNegativeButton(R.string.travel_behavior_dialog_no,
+ (dialog, which) -> {
+ optOutUser();
+ })
+ .create().show();
+ }
+
+ private void showAgeDialog() {
+ new AlertDialog.Builder(mActivityContext)
+ .setMessage(R.string.travel_behavior_age_message)
+ .setTitle(R.string.travel_behavior_opt_in_title)
+ .setIcon(createIcon())
+ .setCancelable(false)
+ .setPositiveButton(R.string.travel_behavior_dialog_yes,
+ (dialog, which) -> {
+ showInformedConsent();
+ dialog.dismiss();
+ })
+ .setNegativeButton(R.string.travel_behavior_dialog_no,
+ (dialog, which) -> {
+ optOutUser();
+ dialog.dismiss();
+ })
+ .create().show();
+ }
+
+ private void showInformedConsent() {
+ String consentHtml = getHtmlConsentDocument();
+ new AlertDialog.Builder(mActivityContext)
+ .setMessage(Html.fromHtml(consentHtml))
+ .setTitle(R.string.travel_behavior_opt_in_title)
+ .setIcon(createIcon())
+ .setCancelable(false)
+ .setPositiveButton(R.string.travel_behavior_dialog_consent_agree,
+ (dialog, which) -> {
+ showEmailDialog();
+ })
+ .setNegativeButton(R.string.travel_behavior_dialog_consent_disagree,
+ (dialog, which) -> {
+ optOutUser();
+ })
+ .create().show();
+ }
+
+ private String getHtmlConsentDocument() {
+ InputStream inputStream = mApplicationContext.getResources().
+ openRawResource(R.raw.travel_behavior_informed_consent);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ byte buf[] = new byte[1024];
+ int len;
+ try {
+ while ((len = inputStream.read(buf)) != -1) {
+ outputStream.write(buf, 0, len);
+ }
+ outputStream.close();
+ inputStream.close();
+ } catch (IOException e) {
+
+ }
+ return outputStream.toString();
+ }
+
+ private void showEmailDialog() {
+ showEmailDialog(null);
+ }
+
+ private void showEmailDialog(String email) {
+ EditText emailEditText = new EditText(mActivityContext);
+ emailEditText.setHint(R.string.travel_behavior_email_hint);
+ emailEditText.setInputType(InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+
+ if (email != null) {
+ emailEditText.setText(email);
+ }
+
+ new AlertDialog.Builder(mActivityContext)
+ .setTitle(R.string.travel_behavior_opt_in_title)
+ .setMessage(R.string.travel_behavior_email_message)
+ .setIcon(createIcon())
+ .setCancelable(false)
+ .setView(emailEditText)
+ .setPositiveButton(R.string.travel_behavior_dialog_email_save,
+ (dialog, which) -> {
+ String currentEmail = emailEditText.getText().toString();
+ if (!TextUtils.isEmpty(currentEmail) &&
+ Patterns.EMAIL_ADDRESS.matcher(currentEmail).matches()) {
+ registerUser(currentEmail);
+ checkPermissions();
+ } else {
+ Toast.makeText(mApplicationContext, R.string.travel_behavior_email_invalid,
+ Toast.LENGTH_LONG).show();
+ // Android automatically dismisses the dialog.
+ // Show the dialog again if the email is invalid
+ showEmailDialog(currentEmail);
+ }
+ })
+ .create().show();
+ }
+
+ private void checkPermissions() {
+ if (!PermissionUtils.hasGrantedPermissions(mApplicationContext, TravelBehaviorConstants.PERMISSIONS)) {
+ Activity homeActivity = (Activity) mActivityContext;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ homeActivity.requestPermissions(TravelBehaviorConstants.PERMISSIONS, 1);
+ }
+ }
+ }
+
+
+ private void registerUser(String email) {
+ Data myData = new Data.Builder()
+ .putString(TravelBehaviorConstants.USER_EMAIL, email)
+ .build();
+
+ Constraints constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build();
+
+ OneTimeWorkRequest workRequest = new OneTimeWorkRequest.
+ Builder(RegisterTravelBehaviorParticipantWorker.class)
+ .setInputData(myData)
+ .setConstraints(constraints)
+ .build();
+
+ WorkManager workManager = WorkManager.getInstance();
+ workManager.enqueue(workRequest);
+ ListenableFuture listenableFuture = workManager.
+ getWorkInfoById(workRequest.getId());
+ Futures.addCallback(listenableFuture, new FutureCallback() {
+ @Override
+ public void onSuccess(@NullableDecl WorkInfo result) {
+ startCollectingData();
+ Activity activity = (Activity) mActivityContext;
+ activity.runOnUiThread(() -> Toast.makeText(mApplicationContext, R.string.travel_behavior_enroll_success,
+ Toast.LENGTH_LONG).show());
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Activity activity = (Activity) mActivityContext;
+ activity.runOnUiThread(() -> Toast.makeText(mApplicationContext, R.string.travel_behavior_enroll_fail,
+ Toast.LENGTH_LONG).show());
+ }
+ }, TravelBehaviorFileSaverExecutorManager.getInstance().getThreadPoolExecutor());
+ }
+
+ public static void startCollectingData(Context applicationContext) {
+ if(TravelBehaviorUtils.isTravelBehaviorDataCollectionActive()){
+ new TravelBehaviorManager(null, applicationContext).startCollectingData();
+ }
+ }
+
+ private void startCollectingData() {
+ int[] activities = {DetectedActivity.IN_VEHICLE,
+ DetectedActivity.ON_BICYCLE, DetectedActivity.WALKING,
+ DetectedActivity.STILL, DetectedActivity.RUNNING};
+
+ List transitions = new ArrayList<>();
+
+ for (int activity : activities) {
+ transitions.add(new ActivityTransition.Builder()
+ .setActivityType(activity)
+ .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
+ .build());
+
+ transitions.add(new ActivityTransition.Builder()
+ .setActivityType(activity)
+ .setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
+ .build());
+ }
+
+ ActivityTransitionRequest atr = new ActivityTransitionRequest(transitions);
+ Intent intent = new Intent(mApplicationContext, TransitionBroadcastReceiver.class);
+ // If pending intent is already created do not create a new one
+
+// PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 100, intent,
+// PendingIntent.FLAG_NO_CREATE);
+
+ // The above method returns null if the pending intent is not active
+ // it returns the pending intent object if the pending intent is alive
+ // --> The idea is here that if the pending intent is alive (i.e., pi object is not null)
+ // then do not create a new object
+ // However, every time the above method is called, the application stops receiving
+ // activity transitions
+ // TODO: figure out the problem described above
+
+ PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 100,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ Task task = ActivityRecognition.getClient(mApplicationContext)
+ .requestActivityTransitionUpdates(atr, pi);
+ task.addOnCompleteListener(task1 -> {
+ if (task1.isSuccessful()) {
+ Log.v(TAG, "Travel behavior activity-transition-update set up");
+ } else {
+ Log.v(TAG, "Travel behavior activity-transition-update failed set up: " +
+ task1.getException().getMessage());
+ task1.getException().printStackTrace();
+ }
+ });
+ }
+
+ public void stopCollectingData() {
+ Intent intent = new Intent(mApplicationContext, TransitionBroadcastReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 100, intent,
+ PendingIntent.FLAG_NO_CREATE);
+ if (pi != null) {
+ ActivityRecognition.getClient(mApplicationContext).removeActivityUpdates(pi);
+ }
+ }
+
+ private Drawable createIcon() {
+ Drawable icon = mApplicationContext.getResources().getDrawable(R.drawable.ic_light_bulb);
+ DrawableCompat.setTint(icon, mApplicationContext.getResources().getColor(R.color.theme_primary));
+ return icon;
+ }
+
+ public static void optOutUser() {
+ PreferenceUtils.saveBoolean(TravelBehaviorConstants.USER_OPT_OUT, true);
+ PreferenceUtils.saveBoolean(TravelBehaviorConstants.USER_OPT_IN, false);
+ }
+
+ public static void optInUser(String uid) {
+ PreferenceUtils.saveString(TravelBehaviorConstants.USER_ID, uid);
+ PreferenceUtils.saveBoolean(TravelBehaviorConstants.USER_OPT_IN, true);
+ PreferenceUtils.saveBoolean(TravelBehaviorConstants.USER_OPT_OUT, false);
+ }
+
+ public static void saveDestinationReminders(String currStopId, String destStopId, String tripId,
+ String routeId, Long serverTime) {
+ if (TravelBehaviorUtils.isTravelBehaviorDataCollectionActive()) {
+ DestinationReminderDataSaverTask saverTask = new DestinationReminderDataSaverTask(currStopId,
+ destStopId, tripId, routeId, serverTime, Application.get().getApplicationContext());
+ TravelBehaviorFileSaverExecutorManager manager = TravelBehaviorFileSaverExecutorManager.getInstance();
+ manager.runTask(saverTask);
+ }
+ }
+
+ public static void saveArrivalInfo(ObaArrivalInfo[] info, String url, long serverTime, String stopId) {
+ if (TravelBehaviorUtils.isTravelBehaviorDataCollectionActive()) {
+ ArrivalAndDepartureDataSaverTask saverTask = new ArrivalAndDepartureDataSaverTask(info,
+ serverTime, url, stopId, Application.get().getApplicationContext());
+ TravelBehaviorFileSaverExecutorManager manager = TravelBehaviorFileSaverExecutorManager.getInstance();
+ manager.runTask(saverTask);
+ }
+ }
+
+ public static void saveTripPlan(TripPlan tripPlan, String url, Context applicationContext) {
+ if (TravelBehaviorUtils.isTravelBehaviorDataCollectionActive()) {
+ TripPlanDataSaverTask dataSaverTask = new TripPlanDataSaverTask(tripPlan, url,
+ applicationContext);
+ TravelBehaviorFileSaverExecutorManager executorManager =
+ TravelBehaviorFileSaverExecutorManager.getInstance();
+ executorManager.runTask(dataSaverTask);
+ }
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/constants/TravelBehaviorConstants.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/constants/TravelBehaviorConstants.java
new file mode 100644
index 000000000..e74abe83d
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/constants/TravelBehaviorConstants.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.constants;
+
+import android.Manifest;
+
+public class TravelBehaviorConstants {
+
+ public static final String RECORD_ID = "tbRecordId";
+
+ public static final String USER_ID = "tbUserId";
+
+ public static final String USER_EMAIL = "tbUserEmail";
+
+ public static final String PARTICIPANT_SERVICE_RESULT = "STATUS OK";
+
+ public static final String REQUEST_CODE = "tbRequestCode";
+
+ public static final String TRIP_PLAN_COUNTER = "tripPlanCounter";
+
+ public static final String ARRIVAL_LIST_COUNTER = "arrivalCounter";
+
+ public static final String DESTINATION_REMINDER_COUNTER = "destinationReminderCounter";
+
+ public static final String FIREBASE_ACTIVITY_TRANSITION_FOLDER = "activity-transitions/";
+
+ public static final String FIREBASE_ARRIVAL_AND_DEPARTURE_FOLDER = "arrival-and-departures";
+
+ public static final String FIREBASE_TRIP_PLAN_FOLDER = "trip-plans";
+
+ public static final String FIREBASE_DESTINATION_REMINDER_FOLDER = "destination-reminders";
+
+ public static final String LOCAL_TRIP_PLAN_FOLDER = "trip-plans";
+
+ public static final String LOCAL_ARRIVAL_AND_DEPARTURE_FOLDER = "arrival-and-departures";
+
+ public static final String LOCAL_DESTINATION_REMINDER_FOLDER = "destination-reminders";
+
+ private static final long MOST_RECENT_DATA_THRESHOLD_MINUTES = 30l;
+
+ public static final long MOST_RECENT_DATA_THRESHOLD_MILLIS = 60000l *
+ MOST_RECENT_DATA_THRESHOLD_MINUTES;
+
+ public static final long MOST_RECENT_DATA_THRESHOLD_NANO = 60000000000l *
+ MOST_RECENT_DATA_THRESHOLD_MINUTES;
+
+ public static final String USER_OPT_IN = "travelBehaviorUserOptIn";
+
+ public static final String USER_OPT_OUT = "travelBehaviorUserOptOut";
+
+ public static final String[] PERMISSIONS = {
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION,
+ Manifest.permission.ACTIVITY_RECOGNITION
+ };
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/TravelBehaviorFileSaverExecutorManager.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/TravelBehaviorFileSaverExecutorManager.java
new file mode 100644
index 000000000..b007bb594
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/TravelBehaviorFileSaverExecutorManager.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class TravelBehaviorFileSaverExecutorManager {
+
+ private final ThreadPoolExecutor mThreadPoolExecutor;
+
+ private final BlockingQueue mBlockingQueue;
+
+ private static final int CORE_POOL_SIZE = 5;
+ private static final int MAX_POOL_SIZE = 5;
+ private static final int KEEP_ALIVE_TIME = 50;
+
+ private static TravelBehaviorFileSaverExecutorManager mManager = null;
+
+ static {
+ mManager = new TravelBehaviorFileSaverExecutorManager();
+ }
+
+ private TravelBehaviorFileSaverExecutorManager(){
+ mBlockingQueue = new LinkedBlockingQueue();
+ mThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE,
+ KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, mBlockingQueue);
+ }
+
+ public static TravelBehaviorFileSaverExecutorManager getInstance(){
+ return mManager;
+ }
+
+ public void runTask(Runnable task){
+ mThreadPoolExecutor.execute(task);
+ }
+
+ public ThreadPoolExecutor getThreadPoolExecutor() {
+ return mThreadPoolExecutor;
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/ArrivalAndDepartureDataSaverTask.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/ArrivalAndDepartureDataSaverTask.java
new file mode 100644
index 000000000..8d377430d
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/ArrivalAndDepartureDataSaverTask.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.task;
+
+import com.google.android.gms.location.FusedLocationProviderClient;
+import com.google.android.gms.location.LocationServices;
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.io.elements.ObaArrivalInfo;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.io.TravelBehaviorFileSaverExecutorManager;
+import org.onebusaway.android.travelbehavior.model.ArrivalAndDepartureInfo;
+import org.onebusaway.android.util.PreferenceUtils;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+public class ArrivalAndDepartureDataSaverTask implements Runnable {
+
+ private static final String TAG = "TravelBehaviorTripPlan";
+
+ private ObaArrivalInfo[] mArrivalInfo;
+
+ private long mServerTime;
+
+ private String mStopId;
+
+ private String mUrl;
+
+ private Context mApplicationContext;
+
+ public ArrivalAndDepartureDataSaverTask(ObaArrivalInfo[] arrivalInfo, long serverTime,
+ String url, String stopId, Context applicationContext) {
+ mArrivalInfo = arrivalInfo;
+ this.mServerTime = serverTime;
+ mStopId = stopId;
+ mUrl = url;
+ mApplicationContext = applicationContext;
+ }
+
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ saveArrivalAndDepartureData(null);
+ } else {
+ requestFusedLocation();
+ }
+
+ }
+
+ @SuppressLint("MissingPermission")
+ private void requestFusedLocation() {
+ FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(mApplicationContext);
+ client.getLastLocation().addOnSuccessListener(TravelBehaviorFileSaverExecutorManager.
+ getInstance().getThreadPoolExecutor(), location -> {
+ saveArrivalAndDepartureData(location);
+ });
+ }
+
+ private void saveArrivalAndDepartureData(Location location) {
+ try {
+ // Get the counter that's incremented for each test
+ int counter = Application.getPrefs().getInt(TravelBehaviorConstants.ARRIVAL_LIST_COUNTER, 0);
+ PreferenceUtils.saveInt(TravelBehaviorConstants.ARRIVAL_LIST_COUNTER, ++counter);
+
+ SimpleDateFormat sdf = new SimpleDateFormat("EEE, MMM d yyyy, hh:mm aaa");
+ Date time = Calendar.getInstance().getTime();
+ String readableDate = sdf.format(time);
+
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator
+ + TravelBehaviorConstants.LOCAL_ARRIVAL_AND_DEPARTURE_FOLDER);
+
+ if (!subFolder.exists()) {
+ subFolder.mkdirs();
+ }
+
+ Long localElapsedRealtimeNanos = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ localElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+ }
+
+ File file = new File(subFolder, counter + "-" + readableDate + ".json");
+ ArrivalAndDepartureInfo.ArrivalAndDepartureData add =
+ new ArrivalAndDepartureInfo.ArrivalAndDepartureData(mArrivalInfo, mStopId,
+ Application.get().getCurrentRegion().getId(), mUrl, localElapsedRealtimeNanos,
+ time.getTime(), mServerTime);
+ add.setLocation(location);
+
+ // Used Gson instead of Jackson library because, Jackson had problems while deserializing
+ // nested objects. When we deserialize the object and push it to Firebase, firebase API
+ // throwed a null pointer exception. Serializing and deserializing this arrival and
+ // departure data with Gson fixed the problem.
+ Gson gson = new Gson();
+ String data = gson.toJson(add);
+
+ FileUtils.write(file, data, false);
+ } catch (IOException e) {
+ Log.e(TAG, "File write failed: " + e.toString());
+ }
+ }
+
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/DestinationReminderDataSaverTask.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/DestinationReminderDataSaverTask.java
new file mode 100644
index 000000000..67c8f5473
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/DestinationReminderDataSaverTask.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.task;
+
+import com.google.android.gms.location.FusedLocationProviderClient;
+import com.google.android.gms.location.LocationServices;
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.io.TravelBehaviorFileSaverExecutorManager;
+import org.onebusaway.android.travelbehavior.model.ArrivalAndDepartureInfo;
+import org.onebusaway.android.travelbehavior.model.DestinationReminderInfo;
+import org.onebusaway.android.util.PreferenceUtils;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+public class DestinationReminderDataSaverTask implements Runnable {
+
+ private static final String TAG = "TravelBehaviorDestRmdr";
+
+ private String mCurrStopId;
+
+ private String mDestStopId;
+
+ private String mTripId;
+
+ private String mRouteId;
+
+ private long mServerTime;
+
+ private Context mApplicationContext;
+
+ public DestinationReminderDataSaverTask(String currStopId, String destStopId,
+ String tripId, String routeId, long serverTime, Context applicationContext) {
+ mCurrStopId = currStopId;
+ mDestStopId = destStopId;
+ mTripId = tripId;
+ mRouteId = routeId;
+ mServerTime = serverTime;
+ mApplicationContext = applicationContext;
+ }
+
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ saveDestinationReminders(null);
+ } else {
+ requestFusedLocation();
+ }
+
+ }
+
+ @SuppressLint("MissingPermission")
+ private void requestFusedLocation() {
+ FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(mApplicationContext);
+ client.getLastLocation().addOnSuccessListener(TravelBehaviorFileSaverExecutorManager.
+ getInstance().getThreadPoolExecutor(), location -> {
+ saveDestinationReminders(location);
+ });
+ }
+
+ private void saveDestinationReminders(Location location) {
+ try {
+ // Get the counter that's incremented for each test
+ int counter = Application.getPrefs().getInt(TravelBehaviorConstants.DESTINATION_REMINDER_COUNTER, 0);
+ PreferenceUtils.saveInt(TravelBehaviorConstants.DESTINATION_REMINDER_COUNTER, ++counter);
+
+ SimpleDateFormat sdf = new SimpleDateFormat("EEE, MMM d yyyy, hh:mm aaa");
+ Date time = Calendar.getInstance().getTime();
+ String readableDate = sdf.format(time);
+
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator +
+ TravelBehaviorConstants.LOCAL_DESTINATION_REMINDER_FOLDER);
+
+ if (!subFolder.exists()) {
+ subFolder.mkdirs();
+ }
+
+ Long localElapsedRealtimeNanos = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ localElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+ }
+
+ File file = new File(subFolder, counter + "-" + readableDate + ".json");
+ DestinationReminderInfo.DestinationReminderData drd = new DestinationReminderInfo.
+ DestinationReminderData(mCurrStopId ,mDestStopId, mTripId, mRouteId,
+ Application.get().getCurrentRegion().getId(), localElapsedRealtimeNanos,
+ time.getTime(), mServerTime, location);
+
+ // Used Gson instead of Jackson library because, Jackson had problems while deserializing
+ // nested objects. When we deserialize the object and push it to Firebase, firebase API
+ // throwed a null pointer exception. Serializing and deserializing this arrival and
+ // departure data with Gson fixed the problem.
+ Gson gson = new Gson();
+ String data = gson.toJson(drd);
+
+ FileUtils.write(file, data, false);
+ } catch (IOException e) {
+ Log.e(TAG, "File write failed: " + e.toString());
+ }
+ }
+
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/TripPlanDataSaverTask.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/TripPlanDataSaverTask.java
new file mode 100644
index 000000000..5b11069bc
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/task/TripPlanDataSaverTask.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.task;
+
+import com.google.android.gms.location.FusedLocationProviderClient;
+import com.google.android.gms.location.LocationServices;
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.io.TravelBehaviorFileSaverExecutorManager;
+import org.onebusaway.android.travelbehavior.model.TripPlanInfo;
+import org.onebusaway.android.util.PreferenceUtils;
+import org.opentripplanner.api.model.TripPlan;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+public class TripPlanDataSaverTask implements Runnable {
+
+ private static final String TAG = "TravelBehaviorTripPlan";
+
+ private TripPlan mTripPlan;
+
+ private String mUrl;
+
+ private Context mApplicationContext;
+
+ public TripPlanDataSaverTask(TripPlan tripPlan, String url, Context applicationContext) {
+ mUrl = url;
+ mTripPlan = tripPlan;
+ mApplicationContext = applicationContext;
+ }
+
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+ mApplicationContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ saveTripPlan(null);
+ } else {
+ requestFusedLocation();
+ }
+
+ }
+
+ @SuppressLint("MissingPermission")
+ private void requestFusedLocation() {
+ FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(mApplicationContext);
+ client.getLastLocation().addOnSuccessListener(TravelBehaviorFileSaverExecutorManager.
+ getInstance().getThreadPoolExecutor(), location -> {
+ saveTripPlan(location);
+ });
+ }
+
+ private void saveTripPlan(Location location) {
+ try {
+ // Get the counter that's incremented for each test
+ int counter = Application.getPrefs().getInt(TravelBehaviorConstants.TRIP_PLAN_COUNTER, 0);
+ PreferenceUtils.saveInt(TravelBehaviorConstants.TRIP_PLAN_COUNTER, ++counter);
+
+ SimpleDateFormat sdf = new SimpleDateFormat("EEE, MMM d yyyy, hh:mm aaa");
+ Date time = Calendar.getInstance().getTime();
+ String readableDate = sdf.format(time);
+
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator +
+ TravelBehaviorConstants.LOCAL_TRIP_PLAN_FOLDER);
+
+ if (!subFolder.exists()) {
+ subFolder.mkdirs();
+ }
+
+ Long localElapsedRealtimeNanos = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ localElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+ }
+
+ Long serverTime = null;
+ if (mTripPlan.getDate() != null) {
+ serverTime = Long.valueOf(mTripPlan.getDate());
+ }
+
+ File file = new File(subFolder, counter + "-" + readableDate + ".json");
+ TripPlanInfo.TripPlanData tpd = new TripPlanInfo.TripPlanData(mTripPlan, mUrl,
+ Application.get().getCurrentRegion().getId(), localElapsedRealtimeNanos,
+ time.getTime(), serverTime);
+ tpd.setLocation(location);
+
+ Gson gson = new Gson();
+ String data = gson.toJson(tpd);
+
+ FileUtils.write(file, data, false);
+ } catch (IOException e) {
+ Log.e(TAG, "File write failed: " + e.toString());
+ }
+ }
+
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/ArrivalsAndDeparturesDataReaderWorker.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/ArrivalsAndDeparturesDataReaderWorker.java
new file mode 100644
index 000000000..10e68fb9e
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/ArrivalsAndDeparturesDataReaderWorker.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.worker;
+
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.TrueFileFilter;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.model.ArrivalAndDepartureInfo;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorFirebaseIOUtils;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+public class ArrivalsAndDeparturesDataReaderWorker extends Worker {
+
+ public ArrivalsAndDeparturesDataReaderWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ readAndPostArrivalsAndDeparturesData();
+ return Result.success();
+ }
+
+ private void readAndPostArrivalsAndDeparturesData() {
+ try {
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator +
+ TravelBehaviorConstants.LOCAL_ARRIVAL_AND_DEPARTURE_FOLDER);
+
+ // If the directory is not exist do not read
+ if (subFolder == null || !subFolder.isDirectory()) return;
+
+ Collection files = FileUtils.listFiles(subFolder, TrueFileFilter.INSTANCE,
+ TrueFileFilter.INSTANCE);
+ if (files != null && !files.isEmpty()) {
+ List l = new ArrayList<>();
+ Gson gson = new Gson();
+ for (File f : files) {
+ try {
+ String jsonStr = FileUtils.readFileToString(f);
+ ArrivalAndDepartureInfo.ArrivalAndDepartureData data =
+ gson.fromJson(jsonStr, ArrivalAndDepartureInfo.ArrivalAndDepartureData.class);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ if (SystemClock.elapsedRealtimeNanos() - data.getLocalElapsedRealtimeNanos() <
+ TravelBehaviorConstants.MOST_RECENT_DATA_THRESHOLD_NANO) {
+ l.add(data);
+ }
+ } else {
+ if (System.currentTimeMillis() - data.getLocalSystemCurrMillis() <
+ TravelBehaviorConstants.MOST_RECENT_DATA_THRESHOLD_MILLIS) {
+ l.add(data);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ FileUtils.cleanDirectory(subFolder);
+
+ String uid = getInputData().getString(TravelBehaviorConstants.USER_ID);
+ String recordId = getInputData().getString(TravelBehaviorConstants.RECORD_ID);
+
+ TravelBehaviorFirebaseIOUtils.saveArrivalsAndDepartures(l, uid, recordId);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/DestinationReminderReaderWorker.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/DestinationReminderReaderWorker.java
new file mode 100644
index 000000000..ea906da6d
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/DestinationReminderReaderWorker.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.worker;
+
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.TrueFileFilter;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.model.DestinationReminderInfo;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorFirebaseIOUtils;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+public class DestinationReminderReaderWorker extends Worker {
+
+ public DestinationReminderReaderWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ readAndPostDestinationReminderData();
+ return Result.success();
+ }
+
+ private void readAndPostDestinationReminderData() {
+ try {
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator +
+ TravelBehaviorConstants.LOCAL_DESTINATION_REMINDER_FOLDER);
+
+ // If the directory is not exist do not read
+ if (subFolder == null || !subFolder.isDirectory()) return;
+
+ Collection files = FileUtils.listFiles(subFolder, TrueFileFilter.INSTANCE,
+ TrueFileFilter.INSTANCE);
+ Gson gson = new Gson();
+ if (files != null && !files.isEmpty()) {
+ List l = new ArrayList<>();
+ for (File f : files) {
+ try {
+ String jsonStr = FileUtils.readFileToString(f);
+ DestinationReminderInfo.DestinationReminderData destinationReminderData =
+ gson.fromJson(jsonStr, DestinationReminderInfo.DestinationReminderData.class);
+ l.add(destinationReminderData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ FileUtils.cleanDirectory(subFolder);
+
+ String uid = getInputData().getString(TravelBehaviorConstants.USER_ID);
+ String recordId = getInputData().getString(TravelBehaviorConstants.RECORD_ID);
+
+ TravelBehaviorFirebaseIOUtils.saveDestinationReminders(l, uid, recordId);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/RegisterTravelBehaviorParticipantWorker.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/RegisterTravelBehaviorParticipantWorker.java
new file mode 100644
index 000000000..0aa1740ba
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/RegisterTravelBehaviorParticipantWorker.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.worker;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.firebase.auth.FirebaseAuth;
+
+import org.apache.commons.io.IOUtils;
+import org.onebusaway.android.R;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.io.ObaConnection;
+import org.onebusaway.android.io.ObaDefaultConnectionFactory;
+import org.onebusaway.android.travelbehavior.TravelBehaviorManager;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.io.TravelBehaviorFileSaverExecutorManager;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import androidx.annotation.NonNull;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.work.ListenableWorker;
+import androidx.work.WorkerParameters;
+
+public class RegisterTravelBehaviorParticipantWorker extends ListenableWorker {
+
+ private static final String TAG = "RegisterTravelUser";
+
+ private ResolvableFuture mFuture;
+
+ public RegisterTravelBehaviorParticipantWorker(@NonNull Context context,
+ @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture startWork() {
+ mFuture = ResolvableFuture.create();
+ registerUser();
+ return mFuture;
+ }
+
+ private void registerUser() {
+ FirebaseAuth auth = FirebaseAuth.getInstance();
+ Log.v(TAG, "Initializing anonymous firebase user");
+ TravelBehaviorFileSaverExecutorManager manager =
+ TravelBehaviorFileSaverExecutorManager.getInstance();
+ auth.signInAnonymously()
+ .addOnCompleteListener(manager.getThreadPoolExecutor(), task -> {
+ if (task.isSuccessful()) {
+ Log.v(TAG, "Firebase user init success ID: " + auth.getUid());
+ saveEmailAddress(auth.getUid());
+ } else {
+ Log.v(TAG, "Firebase user init failed:" + task.getException().getMessage());
+ task.getException().printStackTrace();
+ mFuture.set(Result.failure());
+ }
+ });
+ }
+
+ private void saveEmailAddress(String uid) {
+ String email = getInputData().getString(TravelBehaviorConstants.USER_EMAIL);
+ Uri uri = buildUri(uid, email);
+ try {
+ ObaConnection connection = ObaDefaultConnectionFactory.getInstance().newConnection(uri);
+ Reader reader = connection.get();
+ String result = IOUtils.toString(reader);
+ if (TravelBehaviorConstants.PARTICIPANT_SERVICE_RESULT.equals(result)) {
+ TravelBehaviorManager.optInUser(uid);
+ mFuture.set(Result.success());
+ } else {
+ mFuture.set(Result.failure());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ Result.failure();
+ }
+ }
+
+ private Uri buildUri(String uid, String email) {
+ return Uri.parse(Application.get().getResources().getString(R.string.
+ travel_behavior_participants_url)).buildUpon().appendQueryParameter("id", uid)
+ .appendQueryParameter("email", email).build();
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/TripPlanDataReaderWorker.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/TripPlanDataReaderWorker.java
new file mode 100644
index 000000000..2ac34432f
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/io/worker/TripPlanDataReaderWorker.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.io.worker;
+
+import com.google.gson.Gson;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.TrueFileFilter;
+import org.onebusaway.android.app.Application;
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.model.TripPlanInfo;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorFirebaseIOUtils;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+public class TripPlanDataReaderWorker extends Worker {
+
+ public TripPlanDataReaderWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ readAndPostTripPlanData();
+ return Result.success();
+ }
+
+ private void readAndPostTripPlanData() {
+ try {
+ File subFolder = new File(Application.get().getApplicationContext()
+ .getFilesDir().getAbsolutePath() + File.separator +
+ TravelBehaviorConstants.LOCAL_TRIP_PLAN_FOLDER);
+
+ // If the directory is not exist do not read
+ if (subFolder == null || !subFolder.isDirectory()) return;
+
+ Collection files = FileUtils.listFiles(subFolder, TrueFileFilter.INSTANCE,
+ TrueFileFilter.INSTANCE);
+ Gson gson = new Gson();
+ if (files != null && !files.isEmpty()) {
+ List l = new ArrayList<>();
+ for (File f : files) {
+ try {
+ String jsonStr = FileUtils.readFileToString(f);
+ TripPlanInfo.TripPlanData tripPlanData = gson.fromJson(jsonStr,
+ TripPlanInfo.TripPlanData.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ if (SystemClock.elapsedRealtimeNanos() - tripPlanData.getLocalElapsedRealtimeNanos() <
+ TravelBehaviorConstants.MOST_RECENT_DATA_THRESHOLD_NANO) {
+ l.add(tripPlanData);
+ }
+ } else {
+ if (System.currentTimeMillis() - tripPlanData.getLocalSystemCurrMillis() <
+ TravelBehaviorConstants.MOST_RECENT_DATA_THRESHOLD_MILLIS) {
+ l.add(tripPlanData);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ FileUtils.cleanDirectory(subFolder);
+
+ String uid = getInputData().getString(TravelBehaviorConstants.USER_ID);
+ String recordId = getInputData().getString(TravelBehaviorConstants.RECORD_ID);
+
+ TravelBehaviorFirebaseIOUtils.saveTripPlans(l, uid, recordId);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ArrivalAndDepartureInfo.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ArrivalAndDepartureInfo.java
new file mode 100644
index 000000000..0ef1cd45d
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ArrivalAndDepartureInfo.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.model;
+
+import org.onebusaway.android.io.elements.ObaArrivalInfo;
+
+import android.location.Location;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ArrivalAndDepartureInfo {
+
+ public static class ArrivalAndDepartureData {
+ public TravelBehaviorInfo.LocationInfo locationInfo;
+
+ public List arrivalList;
+
+ public Long localElapsedRealtimeNanos;
+
+ public Long localSystemCurrMillis;
+
+ public Long obaServerTimestamp;
+
+ public String stopId;
+
+ public Long regionId;
+
+ public String url;
+
+ public ArrivalAndDepartureData() {
+ }
+
+ public ArrivalAndDepartureData(ObaArrivalInfo[] info, String stopId, Long regionId,
+ String url, Long localElapsedRealtimeNanos,
+ Long localSystemCurrMillis, Long obaServerTimestamp) {
+ this.stopId = stopId;
+ this.regionId = regionId;
+ this.url = url;
+ this.localElapsedRealtimeNanos = localElapsedRealtimeNanos;
+ this.localSystemCurrMillis = localSystemCurrMillis;
+ this.obaServerTimestamp = obaServerTimestamp;
+
+ arrivalList = new ArrayList<>();
+ if (info != null && info.length != 0) {
+ for (ObaArrivalInfo oai: info) {
+ arrivalList.add(new ObaArrivalInfoPojo(oai));
+ }
+ }
+ }
+
+ public List getArrivalList() {
+ return arrivalList;
+ }
+
+ public Long getLocalElapsedRealtimeNanos() {
+ return localElapsedRealtimeNanos;
+ }
+
+ public Long getLocalSystemCurrMillis() {
+ return localSystemCurrMillis;
+ }
+
+ public Long getObaServerTimestamp() {
+ return obaServerTimestamp;
+ }
+
+ public String getStopId() {
+ return stopId;
+ }
+
+ public Long getRegionId() {
+ return regionId;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setLocation(Location location) {
+ locationInfo = new TravelBehaviorInfo.LocationInfo(location);
+ }
+ }
+
+ List arrivalAndDepartureData;
+
+ public ArrivalAndDepartureInfo(List arrivalAndDepartureData) {
+ this.arrivalAndDepartureData = arrivalAndDepartureData;
+ }
+
+
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/DestinationReminderInfo.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/DestinationReminderInfo.java
new file mode 100644
index 000000000..b2c83a9e9
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/DestinationReminderInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012-2015 Paul Watts (paulcwatts@gmail.com), University of South Florida,
+ * Benjamin Du (bendu@me.com), and individual contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.model;
+
+import android.location.Location;
+
+import java.util.List;
+
+public class DestinationReminderInfo {
+ public static class DestinationReminderData {
+
+ public String currStopId;
+
+ public String destStopId;
+
+ public String tripId;
+
+ public String routeId;
+
+ public Long regionId;
+
+ public Long localElapsedRealtimeNanos;
+
+ public Long localSystemCurrMillis;
+
+ public Long obaServerTimestamp;
+
+ public TravelBehaviorInfo.LocationInfo locationInfo;
+
+ public DestinationReminderData(String currStopId, String destStopId, String tripId,
+ String routeId, Long regionId, Long localElapsedRealtimeNanos,
+ Long localSystemCurrMillis, Long obaServerTimestamp,
+ Location location) {
+ this.currStopId = currStopId;
+ this.destStopId = destStopId;
+ this.tripId = tripId;
+ this.routeId = routeId;
+ this.regionId = regionId;
+ this.localElapsedRealtimeNanos = localElapsedRealtimeNanos;
+ this.localSystemCurrMillis = localSystemCurrMillis;
+ this.obaServerTimestamp = obaServerTimestamp;
+ this.locationInfo = new TravelBehaviorInfo.LocationInfo(location);
+ }
+
+ public Long getLocalElapsedRealtimeNanos() {
+ return localElapsedRealtimeNanos;
+ }
+
+ public Long getLocalSystemCurrMillis() {
+ return localSystemCurrMillis;
+ }
+ }
+
+ List destinationReminderData;
+
+ public DestinationReminderInfo(List destinationReminderData) {
+ this.destinationReminderData = destinationReminderData;
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ObaArrivalInfoPojo.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ObaArrivalInfoPojo.java
new file mode 100644
index 000000000..d95548850
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/ObaArrivalInfoPojo.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.model;
+
+import org.onebusaway.android.io.elements.ObaArrivalInfo;
+import org.onebusaway.android.io.elements.ObaTripStatus;
+import org.onebusaway.android.io.elements.ObaTripStatusElement;
+import org.onebusaway.android.io.elements.Occupancy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ObaArrivalInfoPojo {
+
+ private final String routeId;
+
+ private final String routeShortName;
+
+ private final String routeLongName;
+
+ private final String tripId;
+
+ private final String tripHeadsign;
+
+ private final String stopId;
+
+ private final long predictedArrivalTime;
+
+ private final long scheduledArrivalTime;
+
+ private final long predictedDepartureTime;
+
+ private final long scheduledDepartureTime;
+
+ private final String status;
+
+ private final ObaArrivalInfo.Frequency frequency;
+
+ private final String vehicleId;
+
+ private final Double distanceFromStop;
+
+ private final Integer numberOfStopsAway;
+
+ private final long serviceDate;
+
+ private final long lastUpdateTime;
+
+ private final Boolean predicted;
+
+ private final ObaTripStatusElement tripStatus;
+
+ private final List situationIds;
+
+ private final boolean arrivalEnabled;
+
+ private final boolean departureEnabled;
+
+ private final int stopSequence;
+
+ private final int totalStopsInTrip;
+
+ private final int blockTripSequence;
+
+ private final Occupancy historicalOccupancy;
+
+ private final Occupancy predictedOccupancy;
+
+ public ObaArrivalInfoPojo() {
+ routeId = "";
+ routeShortName = "";
+ routeLongName = "";
+ tripId = "";
+ tripHeadsign = "";
+ stopId = "";
+ predictedArrivalTime = 0;
+ scheduledArrivalTime = 0;
+ predictedDepartureTime = 0;
+ scheduledDepartureTime = 0;
+ status = "";
+ frequency = null;
+ vehicleId = null;
+ distanceFromStop = null;
+ numberOfStopsAway = null;
+ serviceDate = 0;
+ lastUpdateTime = 0;
+ predicted = null;
+ tripStatus = null;
+ situationIds = null;
+ arrivalEnabled = true;
+ departureEnabled = true;
+ stopSequence = 0;
+ totalStopsInTrip = 0;
+ blockTripSequence = 0;
+ historicalOccupancy = null;
+ predictedOccupancy = null;
+ }
+
+ public ObaArrivalInfoPojo(ObaArrivalInfo info) {
+ routeId = info.getRouteId();
+ routeShortName = info.getShortName();
+ routeLongName = info.getRouteLongName();
+ tripId = info.getTripId();
+ tripHeadsign = info.getHeadsign();
+ stopId = info.getStopId();
+ predictedArrivalTime = info.getPredictedArrivalTime();
+ scheduledArrivalTime = info.getScheduledArrivalTime();
+ predictedDepartureTime = info.getPredictedDepartureTime();
+ scheduledDepartureTime = info.getScheduledDepartureTime();
+ status = info.getStatus();
+ frequency = info.getFrequency();
+ vehicleId = info.getVehicleId();
+ distanceFromStop = info.getDistanceFromStop();
+ numberOfStopsAway = info.getNumberOfStopsAway();
+ serviceDate = info.getServiceDate();
+ lastUpdateTime = info.getLastUpdateTime();
+ predicted = info.getPredicted();
+ tripStatus = (ObaTripStatusElement) info.getTripStatus();
+ situationIds = new ArrayList<>();
+ if (info.getSituationIds() != null && info.getSituationIds().length != 0) {
+ for(String id: info.getSituationIds()) {
+ situationIds.add(id);
+ }
+ }
+ arrivalEnabled = info.getArrivalEnabled();
+ departureEnabled = info.getDepartureEnabled();
+ stopSequence = info.getStopSequence();
+ totalStopsInTrip = info.getTotalStopsInTrip();
+ blockTripSequence = info.getBlockTripSequence();
+ historicalOccupancy = info.getHistoricalOccupancy();
+ predictedOccupancy = info.getPredictedOccupancy();
+ }
+
+ /**
+ * @return The ID of the route.
+ */
+ public String getRouteId() {
+ return routeId;
+ }
+
+ /**
+ * @return The short name of the route.
+ */
+ public String getShortName() {
+ return routeShortName;
+ }
+
+ /**
+ * @return The long name of the route.
+ */
+ public String getRouteLongName() {
+ return routeLongName;
+ }
+
+ /**
+ * @return The trip ID of the route.
+ */
+ public String getTripId() {
+ return tripId;
+ }
+
+ /**
+ * @return The trip headsign.
+ */
+ public String getHeadsign() {
+ return tripHeadsign;
+ }
+
+ /**
+ * @return The stop ID.
+ */
+ public String getStopId() {
+ return stopId;
+ }
+
+ /**
+ * @return The scheduled arrival time.
+ */
+ public long getScheduledArrivalTime() {
+ return scheduledArrivalTime;
+ }
+
+ /**
+ * @return The predicted arrival time, or 0.
+ */
+ public long getPredictedArrivalTime() {
+ return predictedArrivalTime;
+ }
+
+ /**
+ * @return The scheduled departure time.
+ */
+ public long getScheduledDepartureTime() {
+ return scheduledDepartureTime;
+ }
+
+ /**
+ * @return The predicted arrival time, or 0.
+ */
+ public long getPredictedDepartureTime() {
+ return predictedDepartureTime;
+ }
+
+ /**
+ * @return The status of the route.
+ */
+ public String getStatus() {
+ return status;
+ }
+
+ /**
+ * @return The frequency of the trip, for frequency-based scheduling. For
+ * time-based schedules, this is null.
+ */
+ public ObaArrivalInfo.Frequency getFrequency() {
+ return frequency;
+ }
+
+ /**
+ * @return The vehicle ID of the trip, or null if it is not provided.
+ */
+ public String getVehicleId() {
+ return vehicleId;
+ }
+
+ /**
+ * @return The distance, in meters, of the transit vehicle from the stop,
+ * or null if it is not provided.
+ */
+ public Double getDistanceFromStop() {
+ return distanceFromStop;
+ }
+
+ /**
+ * @return The number of stops between the transit vehicle and the current stop.
+ */
+ public Integer getNumberOfStopsAway() {
+ return numberOfStopsAway;
+ }
+
+ /**
+ * @return The midnight-based start time of the day of service of which a trip is
+ * operating, in Unix time, or 0 if this is not provided.
+ */
+ public long getServiceDate() {
+ return serviceDate;
+ }
+
+ /**
+ * @return
+ */
+ public long getLastUpdateTime() {
+ return lastUpdateTime;
+ }
+
+ /**
+ * @return Whether this arrival has prediction information. If the 'predicted'
+ * value is set, then that is used; otherwise it is inferred from the existence
+ * of a non-zero predicted start or end time.
+ */
+ public boolean getPredicted() {
+ return (predicted != null) ? predicted : (predictedDepartureTime != 0);
+ }
+
+ /**
+ * @return The trip status, if it exists.
+ */
+ public ObaTripStatus getTripStatus() {
+ return tripStatus;
+ }
+
+ /**
+ * @return The array of situation IDs, or null.
+ */
+ public List getSituationIds() {
+ return situationIds;
+ }
+
+ /**
+ * @return True if arrivals are enabled for this trip, false otherwise.
+ */
+ public boolean getArrivalEnabled() {
+ return arrivalEnabled;
+ }
+
+ /**
+ * @return True if departures are enabled for this trip, false otherwise.
+ */
+ public boolean getDepartureEnabled() {
+ return departureEnabled;
+ }
+
+ /**
+ * @return the index of the stop into the sequence of stops that make up the trip for this
+ * arrival. This value is 0-indexed, and is generated internally by OneBusAway (it is not the
+ * GTFS stop_sequence). The first stop in the trip will always have stopSequence = 0, while the
+ * last stop in the trip will always have stopSequence = totalStopsInTrip - 1.
+ */
+ public int getStopSequence() {
+ return stopSequence;
+ }
+
+ /**
+ * @return the total number of stops visited on the trip for this arrival, or 0 if the server
+ * doesn't support this field. If the same stop is visited more than once in this trip, each
+ * visitation is counted towards the total.
+ */
+ public int getTotalStopsInTrip() {
+ return totalStopsInTrip;
+ }
+
+ /**
+ * @return The index of this arrival's trip into the sequence of trips for the active block.
+ * Compare to blockTripSequence in the TripStatus element to determine where the
+ * arrival-and-departure
+ * is on the block in comparison to the active block location.
+ */
+ public int getBlockTripSequence() {
+ return blockTripSequence;
+ }
+
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TravelBehaviorInfo.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TravelBehaviorInfo.java
new file mode 100644
index 000000000..21be005fc
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TravelBehaviorInfo.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.model;
+
+import android.location.Location;
+import android.os.Build;
+
+import java.util.List;
+
+public class TravelBehaviorInfo {
+
+ public static class TravelBehaviorActivity {
+ public String detectedActivity;
+ public String detectedActivityType;
+ public Integer confidenceLevel;
+
+ public TravelBehaviorActivity() {
+ }
+
+ public TravelBehaviorActivity(String detectedActivity, String detectedActivityType) {
+ this.detectedActivity = detectedActivity;
+ this.detectedActivityType = detectedActivityType;
+ }
+ }
+
+ public static class LocationInfo {
+ public Double lat = null;
+
+ public Double lon = null;
+
+ public Long time = null;
+
+ public Long elapsedRealtimeNanos = null;
+
+ public Double altitude = null;
+
+ public String provider = null;
+
+ public Float accuracy = null;
+
+ public Float bearing = null;
+
+ public Float verticalAccuracyMeters = null;
+
+ public Float bearingAccuracyDegrees = null;
+
+ public Float speed = null;
+
+ public Float speedAccuracyMetersPerSecond = null;
+
+ public Boolean isFromMockProvider = null;
+
+ public LocationInfo() {
+ }
+
+ public LocationInfo(Location location) {
+ if (location == null) return;
+
+ this.lat = location.getLatitude();
+ this.lon = location.getLongitude();
+ this.time = location.getTime();
+ this.altitude = location.hasAltitude()? location.getAltitude(): null;
+ this.provider = location.getProvider();
+ this.accuracy = location.hasAccuracy() ? location.getAccuracy(): null;
+ this.bearing = location.hasBearing() ? location.getBearing(): null;
+ this.speed = location.hasSpeed() ? location.getSpeed() : null;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this.verticalAccuracyMeters = location.hasVerticalAccuracy() ?
+ location.getVerticalAccuracyMeters() : null;
+ this.bearingAccuracyDegrees = location.hasBearingAccuracy() ?
+ location.getBearingAccuracyDegrees() : null;
+ this.speedAccuracyMetersPerSecond = location.hasSpeedAccuracy() ?
+ location.getSpeedAccuracyMetersPerSecond() : null;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ this.elapsedRealtimeNanos = location.getElapsedRealtimeNanos();
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ isFromMockProvider = location.isFromMockProvider();
+ }
+ }
+ }
+
+ public List activities;
+
+ public List locationInfoList;
+
+ public Boolean isIgnoringBatteryOptimizations;
+
+ public TravelBehaviorInfo() {
+ }
+
+ public TravelBehaviorInfo(List activities,
+ Boolean isIgnoringBatteryOptimizations) {
+ this.activities = activities;
+ this.isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations;
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TripPlanInfo.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TripPlanInfo.java
new file mode 100644
index 000000000..8dd6d275a
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/model/TripPlanInfo.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.model;
+
+import org.opentripplanner.api.model.TripPlan;
+
+import android.location.Location;
+
+import java.util.List;
+
+public class TripPlanInfo {
+
+ public static class TripPlanData {
+ public TravelBehaviorInfo.LocationInfo locationInfo;
+
+ public TripPlan tripPlan;
+
+ public String url;
+
+ public Long regionId;
+
+ public Long localElapsedRealtimeNanos;
+
+ public Long localSystemCurrMillis;
+
+ public Long otpServerTimestamp;
+
+ public TripPlanData() {
+ }
+
+ public TripPlanData(TripPlan tripPlan, String url, Long regionId ,
+ Long localElapsedRealtimeNanos, Long localSystemCurrMillis,
+ Long otpServerTimestamp) {
+ this.tripPlan = tripPlan;
+ this.url = url;
+ this.regionId = regionId;
+ this.localElapsedRealtimeNanos = localElapsedRealtimeNanos;
+ this.localSystemCurrMillis = localSystemCurrMillis;
+ this.otpServerTimestamp = otpServerTimestamp;
+ }
+
+ public Long getLocalElapsedRealtimeNanos() {
+ return localElapsedRealtimeNanos;
+ }
+
+ public Long getLocalSystemCurrMillis() {
+ return localSystemCurrMillis;
+ }
+
+ public Long getOtpServerTimestamp() {
+ return otpServerTimestamp;
+ }
+
+ public void setLocation(Location location) {
+ locationInfo = new TravelBehaviorInfo.LocationInfo(location);
+ }
+ }
+
+ List tripPlanDataList;
+
+ public TripPlanInfo(List tripPlanDataList) {
+ this.tripPlanDataList = tripPlanDataList;
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/LocationBroadcastReceiver.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/LocationBroadcastReceiver.java
new file mode 100644
index 000000000..388f993d4
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/LocationBroadcastReceiver.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.receiver;
+
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorFirebaseIOUtils;
+import org.onebusaway.android.util.PreferenceUtils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.location.Location;
+import android.location.LocationManager;
+import android.util.Log;
+
+public class LocationBroadcastReceiver extends BroadcastReceiver {
+ private static final String TAG = "LocationReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String key = LocationManager.KEY_LOCATION_CHANGED;
+ Location location = (Location) intent.getExtras().get(key);
+ if (location != null) {
+ String recordId = intent.getStringExtra(TravelBehaviorConstants.RECORD_ID);
+ String uid = PreferenceUtils.getString(TravelBehaviorConstants.USER_ID);
+ TravelBehaviorFirebaseIOUtils.saveLocation(location, uid, recordId);
+ Log.v(TAG, "Location provider: " + location.getProvider());
+ } else {
+ Log.v(TAG, "Location provider is null");
+ }
+ }
+}
diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/RecognitionBroadcastReceiver.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/RecognitionBroadcastReceiver.java
new file mode 100644
index 000000000..74d3020e7
--- /dev/null
+++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/RecognitionBroadcastReceiver.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 University of South Florida
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.android.travelbehavior.receiver;
+
+import com.google.android.gms.location.ActivityRecognitionResult;
+import com.google.android.gms.location.DetectedActivity;
+import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.OnSuccessListener;
+import com.google.android.gms.tasks.Task;
+import com.google.firebase.firestore.DocumentReference;
+import com.google.firebase.firestore.DocumentSnapshot;
+import com.google.firebase.firestore.FirebaseFirestore;
+
+import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants;
+import org.onebusaway.android.travelbehavior.model.TravelBehaviorInfo;
+import org.onebusaway.android.travelbehavior.utils.TravelBehaviorUtils;
+import org.onebusaway.android.util.PreferenceUtils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+
+public class RecognitionBroadcastReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "ActivityRecognition";
+
+ private Map mDetectedActivityMap;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent != null) {
+ if (ActivityRecognitionResult.hasResult(intent)) {
+ ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
+ mDetectedActivityMap = new HashMap<>();
+ StringBuilder sb = new StringBuilder();
+ for (DetectedActivity da: result.getProbableActivities()) {
+ mDetectedActivityMap.put(TravelBehaviorUtils.
+ toActivityString(da.getType()), da.getConfidence());
+ sb.append(TravelBehaviorUtils.toActivityString(da.getType())).append(" -- ");
+ sb.append("confidence level: ").append(da.getConfidence()).append("\n");
+ }
+
+ Log.v(TAG, "Detected activity recognition: " + sb.toString());
+ String recordId = intent.getStringExtra(TravelBehaviorConstants.RECORD_ID);
+ readActivitiesByRecordId(recordId);
+ }
+ }
+ }
+
+ private void readActivitiesByRecordId(String recordId) {
+ String uid = PreferenceUtils.getString(TravelBehaviorConstants.USER_ID);
+ FirebaseFirestore db = FirebaseFirestore.getInstance();
+
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append("users/").append(uid).append("/").
+ append(TravelBehaviorConstants.FIREBASE_ACTIVITY_TRANSITION_FOLDER);
+
+ DocumentReference document = db.collection(pathBuilder.toString()).document(recordId);
+ document.get().addOnSuccessListener(new OnSuccessListener() {
+ @Override
+ public void onSuccess(DocumentSnapshot documentSnapshot) {
+ if(documentSnapshot.exists()) {
+ Log.v(TAG, "Read document successful RecognitionBroadcastReceiver");
+ TravelBehaviorInfo tbi = documentSnapshot.toObject(TravelBehaviorInfo.class);
+ updateTravelBehavior(tbi, recordId);
+ } else {
+ Log.v(TAG, "Read document FAILED RecognitionBroadcastReceiver");
+ }
+ }
+ });
+ }
+
+ private void updateTravelBehavior(TravelBehaviorInfo tbi, String recordId) {
+ String uid = PreferenceUtils.getString(TravelBehaviorConstants.USER_ID);
+ FirebaseFirestore db = FirebaseFirestore.getInstance();
+
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append("users/").append(uid).append("/").append("activity-transitions");
+
+ DocumentReference document = db.collection(pathBuilder.toString()).document(recordId);
+ List