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 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> list=new ArrayList<>(); + for (TravelBehaviorInfo.TravelBehaviorActivity tba: tbi.activities) { + Integer confidence = mDetectedActivityMap.get(tba.detectedActivity); + tba.confidenceLevel = confidence; + Map updateMap = new HashMap(); + updateMap.put("detectedActivity", tba.detectedActivity); + updateMap.put("detectedActivityType", tba.detectedActivityType); + updateMap.put("confidenceLevel", tba.confidenceLevel); + list.add(updateMap); + } + + document.update("activities", list).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.v(TAG, "Update travel behavior successful."); + } else { + Log.v(TAG, "Update travel behavior failed: " + task.getException().getMessage()); + task.getException().printStackTrace(); + } + } + }); + } +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/TransitionBroadcastReceiver.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/TransitionBroadcastReceiver.java new file mode 100644 index 000000000..63675dac6 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/receiver/TransitionBroadcastReceiver.java @@ -0,0 +1,226 @@ +/* + * 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.ActivityRecognition; +import com.google.android.gms.location.ActivityRecognitionClient; +import com.google.android.gms.location.ActivityTransitionEvent; +import com.google.android.gms.location.ActivityTransitionResult; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FirebaseFirestore; + +import org.onebusaway.android.app.Application; +import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants; +import org.onebusaway.android.travelbehavior.io.worker.ArrivalsAndDeparturesDataReaderWorker; +import org.onebusaway.android.travelbehavior.io.worker.DestinationReminderReaderWorker; +import org.onebusaway.android.travelbehavior.io.worker.TripPlanDataReaderWorker; +import org.onebusaway.android.travelbehavior.model.TravelBehaviorInfo; +import org.onebusaway.android.travelbehavior.utils.TravelBehaviorFirebaseIOUtils; +import org.onebusaway.android.travelbehavior.utils.TravelBehaviorUtils; +import org.onebusaway.android.util.PermissionUtils; +import org.onebusaway.android.util.PreferenceUtils; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +public class TransitionBroadcastReceiver extends BroadcastReceiver { +// implements LocationHelper.Listener + + private static final String TAG = "ActivityTransition"; + + // private LocationHelper mLocationHelper; + private Context mContext; + private List mActivityList; + private PendingIntent mPi; + private ActivityRecognitionClient mClient; + private String mUid; + private String mRecordId; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + if (ActivityTransitionResult.hasResult(intent)) { + mContext = context; + + ActivityTransitionResult result = ActivityTransitionResult.extractResult(intent); + StringBuilder sb = new StringBuilder(); + mActivityList = new ArrayList<>(); + + for (ActivityTransitionEvent event : result.getTransitionEvents()) { + sb.append(TravelBehaviorUtils.toActivityString(event.getActivityType())).append(" -- "); + sb.append(TravelBehaviorUtils.toTransitionType(event.getTransitionType())).append("\n"); + mActivityList.add(new TravelBehaviorInfo.TravelBehaviorActivity( + TravelBehaviorUtils.toActivityString(event.getActivityType()), + TravelBehaviorUtils.toTransitionType(event.getTransitionType()))); + } + + Log.v(TAG, "Detected activity transition: " + sb.toString()); + TravelBehaviorUtils.showDebugToastMessageWithVibration( + "Detected activity transition: " + sb.toString(), mContext); + + mUid = PreferenceUtils.getString(TravelBehaviorConstants.USER_ID); + saveTravelBehavior(); + } + } + } + + private void saveTravelBehavior() { + saveTravelBehavior(new TravelBehaviorInfo(mActivityList, + Application.isIgnoringBatteryOptimizations(mContext))); + + startSaveTripPlansWorker(); + + startSaveArrivalAndDepartureWorker(); + + startSaveDestinationRemindersWorker(); + + requestActivityRecognition(); + + requestLocationUpdates(); + } + + private void saveTravelBehavior(TravelBehaviorInfo tbi) { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append("users/").append(mUid).append("/").append( + TravelBehaviorConstants.FIREBASE_ACTIVITY_TRANSITION_FOLDER); + long riPrefix = PreferenceUtils.getLong(TravelBehaviorConstants.RECORD_ID, 0); + mRecordId = riPrefix++ + "-" + UUID.randomUUID().toString(); + PreferenceUtils.saveLong(TravelBehaviorConstants.RECORD_ID, riPrefix); + pathBuilder.append(mRecordId); + + DocumentReference document = FirebaseFirestore.getInstance().document(pathBuilder.toString()); + + document.set(tbi).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.v(TAG, "Activity transition document added with ID " + document.getId()); + } else { + Log.v(TAG, "Activity transition document failed to be added: " + + task.getException().getMessage()); + task.getException().printStackTrace(); + } + } + }); + } + + private void startSaveArrivalAndDepartureWorker() { + Data myData = new Data.Builder() + .putString(TravelBehaviorConstants.USER_ID, mUid) + .putString(TravelBehaviorConstants.RECORD_ID, mRecordId) + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest. + Builder(ArrivalsAndDeparturesDataReaderWorker.class) + .setInputData(myData) + .build(); + WorkManager.getInstance().enqueue(workRequest); + } + + private void startSaveTripPlansWorker() { + Data myData = new Data.Builder() + .putString(TravelBehaviorConstants.USER_ID, mUid) + .putString(TravelBehaviorConstants.RECORD_ID, mRecordId) + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(TripPlanDataReaderWorker.class) + .setInputData(myData) + .build(); + WorkManager.getInstance().enqueue(workRequest); + } + + private void startSaveDestinationRemindersWorker() { + Data myData = new Data.Builder() + .putString(TravelBehaviorConstants.USER_ID, mUid) + .putString(TravelBehaviorConstants.RECORD_ID, mRecordId) + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DestinationReminderReaderWorker.class) + .setInputData(myData) + .build(); + WorkManager.getInstance().enqueue(workRequest); + } + + private void requestActivityRecognition() { + mClient = ActivityRecognition.getClient(mContext); + Intent intent = new Intent(mContext, RecognitionBroadcastReceiver.class); + intent.putExtra(TravelBehaviorConstants.RECORD_ID, mRecordId); + + mPi = PendingIntent.getBroadcast(mContext, 100, intent, PendingIntent.FLAG_ONE_SHOT); + mClient.requestActivityUpdates(10000l, mPi); + } + + private void requestLocationUpdates() { + String[] requiredPermissions = {Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION}; + if (PermissionUtils.hasGrantedPermissions(mContext, requiredPermissions)) { + Log.v(TAG, "Location permissions are granted, requesting fused, GPS, and Network" + + "locations"); + requestFusedLocation(); + + requestGPSNetworkLocation(); + } else { + Log.v(TAG, "Location permissions not granted. Skipping location requests"); + } + } + + @SuppressLint("MissingPermission") + private void requestFusedLocation() { + FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(mContext); + client.getLastLocation().addOnSuccessListener(location -> { + if (location != null) { + TravelBehaviorFirebaseIOUtils.saveLocation(location, mUid, mRecordId); + } + }); + } + + @SuppressLint("MissingPermission") + private void requestGPSNetworkLocation() { + LocationManager lm = (LocationManager) Application.get().getBaseContext() + .getSystemService(Context.LOCATION_SERVICE); + + List providers = lm.getProviders(true); + for (String provider : providers) { + if (LocationManager.PASSIVE_PROVIDER.equals(provider)) continue; + + int reqCode = PreferenceUtils.getInt(TravelBehaviorConstants.REQUEST_CODE, 0); + Intent intent = new Intent(mContext, LocationBroadcastReceiver.class); + intent.putExtra(TravelBehaviorConstants.RECORD_ID, mRecordId); + PendingIntent pi = PendingIntent.getBroadcast(mContext, reqCode++, intent, PendingIntent.FLAG_ONE_SHOT); + lm.requestLocationUpdates(provider, 0, 0, pi); + PreferenceUtils.saveInt(TravelBehaviorConstants.REQUEST_CODE, reqCode); + } + } +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorFirebaseIOUtils.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorFirebaseIOUtils.java new file mode 100644 index 000000000..e7a8c0698 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorFirebaseIOUtils.java @@ -0,0 +1,114 @@ +package org.onebusaway.android.travelbehavior.utils; + +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FieldValue; +import com.google.firebase.firestore.FirebaseFirestore; + +import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants; +import org.onebusaway.android.travelbehavior.model.ArrivalAndDepartureInfo; +import org.onebusaway.android.travelbehavior.model.DestinationReminderInfo; +import org.onebusaway.android.travelbehavior.model.TravelBehaviorInfo; +import org.onebusaway.android.travelbehavior.model.TripPlanInfo; + +import android.location.Location; +import android.util.Log; + +import java.util.List; +import java.util.Map; + +public class TravelBehaviorFirebaseIOUtils { + + private static final String TAG = "TravelBehaviorFirebase"; + + private static String buildDocumentPathByUid(String uid, String folder) { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append("users/").append(uid).append("/"). + append(folder); + return pathBuilder.toString(); + } + + private static DocumentReference getFirebaseDocReferenceByUserIdAndRecordId(String userId, + String recordId, + String folder) { + String path = TravelBehaviorFirebaseIOUtils.buildDocumentPathByUid(userId, folder); + FirebaseFirestore db = FirebaseFirestore.getInstance(); + return db.collection(path).document(recordId); + } + + public static void saveLocation(Location location, String userId, String recordId) { + TravelBehaviorInfo.LocationInfo locationInfo = new TravelBehaviorInfo.LocationInfo(location); + Map locationMap = TravelBehaviorUtils.getLocationMapByLocationInfo(locationInfo); + + DocumentReference document = TravelBehaviorFirebaseIOUtils. + getFirebaseDocReferenceByUserIdAndRecordId(userId, recordId, + TravelBehaviorConstants.FIREBASE_ACTIVITY_TRANSITION_FOLDER); + document.update("locationInfoList", FieldValue.arrayUnion(locationMap)). + addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Log.v(TAG, "Location update saved with provider: " + + location.getProvider()); + } else { + Log.v(TAG, "Location update failed: " + + task.getException().getMessage()); + task.getException().printStackTrace(); + } + }); + } + + public static void saveArrivalsAndDepartures(List arrivalAndDepartureList, + String userId, String recordId) { + DocumentReference document = TravelBehaviorFirebaseIOUtils. + getFirebaseDocReferenceByUserIdAndRecordId(userId, recordId, + TravelBehaviorConstants.FIREBASE_ARRIVAL_AND_DEPARTURE_FOLDER); + + document.set(new ArrivalAndDepartureInfo(arrivalAndDepartureList)). + addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Log.v(TAG, "Arrivals and departure are saved with ID: " + + document.getId()); + } else { + Log.v(TAG, "Arrivals and departure are failed to be saved " + + task.getException().getMessage()); + task.getException().printStackTrace(); + } + }); + } + + public static void saveTripPlans(List tripPlanDataList, + String userId, String recordId) { + DocumentReference document = TravelBehaviorFirebaseIOUtils. + getFirebaseDocReferenceByUserIdAndRecordId(userId, recordId, + TravelBehaviorConstants.FIREBASE_TRIP_PLAN_FOLDER); + + document.set(new TripPlanInfo(tripPlanDataList)). + addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Log.v(TAG, "Trip plans are saved with ID: " + + document.getId()); + } else { + Log.v(TAG, "Trip plans are failed to be saved " + + task.getException().getMessage()); + task.getException().printStackTrace(); + } + }); + } + + public static void saveDestinationReminders(List reminderData, + String userId, String recordId) { + DocumentReference document = TravelBehaviorFirebaseIOUtils. + getFirebaseDocReferenceByUserIdAndRecordId(userId, recordId, + TravelBehaviorConstants.FIREBASE_DESTINATION_REMINDER_FOLDER); + + document.set(new DestinationReminderInfo(reminderData)). + addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Log.v(TAG, "Destination reminders are saved with ID: " + + document.getId()); + } else { + Log.v(TAG, "Destination reminders are failed to be saved " + + task.getException().getMessage()); + task.getException().printStackTrace(); + } + }); + } +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorUtils.java b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorUtils.java new file mode 100644 index 000000000..a03404163 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/travelbehavior/utils/TravelBehaviorUtils.java @@ -0,0 +1,88 @@ +package org.onebusaway.android.travelbehavior.utils; + +import com.google.android.gms.location.ActivityTransition; +import com.google.android.gms.location.DetectedActivity; + +import org.onebusaway.android.BuildConfig; +import org.onebusaway.android.app.Application; +import org.onebusaway.android.io.elements.ObaRegion; +import org.onebusaway.android.travelbehavior.constants.TravelBehaviorConstants; +import org.onebusaway.android.travelbehavior.model.TravelBehaviorInfo; +import org.onebusaway.android.util.PreferenceUtils; + +import android.content.Context; +import android.os.Vibrator; +import android.widget.Toast; + +import java.util.HashMap; +import java.util.Map; + +public class TravelBehaviorUtils { + + public static String toActivityString(int activity) { + switch (activity) { + case DetectedActivity.STILL: + return "STILL"; + case DetectedActivity.WALKING: + return "WALKING"; + case DetectedActivity.RUNNING: + return "RUNNING"; + case DetectedActivity.IN_VEHICLE: + return "IN_VEHICLE"; + case DetectedActivity.ON_FOOT: + return "ON_FOOT"; + default: + return "UNKNOWN"; + } + } + + public static String toTransitionType(int transitionType) { + switch (transitionType) { + case ActivityTransition.ACTIVITY_TRANSITION_ENTER: + return "ENTER"; + case ActivityTransition.ACTIVITY_TRANSITION_EXIT: + return "EXIT"; + default: + return "UNKNOWN"; + } + } + + public static Map getLocationMapByLocationInfo(TravelBehaviorInfo.LocationInfo locationInfo) { + Map m = new HashMap<>(); + if (locationInfo == null) return m; + m.put("lat", locationInfo.lat); + m.put("lon", locationInfo.lon); + m.put("time", locationInfo.time); + m.put("elapsedRealtimeNanos", locationInfo.elapsedRealtimeNanos); + m.put("altitude", locationInfo.altitude); + m.put("provider", locationInfo.provider); + m.put("accuracy", locationInfo.accuracy); + m.put("verticalAccuracyMeters", locationInfo.verticalAccuracyMeters); + m.put("bearingAccuracyDegrees", locationInfo.bearingAccuracyDegrees); + m.put("bearing", locationInfo.bearing); + m.put("speed", locationInfo.speed); + m.put("speedAccuracyMetersPerSecond", locationInfo.speedAccuracyMetersPerSecond); + m.put("isFromMockProvider", locationInfo.isFromMockProvider); + return m; + } + + public static boolean isTravelBehaviorActiveInRegion() { + ObaRegion currentRegion = Application.get().getCurrentRegion(); + return currentRegion != null && currentRegion.isTravelBehaviorDataCollectionEnabled(); + } + + public static boolean isTravelBehaviorDataCollectionActive() { + return isTravelBehaviorActiveInRegion() && + !PreferenceUtils.getBoolean(TravelBehaviorConstants.USER_OPT_OUT, + false) && PreferenceUtils.getBoolean(TravelBehaviorConstants.USER_OPT_IN, + false); + } + + public static void showDebugToastMessageWithVibration(String message, Context context) { + if (BuildConfig.DEBUG) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(200); + } + } +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListFragment.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListFragment.java index 8310ed88c..bbe893d9e 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListFragment.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListFragment.java @@ -18,35 +18,6 @@ */ package org.onebusaway.android.ui; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.ContentQueryMap; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnMultiChoiceClickListener; -import android.content.Intent; -import android.database.Cursor; -import android.location.Location; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - import com.google.firebase.analytics.FirebaseAnalytics; import org.onebusaway.android.R; @@ -66,6 +37,7 @@ import org.onebusaway.android.map.MapParams; import org.onebusaway.android.provider.ObaContract; import org.onebusaway.android.report.ui.InfrastructureIssueActivity; +import org.onebusaway.android.travelbehavior.TravelBehaviorManager; import org.onebusaway.android.util.ArrayAdapterWithIcon; import org.onebusaway.android.util.ArrivalInfoUtils; import org.onebusaway.android.util.BuildFlavorUtils; @@ -77,6 +49,35 @@ import org.onebusaway.android.util.ShowcaseViewUtils; import org.onebusaway.android.util.UIUtils; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnMultiChoiceClickListener; +import android.content.Intent; +import android.database.Cursor; +import android.location.Location; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -114,7 +115,7 @@ public class ArrivalsListFragment extends ListFragment /** * Comma-delimited set of routes that serve this stop * See {@link UIUtils#serializeRouteDisplayNames(ObaStop, - * java.util.HashMap)} + * HashMap)} */ public static final String STOP_ROUTES = ".StopRoutes"; @@ -263,7 +264,7 @@ public IntentBuilder setStopLocation(Location stopLocation) { * Sets the routes that serve this stop via a comma-delimited set of route display names *

* See {@link UIUtils#serializeRouteDisplayNames(ObaStop, - * java.util.HashMap)} + * HashMap)} * * @param routes comma-delimited list of route display names that serve this stop */ @@ -449,6 +450,9 @@ public void onLoadFinished(Loader loader, situations = UIUtils.getAllSituations(result, mRoutesFilter); refs = result.getRefs(); + TravelBehaviorManager.saveArrivalInfo(info, result.getUrl(), + result.getCurrentTime(), mStopId); + // Report Stop distance metric Location stopLocation = mStop.getLocation(); Location myLocation = Application.getLastKnownLocation(getActivity(), null); diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListLoader.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListLoader.java index ab72dbaa4..aebbe7db5 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListLoader.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/ArrivalsListLoader.java @@ -42,6 +42,8 @@ public class ArrivalsListLoader extends AsyncTaskLoader private static final int MINUTES_INCREMENT = 60; // minutes + private String mUrl; + public ArrivalsListLoader(Context context, String stopId) { super(context); mStopId = stopId; @@ -49,12 +51,20 @@ public ArrivalsListLoader(Context context, String stopId) { @Override public ObaArrivalInfoResponse loadInBackground() { - return ObaArrivalInfoRequest.newRequest(getContext(), mStopId, mMinutesAfter).call(); + // TBC collection project: Module of cache blaa + // TODO: cache get Url + ObaArrivalInfoRequest obaArrivalInfoRequest = ObaArrivalInfoRequest.newRequest(getContext(), + mStopId, mMinutesAfter); + mUrl = obaArrivalInfoRequest.getUri().toString(); + return obaArrivalInfoRequest.call(); } @Override public void deliverResult(ObaArrivalInfoResponse data) { mLastResponseTime = System.currentTimeMillis(); + if (data != null) { + data.setUrl(mUrl); + } if (data.getCode() == ObaApi.OBA_OK) { mLastGoodResponse = data; mLastGoodResponseTime = mLastResponseTime; diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java index b367bb817..12951da13 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java @@ -17,46 +17,12 @@ */ package org.onebusaway.android.ui; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.drawable.GradientDrawable; -import android.location.Location; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.Animation; -import android.view.animation.Transformation; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.firebase.analytics.FirebaseAnalytics; + import com.microsoft.embeddedsocial.sdk.EmbeddedSocial; import com.microsoft.embeddedsocial.ui.fragment.ActivityFeedFragment; import com.microsoft.embeddedsocial.ui.fragment.MyProfileFragment; @@ -77,6 +43,7 @@ import org.onebusaway.android.map.googlemapsv2.LayerInfo; import org.onebusaway.android.region.ObaRegionsTask; import org.onebusaway.android.report.ui.ReportActivity; +import org.onebusaway.android.travelbehavior.TravelBehaviorManager; import org.onebusaway.android.tripservice.TripService; import org.onebusaway.android.util.FragmentUtils; import org.onebusaway.android.util.LocationUtils; @@ -87,6 +54,41 @@ import org.onebusaway.android.util.UIUtils; import org.opentripplanner.routing.bike_rental.BikeRentalStation; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.drawable.GradientDrawable; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; @@ -382,6 +384,11 @@ public void onCreate(Bundle savedInstanceState) { UIUtils.setupActionBar(this); + checkBatteryOptimizations(); + + new TravelBehaviorManager(this, getApplicationContext()). + registerTravelBehaviorParticipant(); + if (!mInitialStartup || PermissionUtils.hasGrantedPermissions(this, LOCATION_PERMISSIONS)) { // It's not the first startup or if the user has already granted location permissions (Android L and lower), then check the region status // Otherwise, wait for a permission callback from the BaseMapFragment before checking the region status @@ -2001,4 +2008,22 @@ private boolean isSlidingPanelCollapsed() { public ArrivalsListFragment getArrivalsListFragment() { return mArrivalsListFragment; } + + private void checkBatteryOptimizations() { + if (Application.isIgnoringBatteryOptimizations(getApplicationContext())) { + showIgnoreBatteryOptimizationDialog(); + } + } + + private void showIgnoreBatteryOptimizationDialog() { + new android.app.AlertDialog.Builder(this) + .setMessage(R.string.application_ignoring_battery_opt_message) + .setTitle(R.string.application_ignoring_battery_opt_title) + .setIcon(R.drawable.ic_alert_warning) + .setPositiveButton(R.string.ok, + (dialog, which) -> { + dialog.dismiss(); + }) + .create().show(); + } } diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/PreferencesActivity.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/PreferencesActivity.java index cbc54857d..2460f62cb 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/PreferencesActivity.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/PreferencesActivity.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; @@ -53,6 +54,8 @@ import org.onebusaway.android.io.ObaAnalytics; import org.onebusaway.android.io.elements.ObaRegion; import org.onebusaway.android.region.ObaRegionsTask; +import org.onebusaway.android.travelbehavior.TravelBehaviorManager; +import org.onebusaway.android.travelbehavior.utils.TravelBehaviorUtils; import org.onebusaway.android.util.BackupUtils; import org.onebusaway.android.util.BuildFlavorUtils; import org.onebusaway.android.util.EmbeddedSocialUtils; @@ -90,6 +93,8 @@ public class PreferencesActivity extends PreferenceActivity Preference mAnalyticsPref; + CheckBoxPreference mTravelBehaviorPref; + Preference mTutorialPref; Preference mDonatePref; @@ -142,6 +147,17 @@ public void onCreate(Bundle savedInstanceState) { mAnalyticsPref = findPreference(getString(R.string.preferences_key_analytics)); mAnalyticsPref.setOnPreferenceChangeListener(this); + mTravelBehaviorPref = (CheckBoxPreference) findPreference(getString(R.string.preferences_key_travel_behavior)); + mTravelBehaviorPref.setOnPreferenceChangeListener(this); + + if (!TravelBehaviorUtils.isTravelBehaviorActiveInRegion()) { + PreferenceCategory aboutCategory = (PreferenceCategory) + findPreference(getString(R.string.preferences_category_about)); + aboutCategory.removePreference(mTravelBehaviorPref); + } else { + mTravelBehaviorPref.setChecked(TravelBehaviorUtils.isTravelBehaviorDataCollectionActive()); + } + mTutorialPref = findPreference(getString(R.string.preference_key_tutorial)); mTutorialPref.setOnPreferenceClickListener(this); @@ -437,6 +453,17 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { Boolean isAnalyticsActive = (Boolean) newValue; //Report if the analytics turns off, just before shared preference changed ObaAnalytics.setSendAnonymousData(mFirebaseAnalytics, isAnalyticsActive); + } else if (preference.equals(mTravelBehaviorPref) && newValue instanceof Boolean) { + Boolean isTravelBehaviorActive = (Boolean) newValue; + if(isTravelBehaviorActive) { + new TravelBehaviorManager(this, getApplicationContext()). + registerTravelBehaviorParticipant(true); + } else { + new TravelBehaviorManager(this, getApplicationContext()). + stopCollectingData(); + TravelBehaviorManager.optOutUser(); + } + } else if (preference.equals(mLeftHandMode) && newValue instanceof Boolean) { Boolean isLeftHandEnabled = (Boolean) newValue; //Report if left handed mode is turned on, just before shared preference changed diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripDetailsListFragment.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripDetailsListFragment.java index 9847a979a..53b79d663 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripDetailsListFragment.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripDetailsListFragment.java @@ -82,6 +82,7 @@ import org.onebusaway.android.io.request.ObaTripDetailsRequest; import org.onebusaway.android.io.request.ObaTripDetailsResponse; import org.onebusaway.android.nav.NavigationService; +import org.onebusaway.android.travelbehavior.TravelBehaviorManager; import org.onebusaway.android.util.ArrivalInfoUtils; import org.onebusaway.android.util.DBUtil; import org.onebusaway.android.util.LocationUtils; @@ -540,6 +541,9 @@ public void onClick(DialogInterface dialog, int which) { Application.get().getString(R.string.destination_reminder_title), Toast.LENGTH_LONG ).show(); + + TravelBehaviorManager.saveDestinationReminders(mStopId, mDestinationId, + mTripId, mRouteId, mTripInfo.getCurrentTime()); } }); diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripPlanActivity.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripPlanActivity.java index 5bdb0adaa..800a9dd77 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripPlanActivity.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripPlanActivity.java @@ -17,21 +17,8 @@ */ package org.onebusaway.android.ui; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.location.Location; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.ListView; -import android.widget.Toast; - import com.google.firebase.analytics.FirebaseAnalytics; + import com.sothree.slidinguppanel.ScrollableViewHelper; import com.sothree.slidinguppanel.SlidingUpPanelLayout; @@ -41,13 +28,28 @@ import org.onebusaway.android.directions.util.OTPConstants; import org.onebusaway.android.directions.util.TripRequestBuilder; import org.onebusaway.android.io.ObaAnalytics; +import org.onebusaway.android.travelbehavior.TravelBehaviorManager; import org.onebusaway.android.util.LocationUtils; import org.onebusaway.android.util.UIUtils; import org.opentripplanner.api.model.Itinerary; +import org.opentripplanner.api.model.TripPlan; import org.opentripplanner.api.ws.Message; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.location.Location; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.ListView; +import android.widget.Toast; + import java.util.ArrayList; -import java.util.List; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; @@ -310,12 +312,13 @@ private void initResultsFragment() { } @Override - public void onTripRequestComplete(List itineraries, String url) { + public void onTripRequestComplete(TripPlan tripPlan, String url) { + TravelBehaviorManager.saveTripPlan(tripPlan, url, getApplicationContext()); // Send intent to ourselves... Intent intent = new Intent(this, TripPlanActivity.class) .setAction(Intent.ACTION_MAIN) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - .putExtra(OTPConstants.ITINERARIES, (ArrayList) itineraries) + .putExtra(OTPConstants.ITINERARIES, (ArrayList) tripPlan.getItinerary()) .putExtra(OTPConstants.INTENT_SOURCE, OTPConstants.Source.ACTIVITY) .putExtra(PLAN_REQUEST_URL, url); startActivity(intent); diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/util/RegionUtils.java b/onebusaway-android/src/main/java/org/onebusaway/android/util/RegionUtils.java index dd750ecaf..0306f4173 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/util/RegionUtils.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/util/RegionUtils.java @@ -415,7 +415,8 @@ public static ArrayList getRegionsFromProvider(Context context) { ObaContract.Regions.SUPPORTS_EMBEDDED_SOCIAL, ObaContract.Regions.PAYMENT_ANDROID_APP_ID, ObaContract.Regions.PAYMENT_WARNING_TITLE, - ObaContract.Regions.PAYMENT_WARNING_BODY + ObaContract.Regions.PAYMENT_WARNING_BODY, + ObaContract.Regions.TRAVEL_BEHAVIOR_DATA_COLLECTION }; ContentResolver cr = context.getContentResolver(); @@ -465,7 +466,8 @@ public static ArrayList getRegionsFromProvider(Context context) { c.getInt(15) > 0, // Supports Embedded Social c.getString(16), // Android App ID for mobile fare payment app of region c.getString(17), // Payment Warning Title - c.getString(18) // Payment Warning Body + c.getString(18), + c.getInt(19) > 0 // travel behavior data collection// Payment Warning Body )); } while (c.moveToNext()); @@ -652,7 +654,8 @@ public static ObaRegion getRegionFromBuildFlavor() { BuildConfig.FIXED_REGION_SUPPORTS_EMBEDDEDSOCIAL, BuildConfig.FIXED_REGION_PAYMENT_ANDROID_APP_ID, BuildConfig.FIXED_REGION_PAYMENT_WARNING_TITLE, - BuildConfig.FIXED_REGION_PAYMENT_WARNING_BODY); + BuildConfig.FIXED_REGION_PAYMENT_WARNING_BODY, + BuildConfig.FIXED_TRAVEL_BEHAVIOR_DATA_COLLECTION); return region; } @@ -727,6 +730,8 @@ private static ContentValues toContentValues(ObaRegion region) { values.put(ObaContract.Regions.PAYMENT_ANDROID_APP_ID, region.getPaymentAndroidAppId()); values.put(ObaContract.Regions.PAYMENT_WARNING_TITLE, region.getPaymentWarningTitle()); values.put(ObaContract.Regions.PAYMENT_WARNING_BODY, region.getPaymentWarningBody()); + values.put(ObaContract.Regions.TRAVEL_BEHAVIOR_DATA_COLLECTION, + region.isTravelBehaviorDataCollectionEnabled() ? 1 : 0); return values; } diff --git a/onebusaway-android/src/main/res/raw/travel_behavior_informed_consent.html b/onebusaway-android/src/main/res/raw/travel_behavior_informed_consent.html new file mode 100644 index 000000000..357c8e5c6 --- /dev/null +++ b/onebusaway-android/src/main/res/raw/travel_behavior_informed_consent.html @@ -0,0 +1,135 @@ +

+ Informed Consent to Participate in Research +

+

+ Information to Consider Before Taking Part in this Research Study +

+

+ Pro + # Pro00041019 +

+

+ Project ID + # 2117-9063-19 +

+

+ Researchers at the University of South Florida (USF) study many topics. To + do this, we need the help of people who agree to take part in a research + study. This form tells you about this research study. We are asking you to + take part in a research study that is called: “Improving the Quality and + Cost Effectiveness of Multimodal Travel Behavior Data Collection”. The + person who is in charge of this research study is Dr. Sean J. Barbeau. This + person is called the Principal Investigator. +

+

+ This study is sponsored by: National Center for Transit Research +

+

+ Purpose of the Study +

+

+ + The purposes of this project are to improve transit service quality and + multimodal transportation planning. More specifically, the objectives of + the research project are to: +

+

+ A. Better understand the travel behavior of transit users, including where + and when travelers choose different mode of transportation such as the bus, + walking, biking, and a car. +

+

+ B. Reduce travel behavior data collection cost per completed trip for + transit agencies +

+

+ Why are you being asked to take part? +

+

+ You are being asked to participate because you use OneBusAway Android + mobile application. +

+

+ Study Procedures +

+

+ If you take part in this study, your travel behavior information will be + automatically collected through the OneBusAway Android application and you + will be asked to participate in online surveys about how and when you + travel places. The surveys will ask you about your daily travel activities + and demographic questions. Each survey will take about 10 minutes to + complete it, and you will be asked to complete approximately 4 surveys over + a period of two years. The research will be done at the University of South + Florida, University of Tennessee-Knoxville, Kyoto University, Edinburgh + Napier University, and Georgia Institute of Technology. +

+

+ Alternatives / Voluntary Participation / Withdrawal +

+

+ + You have the alternative to choose not to participate in this research + study by tapping the “I Decline” button at the bottom of this form. +
+ You should only take part in this study if you want to volunteer; you are + free to participate in this research or withdraw at any time. There will be + no penalty or loss of benefits you are entitled to receive if you stop + taking part in this study. To participate in this study tap the "I Accept" + button at the bottom of this form. +

+

+ + Compensation, + Benefits and Risks +

+

+ You will a receive a small incentive such as a $10 Amazon gift card to + participate in this study. There is no cost to participate. Enrolling in + this study may have a negative affect on your phone’s battery life. This + research is considered to be minimal risk. Minimal risk means that study + risks are the same as the risks you face in daily life. +

+

+ Privacy and Confidentiality +

+

+ We will keep your study records as confidential as possible. It is + possible, although unlikely, that unauthorized individuals could gain + access to your responses because you are responding online. +

+

+ Certain people may need to see your study records. By law, anyone who looks + at your records must keep them completely confidential. The only people who + will be allowed to see these records are: principal investigator - Dr. Sean + J. Barbeau, the research assistant - Cagri Cetin, research collaborators + such as Dr. Candace Brakewood, Dr. Jan-Dirk Schmoecker, Dr. Achille + Fonzone, Dr. Kari E. Watkins, and Aaron Brethorst. +

+

+ It is possible, although unlikely, that unauthorized individuals could gain + access to your responses. Confidentiality will be maintained to the degree + permitted by the technology used. No guarantees can be made regarding the + interception of data sent via the Internet. However, your participation in + this study involves risks similar to a person’s everyday use of the + Internet. +

+

+ Contact Information +

+

+ If you have any questions about your rights as a research participant, + please contact the USF IRB at (813) 974-5638 or contact by email at RSCH-IRB@usf.edu. If you have + questions regarding the research, please contact the Principal Investigator + at (813) 974-7208 or contact by email at barbeau@cutr.usf.edu. +

+

+ We may publish what we learn from this study. If we do, we will not let + anyone know your name. We will not publish anything else that would let + people know who you are. You can print a copy of this consent form for your + records. +

+

+ I freely give my consent to take part in this study. I understand that by + proceeding with this survey that I am agreeing to take part in research and + I am 18 years of age or older. +

diff --git a/onebusaway-android/src/main/res/values/donottranslate.xml b/onebusaway-android/src/main/res/values/donottranslate.xml index 3784e25f1..c8b764cdd 100644 --- a/onebusaway-android/src/main/res/values/donottranslate.xml +++ b/onebusaway-android/src/main/res/values/donottranslate.xml @@ -64,6 +64,7 @@ preferences_key_user_debugging_logs_category preferences_user_share_logs preference_never_show_destination_reminder_beta_dialog + preference_travel_behavior http://regions.onebusaway.org/regions-v3.json @@ -77,6 +78,9 @@ http://onebusaway.org + + https://script.google.com/macros/s/AKfycbz9qep6o0LgtlXkT755Rf51uh8JM7m0Z-2tIfuUn8hX0W7c9sbz/exec + https://github.com/OneBusAway/onebusaway-android diff --git a/onebusaway-android/src/main/res/values/strings.xml b/onebusaway-android/src/main/res/values/strings.xml index 2287ee788..87bc49a74 100644 --- a/onebusaway-android/src/main/res/values/strings.xml +++ b/onebusaway-android/src/main/res/values/strings.xml @@ -682,6 +682,8 @@
Send anonymous usage data Help us improve the app + Send anonymous travel behavior data + Help us improve the public transit Show tutorial Show instructions for using this app Donate @@ -1123,4 +1125,25 @@ Signed into Embedded Social Signed out of Embedded Social Embedded Social account deleted + + Performance Alert! + Battery optimizations are enabled for + this application which may decrease the performance of the application. To get the best experience + from this application, please go to settings and disable performance optimizations for OneBusAway. + + + + Would you like to improve public transportation? + Improve public transportation + Are you over the age of 18? + Please provide your email address + Email address is invalid + You successfully enrolled in the study. + Failed to enroll in the study. Please try again later. + Email + Yes + No + Agree + Decline + Save diff --git a/onebusaway-android/src/main/res/xml/preferences.xml b/onebusaway-android/src/main/res/xml/preferences.xml index 263291a51..21351ce27 100644 --- a/onebusaway-android/src/main/res/xml/preferences.xml +++ b/onebusaway-android/src/main/res/xml/preferences.xml @@ -135,6 +135,11 @@ android:title="@string/preferences_analytics_title" android:summary="@string/preferences_analytics_summary" android:defaultValue="true"/> +