diff --git a/.gitignore b/.gitignore
index 26614e1..1aca587 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,3 +99,10 @@ xcuserdata
# We're using source-control, so this is a "feature" that we do not want!
*.moved-aside
+
+.gradle
+/local.properties
+/.idea/
+*.iml
+.DS_Store
+/build/
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c5abd87..9ba6990 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,12 +1,7 @@
-
-
+>
-
-
+
+
+
@@ -41,26 +40,17 @@
-
-
-
-
+
-
-
@@ -77,10 +67,6 @@
android:name="edu.gatech.ppl.cycleatlanta.NoteMapActivity"
android:label="@string/title_activity_note_map" >
-
-
-
-
-
+ android:value="AIzaSyB0nRhRzcprm2h9panNqdBsFzVXRI5JOtE" />
\ No newline at end of file
diff --git a/Cycle-Atlanta-Android.iml b/Cycle-Atlanta-Android.iml
new file mode 100644
index 0000000..ab81124
--- /dev/null
+++ b/Cycle-Atlanta-Android.iml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugAndroidTestSources
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6bd6758
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# Cycle-Atlanta-Android
+
+Cycle Atlanta Android is a multi-region app to collect data from bicyclists.
+
+Cycle Atlanta Android provides:
+
+1. Collecting bicyclists' route information
+1. A list of bicyclists' previous trips
+1. Ability to add notes to trips
+
+
+### Prerequisites for both Android Studio and Gradle
+
+1. Clone this repository
+1. Install [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/index.html)
+
+### Building in Android Studio
+
+1. Download, install, and run the latest version of [Android Studio](http://developer.android.com/sdk/installing/studio.html).
+1. At the welcome screen select `Import Project`, browse to the location of this repository and select it then select Ok.
+1. Open the Android SDK Manager (Tools->Android->SDK Manager) and add a checkmark for the necessary API level (see `compileSdkVersion` in [`onebusaway-android/build.gradle`](onebusaway-android/build.gradle)) then select OK.
+1. Connect a [debugging enabled](https://developer.android.com/tools/device.html) Android device to your computer or setup an Android Virtual Device (Tools->Andorid->AVD Manager).
+1. Open the "Build Variants" window (it appears as a vertical button on left side of workspace by default) & choose **obaGoogleDebug** to select the Google Play version, or **obaAmazonDebug** to select the Fire Phone.
+1. Click the green play button (or Alt+Shift+F10) to build and run the project!
+
+### Building from the command line using Gradle
+
+1. Set the `JAVA_HOME` environmental variables to point to your JDK folder (e.g. `C:\Program Files\Java\jdk1.6.0_27`)
+1. Download and install the [Android SDK](http://developer.android.com/sdk/index.html). Make sure to install the Google APIs for your API level (e.g. 17), the Android SDK Build-tools version for your `buildToolsVersion` version, the Android Support Repository and the Google Repository.
+1. Set the `ANDROID_HOME` environmental variable to your Android SDK location.
+1. To start the app, run `adb shell am start -n com.joulespersecond.seattlebusbot/org.onebusaway.android.ui.HomeActivity` (alternately, you can manually start the app)
+
+### Release builds
+
+To build a release build, you need to create a `gradle.properties` file that points to a `secure.properties` file, and a `secure.properties` file that points to your keystore and alias.
+
+The `gradle.properties` file is located in the onebusaway-android directory and has the contents:
+```
+secure.properties=
+```
+
+The `secure.properties` file (in the location specified in gradle.properties) has the contents:
+```
+key.store=
+key.alias=
+```
+
+Note that the paths in these files always use the Unix path separator `/`, even on Windows. If you use the Windows path separator `\` you will get the error `No value has been specified for property 'signingConfig.keyAlias'.`
+
+### Deploying Cycle Atlanta in Your City
+
+1. Set up your own server and database. See [this page](https://github.com/CUTR-at-USF/cycleatlanta.org/tree/regions) to setup server instructuions.
+2. Add your region specs to [this spreadsheet](https://docs.google.com/spreadsheets/d/1g9ROmJh-jhQxU_YfxeovIfAx9EAb3MEvpROx8Aa1-u4/edit#gid=0).
+
diff --git a/apk/Cycle Atlanta.apk b/apk/Cycle Atlanta.apk
new file mode 100644
index 0000000..6d8f9f6
Binary files /dev/null and b/apk/Cycle Atlanta.apk differ
diff --git a/build.gradle b/build.gradle
index 6172a58..cdba6a4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,34 @@
apply plugin: 'android'
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.1.2'
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ }
+}
+
dependencies {
- compile fileTree(dir: 'libs', include: '*.jar')
- compile project(':google-play-services_lib')
+ compile 'com.google.android.gms:play-services-maps:8.3.0'
+ compile 'com.fasterxml.jackson.core:jackson-databind:2.6.2'
+ compile 'com.google.android.gms:play-services-location:8.3.0'
}
android {
- compileSdkVersion 14
- buildToolsVersion "18.1.1"
+ compileSdkVersion 19
+ buildToolsVersion "21.0.0"
+
+ packagingOptions {
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/NOTICE'
+ }
sourceSets {
main {
@@ -32,4 +53,56 @@ android {
debug.setRoot('build-types/debug')
release.setRoot('build-types/release')
}
+
+ buildTypes {
+ debug {
+ buildConfigField "boolean", "USE_FIXED_REGION", "false" // Supports multi-region
+ // Below fields need to be defined so flavor builds, but will not be used by app
+ //buildConfigField "String", "DATABASE_AUTHORITY", "\"edu.gatech.ppl.cycleatlanta\""
+ manifestPlaceholders = [databaseAuthority: "edu.gatech.ppl.cycleatlanta"]
+ buildConfigField "String", "FIXED_REGION_NAME", "null"
+ buildConfigField "String", "FIXED_REGION_OBA_BASE_URL", "null"
+ buildConfigField "String", "FIXED_REGION_SIRI_BASE_URL", "null"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LAT", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LON", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LAT_SPAN", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LON_SPAN", "0"
+ buildConfigField "String", "FIXED_REGION_LANG", "null"
+ buildConfigField "String", "FIXED_REGION_CONTACT_EMAIL", "null"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_OBA_DISCOVERY_APIS", "true"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_OBA_REALTIME_APIS", "true"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_SIRI_REALTIME_APIS", "false"
+ buildConfigField "String", "FIXED_REGION_TWITTER_URL", "null"
+ buildConfigField "String", "FIXED_REGION_STOP_INFO_URL", "null"
+ }
+
+ release {
+// buildConfigField "String", "DATABASE_AUTHORITY", "\"edu.gatech.ppl.cycleatlanta\""
+ manifestPlaceholders = [databaseAuthority: "edu.gatech.ppl.cycleatlanta"]
+ buildConfigField "boolean", "USE_FIXED_REGION", "false" // Supports multi-region
+ // Below fields need to be defined so flavor builds, but will not be used by app
+ buildConfigField "String", "FIXED_REGION_NAME", "null"
+ buildConfigField "String", "FIXED_REGION_OBA_BASE_URL", "null"
+ buildConfigField "String", "FIXED_REGION_SIRI_BASE_URL", "null"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LAT", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LON", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LAT_SPAN", "0"
+ buildConfigField "double", "FIXED_REGION_BOUNDS_LON_SPAN", "0"
+ buildConfigField "String", "FIXED_REGION_LANG", "null"
+ buildConfigField "String", "FIXED_REGION_CONTACT_EMAIL", "null"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_OBA_DISCOVERY_APIS", "true"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_OBA_REALTIME_APIS", "true"
+ buildConfigField "boolean", "FIXED_REGION_SUPPORTS_SIRI_REALTIME_APIS", "false"
+ buildConfigField "String", "FIXED_REGION_TWITTER_URL", "null"
+ buildConfigField "String", "FIXED_REGION_STOP_INFO_URL", "null"
+ }
+ }
+
+ applicationVariants.all {
+ variant ->
+ def authority;
+ authority = '"' + "edu.gatech.ppl.cycleatlanta" + '"'
+ // Must keep the original OBA authority
+ variant.buildConfigField "String", "DATABASE_AUTHORITY", authority
+ }
}
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/res/layout/activity_user_info.xml b/res/layout/activity_user_info.xml
index f050ac0..b088562 100755
--- a/res/layout/activity_user_info.xml
+++ b/res/layout/activity_user_info.xml
@@ -38,12 +38,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/buttonGetStarted"
- android:layout_below="@+id/buttonGetStarted"
+ android:layout_below="@+id/textView11"
android:layout_marginLeft="10dp"
- android:layout_marginTop="10dp"
+ android:layout_marginTop="40dp"
android:text="Tell us about yourself"
android:textAppearance="?android:attr/textAppearanceSmall" />
+
+
+
+
+
+
300150
+ 8298000
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cb4cf58..2b65efa 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -92,4 +92,20 @@
Cycle Atlanta
+
+ https://script.google.com/macros/s/AKfycbxpN47XZQGAoh-N5wQtBETp51tznG3JnOrWsAVNy0xGJOkD8ibS/exec
+
+ Choose region
+ preference_last_region_update
+ preference_auto_select_region
+ preferences_oba_api_url
+ preference_region
+ Finding your transit services…
+ http://
+ preference_experimental_regions
+ Found %1$s region
+ Set Custom Api Server
+ Custom Api Server
+ Invalid URL. Please enter a working server url.
+
\ No newline at end of file
diff --git a/src/edu/gatech/ppl/cycleatlanta/Application.java b/src/edu/gatech/ppl/cycleatlanta/Application.java
new file mode 100644
index 0000000..bdff5c0
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/Application.java
@@ -0,0 +1,265 @@
+package edu.gatech.ppl.cycleatlanta;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.api.GoogleApiClient;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.hardware.GeomagneticField;
+import android.location.Location;
+import android.location.LocationManager;
+import android.preference.PreferenceManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import edu.gatech.ppl.cycleatlanta.provider.ObaContract;
+import edu.gatech.ppl.cycleatlanta.region.ObaApi;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.utils.LocationUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.PreferenceUtils;
+
+import static com.google.android.gms.location.LocationServices.FusedLocationApi;
+
+public class Application extends android.app.Application{
+
+ public static final String APP_UID = "app_uid";
+
+ // Region preference (long id)
+ private static final String TAG = "Application";
+
+ private SharedPreferences mPrefs;
+
+ private static Application mApp;
+
+ private static final String HEXES = "0123456789abcdef";
+
+ // Magnetic declination is based on location, so track this centrally too.
+ static GeomagneticField mGeomagneticField = null;
+
+ /**
+ * We centralize location tracking in the Application class to allow all objects to make
+ * use of the last known location that we've seen. This is more reliable than using the
+ * getLastKnownLocation() method of the location providers, and allows us to track both
+ * Location
+ * API v1 and fused provider. It allows us to avoid strange behavior like animating a mMap view
+ * change when opening a new Activity, even when the previous Activity had a current location.
+ */
+ private static Location mLastKnownLocation = null;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mApp = this;
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ initObaRegion();
+ }
+
+ public static Application get() {
+ return mApp;
+ }
+
+ public synchronized ObaRegion getCurrentRegion() {
+ return ObaApi.getDefaultContext().getRegion();
+ }
+
+ public synchronized void setCurrentRegion(ObaRegion region) {
+ if (region != null) {
+ // First set it in preferences, then set it in OBA.
+ ObaApi.getDefaultContext().setRegion(region);
+ PreferenceUtils
+ .saveLong(mPrefs, getString(R.string.preference_key_region), region.getId());
+ //We're using a region, so clear the custom API URL preference
+ setCustomApiUrl(null);
+ } else {
+ //User must have just entered a custom API URL via Preferences, so clear the region info
+ ObaApi.getDefaultContext().setRegion(null);
+ PreferenceUtils.saveLong(mPrefs, getString(R.string.preference_key_region), -1);
+ }
+ }
+
+ /**
+ * Gets the date at which the region information was last updated, in the number of
+ * milliseconds
+ * since January 1, 1970, 00:00:00 GMT
+ * Default value is 0 if the region info has never been updated.
+ *
+ * @return the date at which the region information was last updated, in the number of
+ * milliseconds since January 1, 1970, 00:00:00 GMT. Default value is 0 if the region info has
+ * never been updated.
+ */
+ public long getLastRegionUpdateDate() {
+ SharedPreferences preferences = getPrefs();
+ return preferences.getLong(getString(R.string.preference_key_last_region_update), 0);
+ }
+
+ /**
+ * Sets the date at which the region information was last updated
+ *
+ * @param date the date at which the region information was last updated, in the number of
+ * milliseconds since January 1, 1970, 00:00:00 GMT
+ */
+ public void setLastRegionUpdateDate(long date) {
+ PreferenceUtils
+ .saveLong(mPrefs, getString(R.string.preference_key_last_region_update), date);
+ }
+
+ /**
+ * Returns the custom URL if the user has set a custom API URL manually via Preferences, or
+ * null
+ * if it has not been set
+ *
+ * @return the custom URL if the user has set a custom API URL manually via Preferences, or null
+ * if it has not been set
+ */
+ public String getCustomApiUrl() {
+ SharedPreferences preferences = getPrefs();
+ return preferences.getString(getString(R.string.preference_key_oba_api_url), null);
+ }
+
+ /**
+ * Sets the custom URL used to reach a OBA REST API server that is not available via the
+ * Regions
+ * REST API
+ *
+ * @param url the custom URL
+ */
+ public void setCustomApiUrl(String url) {
+ PreferenceUtils.saveString(getString(R.string.preference_key_oba_api_url), url);
+ }
+
+ public static SharedPreferences getPrefs() {
+ return get().mPrefs;
+ }
+
+ /**
+ * Returns the last known location that the application has seen, or null if we haven't seen a
+ * location yet. When trying to get a most recent location in one shot, this method should
+ * always be called.
+ *
+ * @param cxt The Context being used, or null if one isn't available
+ * @param client The GoogleApiClient being used to obtain fused provider updates, or null if
+ * one
+ * isn't available
+ * @return the last known location that the application has seen, or null if we haven't seen a
+ * location yet
+ */
+ public static synchronized Location getLastKnownLocation(Context cxt, GoogleApiClient client) {
+ if (mLastKnownLocation == null) {
+ // Try to get a last known location from the location providers
+ mLastKnownLocation = getLocation2(cxt, client);
+ }
+ // Pass back last known saved location, hopefully from past location listener updates
+ return mLastKnownLocation;
+ }
+
+ private static Location getLocation2(Context cxt, GoogleApiClient client) {
+ Location playServices = null;
+ if (client != null &&
+ cxt != null &&
+ GooglePlayServicesUtil.isGooglePlayServicesAvailable(cxt)
+ == ConnectionResult.SUCCESS
+ && client.isConnected()) {
+ playServices = FusedLocationApi.getLastLocation(client);
+ Log.d(TAG, "Got location from Google Play Services, testing against API v1...");
+ }
+ Location apiV1 = getLocationApiV1(cxt);
+
+ if (LocationUtils.compareLocationsByTime(playServices, apiV1)) {
+ Log.d(TAG, "Using location from Google Play Services");
+ return playServices;
+ } else {
+ Log.d(TAG, "Using location from Location API v1");
+ return apiV1;
+ }
+ }
+
+ /**
+ * Sets the last known location observed by the application via an instance of LocationHelper
+ *
+ * @param l a location received by a LocationHelper instance
+ */
+ public static synchronized void setLastKnownLocation(Location l) {
+ // If the new location is better than the old one, save it
+ if (LocationUtils.compareLocations(l, mLastKnownLocation)) {
+ if (mLastKnownLocation == null) {
+ mLastKnownLocation = new Location("Last known location");
+ }
+ mLastKnownLocation.set(l);
+ mGeomagneticField = new GeomagneticField(
+ (float) l.getLatitude(),
+ (float) l.getLongitude(),
+ (float) l.getAltitude(),
+ System.currentTimeMillis());
+ // Log.d(TAG, "Newest best location: " + mLastKnownLocation.toString());
+ }
+ }
+
+ private static Location getLocationApiV1(Context cxt) {
+ if (cxt == null) {
+ return null;
+ }
+ LocationManager mgr = (LocationManager) cxt.getSystemService(Context.LOCATION_SERVICE);
+ List providers = mgr.getProviders(true);
+ Location last = null;
+ for (Iterator i = providers.iterator(); i.hasNext(); ) {
+ Location loc = mgr.getLastKnownLocation(i.next());
+ // If this provider has a last location, and either:
+ // 1. We don't have a last location,
+ // 2. Our last location is older than this location.
+ if (LocationUtils.compareLocationsByTime(loc, last)) {
+ last = loc;
+ }
+ }
+ return last;
+ }
+
+ private String getAppUid() {
+ try {
+ final TelephonyManager telephony =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+ final String id = telephony.getDeviceId();
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+ digest.update(id.getBytes());
+ return getHex(digest.digest());
+ } catch (Exception e) {
+ return UUID.randomUUID().toString();
+ }
+ }
+
+ public static String getHex(byte[] raw) {
+ final StringBuilder hex = new StringBuilder(2 * raw.length);
+ for (byte b : raw) {
+ hex.append(HEXES.charAt((b & 0xF0) >> 4))
+ .append(HEXES.charAt((b & 0x0F)));
+ }
+ return hex.toString();
+ }
+
+ private void initObaRegion() {
+ // Read the region preference, look it up in the DB, then set the region.
+ long id = mPrefs.getLong(getString(R.string.preference_key_region), -1);
+ if (id < 0) {
+ Log.d(TAG, "Regions preference ID is less than 0, returning...");
+ return;
+ }
+
+ ObaRegion region = ObaContract.Regions.get(this, (int) id);
+ if (region == null) {
+ Log.d(TAG, "Regions preference is null, returning...");
+ return;
+ }
+
+
+ ObaApi.getDefaultContext().setRegion(region);
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/FragmentMainInput.java b/src/edu/gatech/ppl/cycleatlanta/FragmentMainInput.java
index 9d8f76d..486ed6c 100755
--- a/src/edu/gatech/ppl/cycleatlanta/FragmentMainInput.java
+++ b/src/edu/gatech/ppl/cycleatlanta/FragmentMainInput.java
@@ -1,9 +1,17 @@
package edu.gatech.ppl.cycleatlanta;
-import java.text.SimpleDateFormat;
-import java.util.TimeZone;
-import java.util.Timer;
-import java.util.TimerTask;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.GoogleMap.OnMyLocationButtonClickListener;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.UiSettings;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
import android.app.AlertDialog;
import android.content.ComponentName;
@@ -11,13 +19,19 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.Settings;
import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,624 +40,642 @@
import android.widget.TextView;
import android.widget.Toast;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
-import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
-import com.google.android.gms.location.LocationClient;
-import com.google.android.gms.location.LocationListener;
-import com.google.android.gms.location.LocationRequest;
-import com.google.android.gms.maps.CameraUpdateFactory;
-import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.GoogleMap.OnMyLocationButtonClickListener;
-import com.google.android.gms.maps.SupportMapFragment;
-import com.google.android.gms.maps.UiSettings;
-import com.google.android.gms.maps.model.LatLng;
-
-public class FragmentMainInput extends Fragment implements ConnectionCallbacks,
- OnConnectionFailedListener, LocationListener,
- OnMyLocationButtonClickListener {
-
- public static final String ARG_SECTION_NUMBER = "section_number";
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.Timer;
+import java.util.TimerTask;
- Intent fi;
- TripData trip;
- NoteData note;
- boolean isRecording = false;
- Timer timer;
- float curDistance;
-
- TextView txtDuration;
- TextView txtDistance;
- TextView txtCurSpeed;
-
- int zoomFlag = 1;
-
- Location currentLocation = new Location("");
-
- final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
-
- // Need handler for callbacks to the UI thread
- final Handler mHandler = new Handler();
- final Runnable mUpdateTimer = new Runnable() {
- public void run() {
- updateTimer();
- }
- };
-
- private final static int MENU_USER_INFO = 0;
- private final static int MENU_HELP = 1;
-
- private final static int CONTEXT_RETRY = 0;
- private final static int CONTEXT_DELETE = 1;
-
- DbAdapter mDb;
- GoogleMap map;
- UiSettings mUiSettings;
- private LocationClient mLocationClient;
-
- private static final LocationRequest REQUEST = LocationRequest.create()
- .setInterval(5000) // 5 seconds
- .setFastestInterval(16) // 16ms = 60fps
- .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
-
- public FragmentMainInput() {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Log.v("Jason", "Cycle: MainInput onCreateView");
-
- // Toast.makeText(getActivity(), "Record Created",
- // Toast.LENGTH_LONG).show();
-
- View rootView = inflater.inflate(R.layout.activity_main_input,
- container, false);
- setUpMapIfNeeded();
-
- // LatLng myLocation = new
- // LatLng(mLocationClient.getLastLocation().getLatitude(),
- // mLocationClient.getLastLocation().getLongitude());
- // map.moveCamera(CameraUpdateFactory.newLatLngZoom(myLocation, 13));
-
- // map.moveCamera(CameraUpdateFactory.newLatLngZoom(atlanta, 13));
-
- // map = ((SupportMapFragment)
- // getActivity().getSupportFragmentManager().findFragmentById(R.id.map)).getMap();
-
- // LatLng atlanta = new LatLng(33.749038, -84.388068);
-
- // map.setMyLocationEnabled(true);
- // map.moveCamera(CameraUpdateFactory.newLatLngZoom(atlanta, 13));
-
- // Log.d("Jason", "Start");
-
- // Hide action bar title on Main Screen
- // getActivity().getActionBar().setDisplayShowTitleEnabled(true);
- // getActivity().getActionBar().setDisplayShowHomeEnabled(true);
-
- Intent rService = new Intent(getActivity(), RecordingService.class);
- ServiceConnection sc = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName name) {
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- IRecordService rs = (IRecordService) service;
- int state = rs.getState();
- if (state > RecordingService.STATE_IDLE) {
- if (state == RecordingService.STATE_FULL) {
- startActivity(new Intent(getActivity(),
- TripPurposeActivity.class));
- } else { // RECORDING OR PAUSED:
- // startActivity(new Intent(MainInput.this,
- // RecordingActivity.class));
- }
- getActivity().finish();
- } else {
- // Idle. First run? Switch to user prefs screen if there are
- // no prefs stored yet
- // SharedPreferences settings =
- // getSharedPreferences("PREFS", 0);
- // if (settings.getAll().isEmpty()) {
- // showWelcomeDialog();
- // }
- // // Not first run - set up the list view of saved trips
- // ListView listSavedTrips = (ListView)
- // findViewById(R.id.ListSavedTrips);
- // populateList(listSavedTrips);
- }
- getActivity().unbindService(this); // race? this says
- // we no longer care
- }
- };
- // This needs to block until the onServiceConnected (above) completes.
- // Thus, we can check the recording status before continuing on.
- getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
-
- // Log.d("Jason", "Start2");
-
- // And set up the record button
- Button startButton = (Button) rootView.findViewById(R.id.buttonStart);
- startButton.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- if (isRecording == false) {
- // Before we go to record, check GPS status
- final LocationManager manager = (LocationManager) getActivity()
- .getSystemService(Context.LOCATION_SERVICE);
- if (!manager
- .isProviderEnabled(LocationManager.GPS_PROVIDER)) {
- buildAlertMessageNoGps();
- } else {
- // startActivity(i);
- // call function in Recording Activity
- // Toast.makeText(getApplicationContext(),
- // "Start Clicked",Toast.LENGTH_LONG).show();
- startRecording();
- // MainInputActivity.this.finish();
- }
- } else if (isRecording == true) {
- // pop up: save, discard, cancel
- buildAlertMessageSaveClicked();
- }
- }
- });
-
- Button noteThisButton = (Button) rootView
- .findViewById(R.id.buttonNoteThis);
- noteThisButton.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- final LocationManager manager = (LocationManager) getActivity()
- .getSystemService(Context.LOCATION_SERVICE);
- if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
- buildAlertMessageNoGps();
- } else {
- fi = new Intent(getActivity(), NoteTypeActivity.class);
- // update note entity
- note = NoteData.createNote(getActivity());
-
- fi.putExtra("noteid", note.noteid);
-
- Log.v("Jason", "Note ID in MainInput: " + note.noteid);
-
- if (isRecording == true) {
- fi.putExtra("isRecording", 1);
- } else {
- fi.putExtra("isRecording", 0);
- }
-
- note.updateNoteStatus(NoteData.STATUS_INCOMPLETE);
-
- double currentTime = System.currentTimeMillis();
-
- if (currentLocation != null) {
- note.addPointNow(currentLocation, currentTime);
-
- // Log.v("Jason", "Note ID: "+note);
-
- startActivity(fi);
- getActivity().overridePendingTransition(
- R.anim.slide_in_right, R.anim.slide_out_left);
- // getActivity().finish();
- } else {
- Toast.makeText(getActivity(),
- "No GPS data acquired; nothing to submit.",
- Toast.LENGTH_SHORT).show();
- }
- }
- }
- });
-
- // copy from Recording Activity
- txtDuration = (TextView) rootView
- .findViewById(R.id.textViewElapsedTime);
- txtDistance = (TextView) rootView.findViewById(R.id.textViewDistance);
- txtCurSpeed = (TextView) rootView.findViewById(R.id.textViewSpeed);
-
- sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
-
- return rootView;
- }
-
- // @Override
- // public View onCreateView(LayoutInflater inflater, ViewGroup container,
- // Bundle savedInstanceState) {
- // View rootView = inflater.inflate(
- // R.layout.activity_main_input, container, false);
- // return rootView;
- // }
-
- public void updateStatus(int points, float distance, float spdCurrent,
- float spdMax) {
- this.curDistance = distance;
-
- // fix GPS Issue to ensure this
- // // TODO: check task status before doing this?
- // if (points > 0) {
- // txtStat.setText("" + points + " data points received...");
- // } else {
- // txtStat.setText("Waiting for GPS fix...");
- // }
-
- txtCurSpeed.setText(String.format("%1.1f mph", spdCurrent));
-
- float miles = 0.0006212f * distance;
- txtDistance.setText(String.format("%1.1f miles", miles));
- }
-
- void cancelRecording() {
- final Button startButton = (Button) getActivity().findViewById(
- R.id.buttonStart);
- startButton.setText("Start");
- // startButton.setBackgroundColor(0x4d7d36);
- Intent rService = new Intent(getActivity(), RecordingService.class);
- ServiceConnection sc = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName name) {
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- IRecordService rs = (IRecordService) service;
- rs.cancelRecording();
- getActivity().unbindService(this);
- }
- };
- // This should block until the onServiceConnected (above) completes.
- getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
-
- isRecording = false;
-
- txtDuration = (TextView) getActivity().findViewById(
- R.id.textViewElapsedTime);
- txtDuration.setText("00:00:00");
- txtDistance = (TextView) getActivity().findViewById(
- R.id.textViewDistance);
- txtDistance.setText("0.0 miles");
-
- txtCurSpeed = (TextView) getActivity().findViewById(R.id.textViewSpeed);
- txtCurSpeed.setText("0.0 mph");
- }
-
- void startRecording() {
- // Query the RecordingService to figure out what to do.
- final Button startButton = (Button) getActivity().findViewById(
- R.id.buttonStart);
- Intent rService = new Intent(getActivity(), RecordingService.class);
- getActivity().startService(rService);
- ServiceConnection sc = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName name) {
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- IRecordService rs = (IRecordService) service;
-
- switch (rs.getState()) {
- case RecordingService.STATE_IDLE:
- trip = TripData.createTrip(getActivity());
- rs.startRecording(trip);
- isRecording = true;
- startButton.setText("Save");
- // startButton.setBackgroundColor(0xFF0000);
- // MainInputActivity.this.pauseButton.setEnabled(true);
- // MainInputActivity.this
- // .setTitle("Cycle Atlanta - Recording...");
- break;
- case RecordingService.STATE_RECORDING:
- long id = rs.getCurrentTrip();
- trip = TripData.fetchTrip(getActivity(), id);
- isRecording = true;
- startButton.setText("Save");
- // startButton.setBackgroundColor(0xFF0000);
- // MainInputActivity.this.pauseButton.setEnabled(true);
- // MainInputActivity.this
- // .setTitle("Cycle Atlanta - Recording...");
- break;
- // case RecordingService.STATE_PAUSED:
- // long tid = rs.getCurrentTrip();
- // isRecording = false;
- // trip = TripData.fetchTrip(MainInputActivity.this, tid);
- // // MainInputActivity.this.pauseButton.setEnabled(true);
- // // MainInputActivity.this.pauseButton.setText("Resume");
- // // MainInputActivity.this
- // // .setTitle("Cycle Atlanta - Paused...");
- // break;
- case RecordingService.STATE_FULL:
- // Should never get here, right?
- break;
- }
- rs.setListener((FragmentMainInput) getActivity()
- .getSupportFragmentManager().findFragmentByTag(
- "android:switcher:" + R.id.pager + ":0"));
- getActivity().unbindService(this);
- }
- };
- getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
-
- isRecording = true;
- }
-
- private void buildAlertMessageNoGps() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(
- getActivity());
- builder.setMessage(
- "Your phone's GPS is disabled. Cycle Atlanta needs GPS to determine your location.\n\nGo to System Settings now to enable GPS?")
- .setCancelable(false)
- .setPositiveButton("GPS Settings...",
- new DialogInterface.OnClickListener() {
- public void onClick(final DialogInterface dialog,
- final int id) {
- final ComponentName toLaunch = new ComponentName(
- "com.android.settings",
- "com.android.settings.SecuritySettings");
- final Intent intent = new Intent(
- Settings.ACTION_LOCATION_SOURCE_SETTINGS);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- intent.setComponent(toLaunch);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivityForResult(intent, 0);
- }
- })
- .setNegativeButton("Cancel",
- new DialogInterface.OnClickListener() {
- public void onClick(final DialogInterface dialog,
- final int id) {
- dialog.cancel();
- }
- });
- final AlertDialog alert = builder.create();
- alert.show();
- }
-
- private void buildAlertMessageSaveClicked() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(
- getActivity());
- builder.setTitle("Save Trip");
- builder.setMessage("Do you want to save this trip?");
- builder.setNegativeButton("Save",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- // save
- // If we have points, go to the save-trip activity
- // trip.numpoints > 0
- if (trip.numpoints > 0) {
- // Handle pause time gracefully
- if (trip.pauseStartedAt > 0) {
- trip.totalPauseTime += (System
- .currentTimeMillis() - trip.pauseStartedAt);
- }
- if (trip.totalPauseTime > 0) {
- trip.endTime = System.currentTimeMillis()
- - trip.totalPauseTime;
- }
- // Save trip so far (points and extent, but no
- // purpose or
- // notes)
- fi = new Intent(getActivity(),
- TripPurposeActivity.class);
- trip.updateTrip("", "", "", "");
-
- startActivity(fi);
- getActivity().overridePendingTransition(
- R.anim.slide_in_right,
- R.anim.slide_out_left);
- getActivity().finish();
- }
- // Otherwise, cancel and go back to main screen
- else {
- Toast.makeText(getActivity(),
- "No GPS data acquired; nothing to submit.",
- Toast.LENGTH_SHORT).show();
-
- cancelRecording();
- }
- }
- });
-
- builder.setNeutralButton("Discard",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- // discard
- cancelRecording();
- }
- });
-
- builder.setPositiveButton("Cancel",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- // continue
- }
- });
- final AlertDialog alert = builder.create();
- alert.show();
- }
-
- void updateTimer() {
- if (trip != null && isRecording) {
- double dd = System.currentTimeMillis() - trip.startTime
- - trip.totalPauseTime;
-
- txtDuration.setText(sdf.format(dd));
-
- // double avgSpeed = 3600.0 * 0.6212 * this.curDistance / dd;
- // txtAvgSpeed.setText(String.format("%1.1f mph", avgSpeed));
- }
- }
-
- // onResume is called whenever this activity comes to foreground.
- // Use a timer to update the trip duration.
- @Override
- public void onResume() {
- super.onResume();
-
- Log.v("Jason", "Cycle: MainInput onResume");
-
- timer = new Timer();
- timer.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- mHandler.post(mUpdateTimer);
- }
- }, 0, 1000); // every second
-
- setUpMapIfNeeded();
- if (map != null) {
- // Keep the UI Settings state in sync with the checkboxes.
- mUiSettings.setZoomControlsEnabled(true);
- mUiSettings.setCompassEnabled(true);
- mUiSettings.setMyLocationButtonEnabled(true);
- map.setMyLocationEnabled(true);
- mUiSettings.setScrollGesturesEnabled(true);
- mUiSettings.setZoomGesturesEnabled(true);
- mUiSettings.setTiltGesturesEnabled(true);
- mUiSettings.setRotateGesturesEnabled(true);
- }
- setUpLocationClientIfNeeded();
- mLocationClient.connect();
- }
-
- // Don't do pointless UI updates if the activity isn't being shown.
- @Override
- public void onPause() {
- super.onPause();
- Log.v("Jason", "Cycle: MainInput onPause");
- // Background GPS.
- if (timer != null)
- timer.cancel();
- if (mLocationClient != null) {
- mLocationClient.disconnect();
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- Log.v("Jason", "Cycle: MainInput onDestroyView");
- // Toast.makeText(getActivity(), "Record Destroyed",
- // Toast.LENGTH_LONG).show();
- // Fragment fragment =
- // (getFragmentManager().findFragmentById(R.id.map));
- // FragmentTransaction ft = getActivity().getSupportFragmentManager()
- // .beginTransaction();
- // ft.remove(fragment);
- // ft.commit();
-
- // cancelRecording();
- }
-
- private void setUpMapIfNeeded() {
- // Do a null check to confirm that we have not already instantiated the
- // map.
- if (map == null) {
- // Try to obtain the map from the SupportMapFragment.
- map = ((SupportMapFragment) getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.map))
- .getMap();
- // Check if we were successful in obtaining the map.
- if (map != null) {
- map.setMyLocationEnabled(true);
- map.setOnMyLocationButtonClickListener(this);
- mUiSettings = map.getUiSettings();
- // centerMapOnMyLocation();
- }
- }
- }
-
- // private void centerMapOnMyLocation() {
- // // Toast.makeText(getActivity(), "Center", Toast.LENGTH_LONG).show();
- //
- // map.setMyLocationEnabled(true);
- //
- // LocationManager locationManager = (LocationManager) getActivity()
- // .getSystemService(Context.LOCATION_SERVICE);
- //
- // // Creating a criteria object to retrieve provider
- // Criteria criteria = new Criteria();
- //
- // // Getting the name of the best provider
- // String provider = locationManager.getBestProvider(criteria, true);
- //
- // // Getting Current Location
- // Location location = locationManager.getLastKnownLocation(provider);
- //
- // if (location != null) {
- // onLocationChanged(location);
- // }
- //
- // LatLng myLocation;
- //
- // if (location != null) {
- // myLocation = new LatLng(location.getLatitude(),
- // location.getLongitude());
- // map.animateCamera(CameraUpdateFactory.newLatLngZoom(myLocation, 16));
- // }
- // }
-
- private void setUpLocationClientIfNeeded() {
- if (mLocationClient == null) {
- mLocationClient = new LocationClient(getActivity(), this, // ConnectionCallbacks
- this); // OnConnectionFailedListener
- }
- }
-
- /**
- * Implementation of {@link LocationListener}.
- */
- @Override
- public void onLocationChanged(Location location) {
- // onMyLocationButtonClick();
- currentLocation = location;
-
- // Log.v("Jason", "Current Location: "+currentLocation);
-
- if (zoomFlag == 1) {
- LatLng myLocation;
-
- if (location != null) {
- myLocation = new LatLng(location.getLatitude(),
- location.getLongitude());
- map.animateCamera(CameraUpdateFactory.newLatLngZoom(myLocation,
- 16));
- zoomFlag = 0;
- }
- }
- }
-
- /**
- * Callback called when connected to GCore. Implementation of
- * {@link ConnectionCallbacks}.
- */
- @Override
- public void onConnected(Bundle connectionHint) {
- mLocationClient.requestLocationUpdates(REQUEST, this); // LocationListener
- }
-
- /**
- * Callback called when disconnected from GCore. Implementation of
- * {@link ConnectionCallbacks}.
- */
- @Override
- public void onDisconnected() {
- // Do nothing
- }
-
- /**
- * Implementation of {@link OnConnectionFailedListener}.
- */
- @Override
- public void onConnectionFailed(ConnectionResult result) {
- // Do nothing
- }
-
- @Override
- public boolean onMyLocationButtonClick() {
- // Toast.makeText(getActivity(), "MyLocation button clicked",
- // Toast.LENGTH_SHORT).show();
- // Return false so that we don't consume the event and the default
- // behavior still occurs
- // (the camera animates to the user's current position).
- return false;
- }
+import edu.gatech.ppl.cycleatlanta.region.ObaRegionsTask;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.utils.LocationHelper;
+import edu.gatech.ppl.cycleatlanta.region.utils.LocationUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.MapHelpV2;
+import edu.gatech.ppl.cycleatlanta.region.utils.PreferenceUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.RegionUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.UIUtils;
+
+public class FragmentMainInput extends Fragment implements
+ OnMyLocationButtonClickListener, LocationHelper.Listener,
+ ObaRegionsTask.Callback {
+
+ public static final String ARG_SECTION_NUMBER = "section_number";
+
+ private static final String TAG = "FragmentMainInput";
+
+ Intent fi;
+ TripData trip;
+ NoteData note;
+ boolean isRecording = false;
+ Timer timer;
+ float curDistance;
+
+ TextView txtDuration;
+ TextView txtDistance;
+ TextView txtCurSpeed;
+
+ LocationHelper mLocationHelper;
+
+ int zoomFlag = 1;
+
+ Location currentLocation = new Location("");
+
+ final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
+
+ // Need handler for callbacks to the UI thread
+ final Handler mHandler = new Handler();
+ final Runnable mUpdateTimer = new Runnable() {
+ public void run() {
+ updateTimer();
+ }
+ };
+
+ private static final long REGION_UPDATE_THRESHOLD = 1000 * 60 * 60 * 24 * 7;
+
+ private static final String CHECK_REGION_VER = "checkRegionVer";
+
+ GoogleMap mMap;
+ UiSettings mUiSettings;
+ protected GoogleApiClient mGoogleApiClient;
+
+ private static final LocationRequest REQUEST = LocationRequest.create()
+ .setInterval(5000) // 5 seconds
+ .setFastestInterval(16) // 16ms = 60fps
+ .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+
+ public FragmentMainInput() {
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Make sure GoogleApiClient is connected, if available
+ if (mGoogleApiClient != null && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ // Tear down GoogleApiClient
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setupGooglePlayServices();
+
+ mLocationHelper = new LocationHelper(getActivity());
+ mLocationHelper.registerListener(this);
+
+ checkRegionStatus();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ View rootView = inflater.inflate(R.layout.activity_main_input,
+ container, false);
+ setUpMapIfNeeded();
+
+ Intent rService = new Intent(getActivity(), RecordingService.class);
+ ServiceConnection sc = new ServiceConnection() {
+ public void onServiceDisconnected(ComponentName name) {
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IRecordService rs = (IRecordService) service;
+ int state = rs.getState();
+ if (state > RecordingService.STATE_IDLE) {
+ if (state == RecordingService.STATE_FULL) {
+ startActivity(new Intent(getActivity(),
+ TripPurposeActivity.class));
+ }
+
+ getActivity().finish();
+ }
+ getActivity().unbindService(this); // race? this says
+ // we no longer care
+ }
+ };
+ // This needs to block until the onServiceConnected (above) completes.
+ // Thus, we can check the recording status before continuing on.
+ getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
+
+ // Log.d("Jason", "Start2");
+
+ // And set up the record button
+ Button startButton = (Button) rootView.findViewById(R.id.buttonStart);
+ startButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ if (isRecording == false) {
+ // Before we go to record, check GPS status
+ final LocationManager manager = (LocationManager) getActivity()
+ .getSystemService(Context.LOCATION_SERVICE);
+ if (!manager
+ .isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+ buildAlertMessageNoGps();
+ } else {
+ // startActivity(i);
+ // call function in Recording Activity
+ // Toast.makeText(getApplicationContext(),
+ // "Start Clicked",Toast.LENGTH_LONG).show();
+ startRecording();
+ // MainInputActivity.this.finish();
+ }
+ } else if (isRecording == true) {
+ // pop up: save, discard, cancel
+ buildAlertMessageSaveClicked();
+ }
+ }
+ });
+
+ Button noteThisButton = (Button) rootView
+ .findViewById(R.id.buttonNoteThis);
+ noteThisButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ final LocationManager manager = (LocationManager) getActivity()
+ .getSystemService(Context.LOCATION_SERVICE);
+ if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+ buildAlertMessageNoGps();
+ } else {
+ fi = new Intent(getActivity(), NoteTypeActivity.class);
+ // update note entity
+ note = NoteData.createNote(getActivity());
+
+ fi.putExtra("noteid", note.noteid);
+
+ Log.v("Jason", "Note ID in MainInput: " + note.noteid);
+
+ if (isRecording == true) {
+ fi.putExtra("isRecording", 1);
+ } else {
+ fi.putExtra("isRecording", 0);
+ }
+
+ note.updateNoteStatus(NoteData.STATUS_INCOMPLETE);
+
+ double currentTime = System.currentTimeMillis();
+
+ if (currentLocation != null) {
+ note.addPointNow(currentLocation, currentTime);
+
+ // Log.v("Jason", "Note ID: "+note);
+
+ startActivity(fi);
+ getActivity().overridePendingTransition(
+ R.anim.slide_in_right, R.anim.slide_out_left);
+ // getActivity().finish();
+ } else {
+ Toast.makeText(getActivity(),
+ "No GPS data acquired; nothing to submit.",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ });
+
+ // copy from Recording Activity
+ txtDuration = (TextView) rootView
+ .findViewById(R.id.textViewElapsedTime);
+ txtDistance = (TextView) rootView.findViewById(R.id.textViewDistance);
+ txtCurSpeed = (TextView) rootView.findViewById(R.id.textViewSpeed);
+
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ return rootView;
+ }
+
+ private void checkRegionStatus() {
+ //First check for custom API URL set by user via Preferences, since if that is set we don't need region info from the REST API
+ if (!TextUtils.isEmpty(Application.get().getCustomApiUrl())) {
+ return;
+ }
+
+ // Check if region is hard-coded for this build flavor
+ if (BuildConfig.USE_FIXED_REGION) {
+ ObaRegion r = RegionUtils.getRegionFromBuildFlavor();
+ // Set the hard-coded region
+ RegionUtils.saveToProvider(getActivity(), Collections.singletonList(r));
+ Application.get().setCurrentRegion(r);
+ // Disable any region auto-selection in preferences
+ PreferenceUtils
+ .saveBoolean(getString(R.string.preference_key_auto_select_region), false);
+ return;
+ }
+
+ boolean forceReload = false;
+ boolean showProgressDialog = true;
+
+ //If we don't have region info selected, or if enough time has passed since last region info update,
+ //force contacting the server again
+ if (Application.get().getCurrentRegion() == null ||
+ new Date().getTime() - Application.get().getLastRegionUpdateDate()
+ > REGION_UPDATE_THRESHOLD) {
+ forceReload = true;
+ Log.d(TAG,
+ "Region info has expired (or does not exist), forcing a reload from the server...");
+ }
+
+ if (Application.get().getCurrentRegion() != null) {
+ //We already have region info locally, so just check current region status quietly in the background
+ showProgressDialog = false;
+ }
+
+ try {
+ PackageInfo appInfo = getActivity().getPackageManager().getPackageInfo(
+ getActivity().getPackageName(), PackageManager.GET_META_DATA);
+ SharedPreferences settings = Application.getPrefs();
+ final int oldVer = settings.getInt(CHECK_REGION_VER, 0);
+ final int newVer = appInfo.versionCode;
+
+ if (oldVer < newVer) {
+ forceReload = true;
+ }
+ PreferenceUtils.saveInt(CHECK_REGION_VER, appInfo.versionCode);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Do nothing
+ }
+
+ //Check region status, possibly forcing a reload from server and checking proximity to current region
+ ObaRegionsTask task = new ObaRegionsTask(getActivity(), this, forceReload,
+ showProgressDialog);
+ task.execute();
+ }
+
+ private void setupGooglePlayServices() {
+ // Init Google Play Services as early as possible in the Fragment lifecycle to give it time
+ if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(getActivity())
+ == ConnectionResult.SUCCESS) {
+ mGoogleApiClient = LocationUtils.getGoogleApiClientWithCallbacks(getActivity());
+ mGoogleApiClient.connect();
+ }
+ }
+
+ public void updateStatus(int points, float distance, float spdCurrent,
+ float spdMax) {
+ this.curDistance = distance;
+
+ txtCurSpeed.setText(String.format("%1.1f mph", spdCurrent));
+
+ float miles = 0.0006212f * distance;
+ txtDistance.setText(String.format("%1.1f miles", miles));
+ }
+
+ void cancelRecording() {
+ final Button startButton = (Button) getActivity().findViewById(
+ R.id.buttonStart);
+ startButton.setText("Start");
+ // startButton.setBackgroundColor(0x4d7d36);
+ Intent rService = new Intent(getActivity(), RecordingService.class);
+ ServiceConnection sc = new ServiceConnection() {
+ public void onServiceDisconnected(ComponentName name) {
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IRecordService rs = (IRecordService) service;
+ rs.cancelRecording();
+ getActivity().unbindService(this);
+ }
+ };
+ // This should block until the onServiceConnected (above) completes.
+ getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
+
+ isRecording = false;
+
+ txtDuration = (TextView) getActivity().findViewById(
+ R.id.textViewElapsedTime);
+ txtDuration.setText("00:00:00");
+ txtDistance = (TextView) getActivity().findViewById(
+ R.id.textViewDistance);
+ txtDistance.setText("0.0 miles");
+
+ txtCurSpeed = (TextView) getActivity().findViewById(R.id.textViewSpeed);
+ txtCurSpeed.setText("0.0 mph");
+ }
+
+ void startRecording() {
+ // Query the RecordingService to figure out what to do.
+ final Button startButton = (Button) getActivity().findViewById(
+ R.id.buttonStart);
+ Intent rService = new Intent(getActivity(), RecordingService.class);
+ getActivity().startService(rService);
+ ServiceConnection sc = new ServiceConnection() {
+ public void onServiceDisconnected(ComponentName name) {
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IRecordService rs = (IRecordService) service;
+
+ switch (rs.getState()) {
+ case RecordingService.STATE_IDLE:
+ trip = TripData.createTrip(getActivity());
+ rs.startRecording(trip);
+ isRecording = true;
+ startButton.setText("Save");
+ // startButton.setBackgroundColor(0xFF0000);
+ // MainInputActivity.this.pauseButton.setEnabled(true);
+ // MainInputActivity.this
+ // .setTitle("Cycle Atlanta - Recording...");
+ break;
+ case RecordingService.STATE_RECORDING:
+ long id = rs.getCurrentTrip();
+ trip = TripData.fetchTrip(getActivity(), id);
+ isRecording = true;
+ startButton.setText("Save");
+ // startButton.setBackgroundColor(0xFF0000);
+ // MainInputActivity.this.pauseButton.setEnabled(true);
+ // MainInputActivity.this
+ // .setTitle("Cycle Atlanta - Recording...");
+ break;
+ // case RecordingService.STATE_PAUSED:
+ // long tid = rs.getCurrentTrip();
+ // isRecording = false;
+ // trip = TripData.fetchTrip(MainInputActivity.this, tid);
+ // // MainInputActivity.this.pauseButton.setEnabled(true);
+ // // MainInputActivity.this.pauseButton.setText("Resume");
+ // // MainInputActivity.this
+ // // .setTitle("Cycle Atlanta - Paused...");
+ // break;
+ case RecordingService.STATE_FULL:
+ // Should never get here, right?
+ break;
+ }
+ rs.setListener((FragmentMainInput) getActivity()
+ .getSupportFragmentManager().findFragmentByTag(
+ "android:switcher:" + R.id.pager + ":0"));
+ getActivity().unbindService(this);
+ }
+ };
+ getActivity().bindService(rService, sc, Context.BIND_AUTO_CREATE);
+
+ isRecording = true;
+ }
+
+ private void buildAlertMessageNoGps() {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(
+ getActivity());
+ builder.setMessage(
+ "Your phone's GPS is disabled. Cycle Atlanta needs GPS to determine your location.\n\nGo to System Settings now to enable GPS?")
+ .setCancelable(false)
+ .setPositiveButton("GPS Settings...",
+ new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog,
+ final int id) {
+ final ComponentName toLaunch = new ComponentName(
+ "com.android.settings",
+ "com.android.settings.SecuritySettings");
+ final Intent intent = new Intent(
+ Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setComponent(toLaunch);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivityForResult(intent, 0);
+ }
+ })
+ .setNegativeButton("Cancel",
+ new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog,
+ final int id) {
+ dialog.cancel();
+ }
+ });
+ final AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ private void buildAlertMessageSaveClicked() {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(
+ getActivity());
+ builder.setTitle("Save Trip");
+ builder.setMessage("Do you want to save this trip?");
+ builder.setNegativeButton("Save",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ // save
+ // If we have points, go to the save-trip activity
+ // trip.numpoints > 0
+ if (trip.numpoints > 0) {
+ // Handle pause time gracefully
+ if (trip.pauseStartedAt > 0) {
+ trip.totalPauseTime += (System
+ .currentTimeMillis() - trip.pauseStartedAt);
+ }
+ if (trip.totalPauseTime > 0) {
+ trip.endTime = System.currentTimeMillis()
+ - trip.totalPauseTime;
+ }
+ // Save trip so far (points and extent, but no
+ // purpose or
+ // notes)
+ fi = new Intent(getActivity(),
+ TripPurposeActivity.class);
+ trip.updateTrip("", "", "", "");
+
+ startActivity(fi);
+ getActivity().overridePendingTransition(
+ R.anim.slide_in_right,
+ R.anim.slide_out_left);
+ getActivity().finish();
+ }
+ // Otherwise, cancel and go back to main screen
+ else {
+ Toast.makeText(getActivity(),
+ "No GPS data acquired; nothing to submit.",
+ Toast.LENGTH_SHORT).show();
+
+ cancelRecording();
+ }
+ }
+ });
+
+ builder.setNeutralButton("Discard",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ // discard
+ cancelRecording();
+ }
+ });
+
+ builder.setPositiveButton("Cancel",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ // continue
+ }
+ });
+ final AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ void updateTimer() {
+ if (trip != null && isRecording) {
+ double dd = System.currentTimeMillis() - trip.startTime
+ - trip.totalPauseTime;
+
+ txtDuration.setText(sdf.format(dd));
+
+ }
+ }
+
+ // onResume is called whenever this activity comes to foreground.
+ // Use a timer to update the trip duration.
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ mLocationHelper.onResume();
+
+ Log.v("Jason", "Cycle: MainInput onResume");
+
+ timer = new Timer();
+ timer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ mHandler.post(mUpdateTimer);
+ }
+ }, 0, 1000); // every second
+
+ setUpMapIfNeeded();
+ if (mMap != null) {
+ // Keep the UI Settings state in sync with the checkboxes.
+ mUiSettings.setZoomControlsEnabled(true);
+ mUiSettings.setCompassEnabled(true);
+ mUiSettings.setMyLocationButtonEnabled(true);
+ mMap.setMyLocationEnabled(true);
+ mUiSettings.setScrollGesturesEnabled(true);
+ mUiSettings.setZoomGesturesEnabled(true);
+ mUiSettings.setTiltGesturesEnabled(true);
+ mUiSettings.setRotateGesturesEnabled(true);
+ }
+ }
+
+ // Don't do pointless UI updates if the activity isn't being shown.
+ @Override
+ public void onPause() {
+ super.onPause();
+ Log.v("Jason", "Cycle: MainInput onPause");
+ mLocationHelper.onPause();
+ // Background GPS.
+ if (timer != null)
+ timer.cancel();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ Log.v("Jason", "Cycle: MainInput onDestroyView");
+ }
+
+ private void setUpMapIfNeeded() {
+ // Do a null check to confirm that we have not already instantiated the
+ // mMap.
+ if (mMap == null) {
+ // Try to obtain the mMap from the SupportMapFragment.
+
+ mMap = getMapFragment().getMap();
+ // Check if we were successful in obtaining the mMap.
+ if (mMap != null) {
+ mMap.setMyLocationEnabled(true);
+ mMap.setOnMyLocationButtonClickListener(this);
+ mUiSettings = mMap.getUiSettings();
+ // centerMapOnMyLocation();
+ }
+ }
+ }
+
+ private SupportMapFragment getMapFragment() {
+ FragmentManager fm = null;
+
+ Log.d(TAG, "sdk: " + Build.VERSION.SDK_INT);
+ Log.d(TAG, "release: " + Build.VERSION.RELEASE);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ Log.d(TAG, "using getFragmentManager");
+ fm = getFragmentManager();
+ } else {
+ Log.d(TAG, "using getChildFragmentManager");
+ fm = getChildFragmentManager();
+ }
+
+ return (SupportMapFragment) fm.findFragmentById(R.id.map);
+ }
+
+ /**
+ * Implementation of {@link LocationListener}.
+ */
+ @Override
+ public void onLocationChanged(Location location) {
+ // onMyLocationButtonClick();
+ currentLocation = location;
+
+ // Log.v("Jason", "Current Location: "+currentLocation);
+
+ if (zoomFlag == 1) {
+ LatLng myLocation;
+
+ if (location != null) {
+ myLocation = new LatLng(location.getLatitude(),
+ location.getLongitude());
+ mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(myLocation,
+ 16));
+ zoomFlag = 0;
+ }
+ }
+ }
+
+ @Override
+ public boolean onMyLocationButtonClick() {
+ // Toast.makeText(getActivity(), "MyLocation button clicked",
+ // Toast.LENGTH_SHORT).show();
+ // Return false so that we don't consume the event and the default
+ // behavior still occurs
+ // (the camera animates to the user's current position).
+ return false;
+ }
+
+ @Override
+ public void onRegionTaskFinished(boolean currentRegionChanged) {
+ if (currentRegionChanged
+ && Application
+ .getLastKnownLocation(this.getActivity(), mLocationHelper.getGoogleApiClient())
+ == null) {
+ // Move mMap view after a new region has been selected, if we don't have user location
+ zoomToRegion();
+ }
+
+ // If region changed and was auto-selected, show user what region we're using
+ if (currentRegionChanged
+ && Application.getPrefs()
+ .getBoolean(getString(R.string.preference_key_auto_select_region), true)
+ && Application.get().getCurrentRegion() != null
+ && UIUtils.canManageDialog(getActivity())) {
+ Toast.makeText(getActivity(), getString(R.string.region_region_found,
+ Application.get().getCurrentRegion().getName()),
+ Toast.LENGTH_LONG
+ ).show();
+ }
+
+ }
+
+ void zoomToRegion() {
+ // If we have a region, then zoom to it.
+ ObaRegion region = Application.get().getCurrentRegion();
+
+ if (region != null && mMap != null) {
+ LatLngBounds b = MapHelpV2.getRegionBounds(region);
+ int padding = 0;
+ mMap.animateCamera((CameraUpdateFactory.newLatLngBounds(b, padding)));
+ }
+ }
}
\ No newline at end of file
diff --git a/src/edu/gatech/ppl/cycleatlanta/FragmentSavedTripsSection.java b/src/edu/gatech/ppl/cycleatlanta/FragmentSavedTripsSection.java
index ef5e75d..b1ca2cb 100755
--- a/src/edu/gatech/ppl/cycleatlanta/FragmentSavedTripsSection.java
+++ b/src/edu/gatech/ppl/cycleatlanta/FragmentSavedTripsSection.java
@@ -1,7 +1,5 @@
package edu.gatech.ppl.cycleatlanta;
-import java.util.ArrayList;
-
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
@@ -23,422 +21,299 @@
import android.widget.ListView;
import android.widget.Toast;
+import java.util.ArrayList;
+
public class FragmentSavedTripsSection extends Fragment {
- public static final String ARG_SECTION_NUMBER = "section_number";
-
- ListView listSavedTrips;
- ActionMode mActionMode;
- ArrayList tripIdArray = new ArrayList();
- private MenuItem saveMenuItemDelete, saveMenuItemUpload;
- String[] values;
-
- Long storedID;
-
- Cursor allTrips;
-
- public SavedTripsAdapter sta;
-
- public FragmentSavedTripsSection() {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.activity_saved_trips, null);
-
- Log.v("Jason", "Cycle: SavedTrips onCreateView");
-
- setHasOptionsMenu(true);
-
- listSavedTrips = (ListView) rootView
- .findViewById(R.id.listViewSavedTrips);
- populateTripList(listSavedTrips);
-
- final DbAdapter mDb = new DbAdapter(getActivity());
- mDb.open();
-
- // Clean up any bad trips & coords from crashes
- int cleanedTrips = mDb.cleanTripsCoordsTables();
- if (cleanedTrips > 0) {
- Toast.makeText(getActivity(),
- "" + cleanedTrips + " bad trip(s) removed.",
- Toast.LENGTH_SHORT).show();
- }
- mDb.close();
-
- tripIdArray.clear();
-
-// listSavedTrips.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
-// listSavedTrips
-// .setMultiChoiceModeListener(new MultiChoiceModeListener() {
-//
-// @Override
-// public void onItemCheckedStateChanged(ActionMode mode,
-// int position, long id, boolean checked) {
-// // Here you can do something when items are
-// // selected/de-selected,
-// // such as update the title in the CAB
-// // highlight
-//
-// if (tripIdArray.indexOf(id) > -1) {
-// tripIdArray.remove(id);
-// listSavedTrips.getChildAt(position)
-// .setBackgroundColor(
-// Color.parseColor("#80ffffff"));
-// } else {
-// tripIdArray.add(id);
-// listSavedTrips.getChildAt(position)
-// .setBackgroundColor(
-// Color.parseColor("#ff33b5e5"));
-// }
-//
-// // Toast.makeText(getActivity(),
-// // "Selected: " + tripIdArray, Toast.LENGTH_SHORT)
-// // .show();
-//
-// if (tripIdArray.size() == 0) {
-// saveMenuItemDelete.setEnabled(false);
-// } else {
-// saveMenuItemDelete.setEnabled(true);
-// }
-//
-// mode.setTitle(tripIdArray.size() + " Selected");
-// }
-//
-// @Override
-// public boolean onActionItemClicked(ActionMode mode,
-// MenuItem item) {
-// // Respond to clicks on the actions in the CAB
-// switch (item.getItemId()) {
-// case R.id.action_delete_saved_trips:
-// // delete selected trips
-// for (int i = 0; i < tripIdArray.size(); i++) {
-// deleteTrip(tripIdArray.get(i));
-// }
-// mode.finish(); // Action picked, so close the CAB
-// return true;
-// case R.id.action_upload_saved_trips:
-// // upload selected trips
-// // for (int i = 0; i < tripIdArray.size(); i++) {
-// // retryTripUpload(tripIdArray.get(i));
-// // }
-// retryTripUpload(storedID);
-// mode.finish(); // Action picked, so close the CAB
-// return true;
-// default:
-// return false;
-// }
-// }
-//
-// @Override
-// public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-// // Inflate the menu for the CAB
-// MenuInflater inflater = mode.getMenuInflater();
-// inflater.inflate(R.menu.saved_trips_context_menu, menu);
-// return true;
-// }
-//
-// @Override
-// public void onDestroyActionMode(ActionMode mode) {
-// // Here you can make any necessary updates to the
-// // activity when
-// // the CAB is removed. By default, selected items are
-// // deselected/unchecked.
-// mActionMode = null;
-// tripIdArray.clear();
-// for (int i = 0; i < listSavedTrips.getCount(); i++) {
-// Log.v("Jason", "Count" + listSavedTrips.getCount());
-// Log.v("Jason",
-// "Count" + listSavedTrips.getChildCount());
-// if (listSavedTrips.getChildCount() != 0) {
-// listSavedTrips.getChildAt(i)
-// .setBackgroundColor(
-// Color.parseColor("#80ffffff"));
-// }
-//
-// }
-// }
-//
-// @Override
-// public boolean onPrepareActionMode(ActionMode mode,
-// Menu menu) {
-// // Here you can perform updates to the CAB due to
-// // an invalidate() request
-// Log.v("Jason", "Prepare");
-// saveMenuItemDelete = menu.getItem(0);
-// saveMenuItemDelete.setEnabled(false);
-// saveMenuItemUpload = menu.getItem(1);
-//
-// int flag = 1;
-// for (int i = 0; i < listSavedTrips.getCount(); i++) {
-// allTrips.moveToPosition(i);
-// flag = flag
-// * (allTrips.getInt(allTrips
-// .getColumnIndex("status")) - 1);
-// if (flag == 0) {
-// storedID = allTrips.getLong(allTrips
-// .getColumnIndex("_id"));
-// Log.v("Jason", "" + storedID);
-// break;
-// }
-// }
-// if (flag == 1) {
-// saveMenuItemUpload.setEnabled(false);
-// } else {
-// saveMenuItemUpload.setEnabled(true);
-// }
-//
-// mode.setTitle(tripIdArray.size() + " Selected");
-// return false;
-// }
-// });
-
- return rootView;
- }
-
- private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
-
- // Called when the action mode is created; startActionMode() was called
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- // Inflate a menu resource providing context menu items
- MenuInflater inflater = mode.getMenuInflater();
- inflater.inflate(R.menu.saved_trips_context_menu, menu);
- return true;
- }
-
- // Called each time the action mode is shown. Always called after
- // onCreateActionMode, but
- // may be called multiple times if the mode is invalidated.
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- Log.v("Jason", "Prepare");
- saveMenuItemDelete = menu.getItem(0);
- saveMenuItemDelete.setEnabled(false);
- saveMenuItemUpload = menu.getItem(1);
-
- int flag = 1;
- for (int i = 0; i < listSavedTrips.getCount(); i++) {
- allTrips.moveToPosition(i);
- flag = flag
- * (allTrips.getInt(allTrips.getColumnIndex("status")) - 1);
- if (flag == 0) {
- storedID = allTrips.getLong(allTrips.getColumnIndex("_id"));
- Log.v("Jason", "" + storedID);
- break;
- }
- }
- if (flag == 1) {
- saveMenuItemUpload.setEnabled(false);
- } else {
- saveMenuItemUpload.setEnabled(true);
- }
-
- mode.setTitle(tripIdArray.size() + " Selected");
- return false; // Return false if nothing is done
- }
-
- // Called when the user selects a contextual menu item
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_delete_saved_trips:
- // delete selected trips
- for (int i = 0; i < tripIdArray.size(); i++) {
- deleteTrip(tripIdArray.get(i));
- }
- mode.finish(); // Action picked, so close the CAB
- return true;
- case R.id.action_upload_saved_trips:
- // upload selected trips
- // for (int i = 0; i < tripIdArray.size(); i++) {
- // retryTripUpload(tripIdArray.get(i));
- // }
- // Log.v("Jason", "" + storedID);
- retryTripUpload(storedID);
- mode.finish(); // Action picked, so close the CAB
- return true;
- default:
- return false;
- }
- }
-
- // Called when the user exits the action mode
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- mActionMode = null;
- tripIdArray.clear();
- for (int i = 0; i < listSavedTrips.getCount(); i++) {
- // Log.v("Jason", "Count" + listSavedTrips.getCount());
- // Log.v("Jason", "Count" + listSavedTrips.getChildCount());
- if (listSavedTrips.getChildCount() != 0) {
- listSavedTrips.getChildAt(i).setBackgroundColor(
- Color.parseColor("#80ffffff"));
- }
- }
- }
- };
-
- void populateTripList(ListView lv) {
- // Get list from the real phone database. W00t!
- final DbAdapter mDb = new DbAdapter(getActivity());
- mDb.open();
-
- try {
- allTrips = mDb.fetchAllTrips();
-
- String[] from = new String[] { "purp", "fancystart", "fancyinfo",
- "endtime", "start", "distance", "status" };
- int[] to = new int[] { R.id.TextViewPurpose, R.id.TextViewStart,
- R.id.TextViewInfo };
-
- sta = new SavedTripsAdapter(getActivity(),
- R.layout.saved_trips_list_item, allTrips, from, to,
- CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
-
- lv.setAdapter(sta);
- } catch (SQLException sqle) {
- // Do nothing, for now!
- }
- mDb.close();
-
- lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- public void onItemClick(AdapterView> parent, View v, int pos,
- long id) {
- allTrips.moveToPosition(pos);
- if (mActionMode == null) {
- if (allTrips.getInt(allTrips.getColumnIndex("status")) == 2) {
- Intent i = new Intent(getActivity(),
- TripMapActivity.class);
- i.putExtra("showtrip", id);
- startActivity(i);
- } else if (allTrips.getInt(allTrips
- .getColumnIndex("status")) == 1) {
- // Toast.makeText(getActivity(), "Unsent",
- // Toast.LENGTH_SHORT).show();
- buildAlertMessageUnuploadedTripClicked(id);
-
- // Log.v("Jason",
- // ""+allTrips.getLong(allTrips.getColumnIndex("_id")));
- }
-
- } else {
- // highlight
- if (tripIdArray.indexOf(id) > -1) {
- tripIdArray.remove(id);
- v.setBackgroundColor(Color.parseColor("#80ffffff"));
- } else {
- tripIdArray.add(id);
- v.setBackgroundColor(Color.parseColor("#ff33b5e5"));
- }
- // Toast.makeText(getActivity(), "Selected: " + tripIdArray,
- // Toast.LENGTH_SHORT).show();
- if (tripIdArray.size() == 0) {
- saveMenuItemDelete.setEnabled(false);
- } else {
- saveMenuItemDelete.setEnabled(true);
- }
-
- mActionMode.setTitle(tripIdArray.size() + " Selected");
- }
- }
- });
-
- registerForContextMenu(lv);
- }
-
- private void buildAlertMessageUnuploadedTripClicked(final long position) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(
- getActivity());
- builder.setTitle("Upload Trip");
- builder.setMessage("Do you want to upload this trip?");
- builder.setNegativeButton("Upload",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- retryTripUpload(position);
- // Toast.makeText(getActivity(),"Send Clicked: "+position,
- // Toast.LENGTH_SHORT).show();
- }
- });
-
- builder.setPositiveButton("Cancel",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- // continue
- }
- });
- final AlertDialog alert = builder.create();
- alert.show();
- }
-
- private void retryTripUpload(long tripId) {
- TripUploader uploader = new TripUploader(getActivity());
- FragmentSavedTripsSection f2 = (FragmentSavedTripsSection) getActivity()
- .getSupportFragmentManager().findFragmentByTag(
- "android:switcher:" + R.id.pager + ":1");
- uploader.setSavedTripsAdapter(sta);
- uploader.setFragmentSavedTripsSection(f2);
- uploader.setListView(listSavedTrips);
- uploader.execute();
- }
-
- private void deleteTrip(long tripId) {
- DbAdapter mDbHelper = new DbAdapter(getActivity());
- mDbHelper.open();
- mDbHelper.deleteAllCoordsForTrip(tripId);
- mDbHelper.deleteTrip(tripId);
- mDbHelper.close();
- listSavedTrips.invalidate();
- populateTripList(listSavedTrips);
- }
-
- // show edit button and hidden delete button
- @Override
- public void onResume() {
- super.onResume();
- Log.v("Jason", "Cycle: SavedTrips onResume");
- populateTripList(listSavedTrips);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- Log.v("Jason", "Cycle: SavedTrips onPause");
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- Log.v("Jason", "Cycle: SavedTrips onDestroyView");
- }
-
- /* Creates the menu items */
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- // Inflate the menu items for use in the action bar
- inflater.inflate(R.menu.saved_trips, menu);
- super.onCreateOptionsMenu(menu, inflater);
- }
-
- /* Handles item selections */
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle presses on the action bar items
- switch (item.getItemId()) {
- case R.id.action_edit_saved_trips:
- // edit
- if (mActionMode != null) {
- return false;
- }
-
- // Start the CAB using the ActionMode.Callback defined above
- mActionMode = getActivity().startActionMode(mActionModeCallback);
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
+ public static final String ARG_SECTION_NUMBER = "section_number";
+
+ ListView listSavedTrips;
+ ActionMode mActionMode;
+ ArrayList tripIdArray = new ArrayList();
+ private MenuItem saveMenuItemDelete, saveMenuItemUpload;
+ String[] values;
+
+ Long storedID;
+
+ Cursor allTrips;
+
+ public SavedTripsAdapter sta;
+
+ public FragmentSavedTripsSection() {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.activity_saved_trips, null);
+
+ Log.v("Jason", "Cycle: SavedTrips onCreateView");
+
+ setHasOptionsMenu(true);
+
+ listSavedTrips = (ListView) rootView
+ .findViewById(R.id.listViewSavedTrips);
+ populateTripList(listSavedTrips);
+
+ final DbAdapter mDb = new DbAdapter(getActivity());
+ mDb.open();
+
+ // Clean up any bad trips & coords from crashes
+ int cleanedTrips = mDb.cleanTripsCoordsTables();
+ if (cleanedTrips > 0) {
+ Toast.makeText(getActivity(),
+ "" + cleanedTrips + " bad trip(s) removed.",
+ Toast.LENGTH_SHORT).show();
+ }
+ mDb.close();
+
+ tripIdArray.clear();
+
+ return rootView;
+ }
+
+ private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
+
+ // Called when the action mode is created; startActionMode() was called
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Inflate a menu resource providing context menu items
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.saved_trips_context_menu, menu);
+ return true;
+ }
+
+ // Called each time the action mode is shown. Always called after
+ // onCreateActionMode, but
+ // may be called multiple times if the mode is invalidated.
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ Log.v("Jason", "Prepare");
+ saveMenuItemDelete = menu.getItem(0);
+ saveMenuItemDelete.setEnabled(false);
+ saveMenuItemUpload = menu.getItem(1);
+
+ int flag = 1;
+ for (int i = 0; i < listSavedTrips.getCount(); i++) {
+ allTrips.moveToPosition(i);
+ flag = flag
+ * (allTrips.getInt(allTrips.getColumnIndex("status")) - 1);
+ if (flag == 0) {
+ storedID = allTrips.getLong(allTrips.getColumnIndex("_id"));
+ Log.v("Jason", "" + storedID);
+ break;
+ }
+ }
+ if (flag == 1) {
+ saveMenuItemUpload.setEnabled(false);
+ } else {
+ saveMenuItemUpload.setEnabled(true);
+ }
+
+ mode.setTitle(tripIdArray.size() + " Selected");
+ return false; // Return false if nothing is done
+ }
+
+ // Called when the user selects a contextual menu item
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_delete_saved_trips:
+ // delete selected trips
+ for (int i = 0; i < tripIdArray.size(); i++) {
+ deleteTrip(tripIdArray.get(i));
+ }
+ mode.finish(); // Action picked, so close the CAB
+ return true;
+ case R.id.action_upload_saved_trips:
+ // upload selected trips
+ // for (int i = 0; i < tripIdArray.size(); i++) {
+ // retryTripUpload(tripIdArray.get(i));
+ // }
+ // Log.v("Jason", "" + storedID);
+ retryTripUpload(storedID);
+ mode.finish(); // Action picked, so close the CAB
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Called when the user exits the action mode
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActionMode = null;
+ tripIdArray.clear();
+ for (int i = 0; i < listSavedTrips.getCount(); i++) {
+ // Log.v("Jason", "Count" + listSavedTrips.getCount());
+ // Log.v("Jason", "Count" + listSavedTrips.getChildCount());
+ if (listSavedTrips.getChildCount() != 0) {
+ listSavedTrips.getChildAt(i).setBackgroundColor(
+ Color.parseColor("#80ffffff"));
+ }
+ }
+ }
+ };
+
+ void populateTripList(ListView lv) {
+ // Get list from the real phone database. W00t!
+ final DbAdapter mDb = new DbAdapter(getActivity());
+ mDb.open();
+
+ try {
+ allTrips = mDb.fetchAllTrips();
+
+ String[] from = new String[]{"purp", "fancystart", "fancyinfo",
+ "endtime", "start", "distance", "status"};
+ int[] to = new int[]{R.id.TextViewPurpose, R.id.TextViewStart,
+ R.id.TextViewInfo};
+
+ sta = new SavedTripsAdapter(getActivity(),
+ R.layout.saved_trips_list_item, allTrips, from, to,
+ CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
+
+ lv.setAdapter(sta);
+ } catch (SQLException sqle) {
+ // Do nothing, for now!
+ }
+ mDb.close();
+
+ lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView> parent, View v, int pos,
+ long id) {
+ allTrips.moveToPosition(pos);
+ if (mActionMode == null) {
+ if (allTrips.getInt(allTrips.getColumnIndex("status")) == 2) {
+ Intent i = new Intent(getActivity(),
+ TripMapActivity.class);
+ i.putExtra("showtrip", id);
+ startActivity(i);
+ } else if (allTrips.getInt(allTrips
+ .getColumnIndex("status")) == 1) {
+ // Toast.makeText(getActivity(), "Unsent",
+ // Toast.LENGTH_SHORT).show();
+ buildAlertMessageUnuploadedTripClicked(id);
+
+ // Log.v("Jason",
+ // ""+allTrips.getLong(allTrips.getColumnIndex("_id")));
+ }
+
+ } else {
+ // highlight
+ if (tripIdArray.indexOf(id) > -1) {
+ tripIdArray.remove(id);
+ v.setBackgroundColor(Color.parseColor("#80ffffff"));
+ } else {
+ tripIdArray.add(id);
+ v.setBackgroundColor(Color.parseColor("#ff33b5e5"));
+ }
+ // Toast.makeText(getActivity(), "Selected: " + tripIdArray,
+ // Toast.LENGTH_SHORT).show();
+ if (tripIdArray.size() == 0) {
+ saveMenuItemDelete.setEnabled(false);
+ } else {
+ saveMenuItemDelete.setEnabled(true);
+ }
+
+ mActionMode.setTitle(tripIdArray.size() + " Selected");
+ }
+ }
+ });
+
+ registerForContextMenu(lv);
+ }
+
+ private void buildAlertMessageUnuploadedTripClicked(final long position) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(
+ getActivity());
+ builder.setTitle("Upload Trip");
+ builder.setMessage("Do you want to upload this trip?");
+ builder.setNegativeButton("Upload",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ retryTripUpload(position);
+ // Toast.makeText(getActivity(),"Send Clicked: "+position,
+ // Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ builder.setPositiveButton("Cancel",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ // continue
+ }
+ });
+ final AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ private void retryTripUpload(long tripId) {
+ TripUploader uploader = new TripUploader(getActivity());
+ FragmentSavedTripsSection f2 = (FragmentSavedTripsSection) getActivity()
+ .getSupportFragmentManager().findFragmentByTag(
+ "android:switcher:" + R.id.pager + ":1");
+ uploader.setSavedTripsAdapter(sta);
+ uploader.setFragmentSavedTripsSection(f2);
+ uploader.setListView(listSavedTrips);
+ uploader.execute();
+ }
+
+ private void deleteTrip(long tripId) {
+ DbAdapter mDbHelper = new DbAdapter(getActivity());
+ mDbHelper.open();
+ mDbHelper.deleteAllCoordsForTrip(tripId);
+ mDbHelper.deleteTrip(tripId);
+ mDbHelper.close();
+ listSavedTrips.invalidate();
+ populateTripList(listSavedTrips);
+ }
+
+ // show edit button and hidden delete button
+ @Override
+ public void onResume() {
+ super.onResume();
+ Log.v("Jason", "Cycle: SavedTrips onResume");
+ populateTripList(listSavedTrips);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ Log.v("Jason", "Cycle: SavedTrips onPause");
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ Log.v("Jason", "Cycle: SavedTrips onDestroyView");
+ }
+
+ /* Creates the menu items */
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ // Inflate the menu items for use in the action bar
+ inflater.inflate(R.menu.saved_trips, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ /* Handles item selections */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle presses on the action bar items
+ switch (item.getItemId()) {
+ case R.id.action_edit_saved_trips:
+ // edit
+ if (mActionMode != null) {
+ return false;
+ }
+
+ // Start the CAB using the ActionMode.Callback defined above
+ mActionMode = getActivity().startActionMode(mActionModeCallback);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
}
diff --git a/src/edu/gatech/ppl/cycleatlanta/FragmentUserInfo.java b/src/edu/gatech/ppl/cycleatlanta/FragmentUserInfo.java
index 61f3145..5d0202e 100755
--- a/src/edu/gatech/ppl/cycleatlanta/FragmentUserInfo.java
+++ b/src/edu/gatech/ppl/cycleatlanta/FragmentUserInfo.java
@@ -1,13 +1,15 @@
package edu.gatech.ppl.cycleatlanta;
-import java.util.Map;
-import java.util.Map.Entry;
-
+import android.app.AlertDialog;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -15,285 +17,380 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
-public class FragmentUserInfo extends Fragment {
-
- public final static int PREF_AGE = 1;
- public final static int PREF_ZIPHOME = 2;
- public final static int PREF_ZIPWORK = 3;
- public final static int PREF_ZIPSCHOOL = 4;
- public final static int PREF_EMAIL = 5;
- public final static int PREF_GENDER = 6;
- public final static int PREF_CYCLEFREQ = 7;
- public final static int PREF_ETHNICITY = 8;
- public final static int PREF_INCOME = 9;
- public final static int PREF_RIDERTYPE = 10;
- public final static int PREF_RIDERHISTORY = 11;
-
- private static final String TAG = "UserPrefActivity";
-
- private final static int MENU_SAVE = 0;
-
- public FragmentUserInfo() {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- View rootView = inflater.inflate(R.layout.activity_user_info,
- container, false);
- // getActivity().getActionBar().setDisplayShowTitleEnabled(true);
- // getActivity().getActionBar().setDisplayShowHomeEnabled(true);
-
- // Don't pop up the soft keyboard until user clicks!
- // this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
-
- // not using seekbar any more
- // SeekBar sb = (SeekBar) findViewById(R.id.SeekCycleFreq);
- // sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
- //
- // @Override
- // public void onStopTrackingTouch(SeekBar arg0) {
- // // TODO Auto-generated method stub
- // }
- //
- // @Override
- // public void onStartTrackingTouch(SeekBar arg0) {
- // // TODO Auto-generated method stub
- // }
- //
- // @Override
- // public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
- // TextView tv = (TextView) findViewById(R.id.TextFreq);
- // tv.setText(freqDesc[arg1 / 100]);
- // }
- // });
-
- // put on Cycle Atlanta bar
- // Button btn = (Button) findViewById(R.id.saveButton);
- // btn.setOnClickListener(new OnClickListener() {
- // @Override
- // public void onClick(View arg0) {
- // Intent intent = new Intent(UserInfoActivity.this,
- // MainInput.class);
- // startActivity(intent);
- // finish();
- // }
- //
- // });
-
- final Button GetStarted = (Button) rootView
- .findViewById(R.id.buttonGetStarted);
- GetStarted.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- // Toast.makeText(getActivity(), "Get Started Clicked",
- // Toast.LENGTH_LONG).show();
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
- .parse("http://cycleatlanta.org/instructions-v2/"));
- startActivity(browserIntent);
- }
- });
-
- SharedPreferences settings = getActivity().getSharedPreferences(
- "PREFS", 0);
- Map prefs = settings.getAll();
- for (Entry p : prefs.entrySet()) {
- int key = Integer.parseInt(p.getKey());
- // CharSequence value = (CharSequence) p.getValue();
-
- switch (key) {
- case PREF_AGE:
- ((Spinner) rootView.findViewById(R.id.ageSpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- case PREF_ETHNICITY:
- ((Spinner) rootView.findViewById(R.id.ethnicitySpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- case PREF_INCOME:
- ((Spinner) rootView.findViewById(R.id.incomeSpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- case PREF_RIDERTYPE:
- ((Spinner) rootView.findViewById(R.id.ridertypeSpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- case PREF_RIDERHISTORY:
- ((Spinner) rootView.findViewById(R.id.riderhistorySpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- case PREF_ZIPHOME:
- ((EditText) rootView.findViewById(R.id.TextZipHome))
- .setText((CharSequence) p.getValue());
- break;
- case PREF_ZIPWORK:
- ((EditText) rootView.findViewById(R.id.TextZipWork))
- .setText((CharSequence) p.getValue());
- break;
- case PREF_ZIPSCHOOL:
- ((EditText) rootView.findViewById(R.id.TextZipSchool))
- .setText((CharSequence) p.getValue());
- break;
- case PREF_EMAIL:
- ((EditText) rootView.findViewById(R.id.TextEmail))
- .setText((CharSequence) p.getValue());
- break;
- case PREF_CYCLEFREQ:
- ((Spinner) rootView.findViewById(R.id.cyclefreqSpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- // ((SeekBar)
- // findViewById(R.id.SeekCycleFreq)).setProgress(((Integer)
- // p.getValue()).intValue());
- break;
- case PREF_GENDER:
- ((Spinner) rootView.findViewById(R.id.genderSpinner))
- .setSelection(((Integer) p.getValue()).intValue());
- break;
- // int x = ((Integer) p.getValue()).intValue();
- // if (x == 2) {
- // ((RadioButton) findViewById(R.id.ButtonMale)).setChecked(true);
- // } else if (x == 1) {
- // ((RadioButton) findViewById(R.id.ButtonFemale)).setChecked(true);
- // }
- // break;
- }
- }
-
- final EditText edittextEmail = (EditText) rootView
- .findViewById(R.id.TextEmail);
-
- edittextEmail.setImeOptions(EditorInfo.IME_ACTION_DONE);
-
- setHasOptionsMenu(true);
- return rootView;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- // savePreferences();
- }
-
- public void savePreferences() {
- // Toast.makeText(getActivity(), "savePreferences()",
- // Toast.LENGTH_LONG).show();
-
- // Save user preferences. We need an Editor object to
- // make changes. All objects are from android.context.Context
- SharedPreferences settings = getActivity().getSharedPreferences(
- "PREFS", 0);
- SharedPreferences.Editor editor = settings.edit();
-
- editor.putInt("" + PREF_AGE,
- ((Spinner) getActivity().findViewById(R.id.ageSpinner))
- .getSelectedItemPosition());
- editor.putInt("" + PREF_ETHNICITY, ((Spinner) getActivity()
- .findViewById(R.id.ethnicitySpinner)).getSelectedItemPosition());
- editor.putInt("" + PREF_INCOME,
- ((Spinner) getActivity().findViewById(R.id.incomeSpinner))
- .getSelectedItemPosition());
- editor.putInt("" + PREF_RIDERTYPE, ((Spinner) getActivity()
- .findViewById(R.id.ridertypeSpinner)).getSelectedItemPosition());
- editor.putInt("" + PREF_RIDERHISTORY, ((Spinner) getActivity()
- .findViewById(R.id.riderhistorySpinner))
- .getSelectedItemPosition());
-
- editor.putString("" + PREF_ZIPHOME, ((EditText) getActivity()
- .findViewById(R.id.TextZipHome)).getText().toString());
- editor.putString("" + PREF_ZIPWORK, ((EditText) getActivity()
- .findViewById(R.id.TextZipWork)).getText().toString());
- editor.putString("" + PREF_ZIPSCHOOL, ((EditText) getActivity()
- .findViewById(R.id.TextZipSchool)).getText().toString());
- editor.putString("" + PREF_EMAIL, ((EditText) getActivity()
- .findViewById(R.id.TextEmail)).getText().toString());
-
- editor.putInt("" + PREF_CYCLEFREQ, ((Spinner) getActivity()
- .findViewById(R.id.cyclefreqSpinner)).getSelectedItemPosition());
- // editor.putInt("" + PREF_CYCLEFREQ, ((SeekBar)
- // findViewById(R.id.SeekCycleFreq)).getProgress());
-
- editor.putInt("" + PREF_GENDER,
- ((Spinner) getActivity().findViewById(R.id.genderSpinner))
- .getSelectedItemPosition());
- // RadioGroup rbg = (RadioGroup) findViewById(R.id.RadioGroup01);
- // if (rbg.getCheckedRadioButtonId() == R.id.ButtonMale) {
- // editor.putInt("" + PREF_GENDER, 2);
- // //Log.v(TAG, "gender=" + 2);
- // }
- // if (rbg.getCheckedRadioButtonId() == R.id.ButtonFemale) {
- // editor.putInt("" + PREF_GENDER, 1);
- // //Log.v(TAG, "gender=" + 1);
- // }
-
- // Log.v(TAG,
- // "ageIndex="
- // + ((Spinner) findViewById(R.id.ageSpinner))
- // .getSelectedItemPosition());
- // Log.v(TAG,
- // "ethnicityIndex="
- // + ((Spinner) findViewById(R.id.ethnicitySpinner))
- // .getSelectedItemPosition());
- // Log.v(TAG,
- // "incomeIndex="
- // + ((Spinner) findViewById(R.id.incomeSpinner))
- // .getSelectedItemPosition());
- // Log.v(TAG,
- // "ridertypeIndex="
- // + ((Spinner) findViewById(R.id.ridertypeSpinner))
- // .getSelectedItemPosition());
- // Log.v(TAG,
- // "riderhistoryIndex="
- // + ((Spinner) findViewById(R.id.riderhistorySpinner))
- // .getSelectedItemPosition());
- // Log.v(TAG, "ziphome="
- // + ((EditText) findViewById(R.id.TextZipHome)).getText()
- // .toString());
- // Log.v(TAG, "zipwork="
- // + ((EditText) findViewById(R.id.TextZipWork)).getText()
- // .toString());
- // Log.v(TAG, "zipschool="
- // + ((EditText) findViewById(R.id.TextZipSchool)).getText()
- // .toString());
- // Log.v(TAG, "email="
- // + ((EditText) findViewById(R.id.TextEmail)).getText()
- // .toString());
- // Log.v(TAG,
- // "frequency="
- // + ((SeekBar) findViewById(R.id.SeekCycleFreq))
- // .getProgress() / 100);
-
- // Don't forget to commit your edits!!!
- editor.commit();
- Toast.makeText(getActivity(), "User information saved.",
- Toast.LENGTH_SHORT).show();
- // Toast.makeText(getActivity(), ""+((Spinner)
- // getActivity().findViewById(R.id.ageSpinner)).getSelectedItemPosition(),
- // Toast.LENGTH_LONG).show();
- }
-
- /* Creates the menu items */
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- // Inflate the menu items for use in the action bar
- inflater.inflate(R.menu.user_info, menu);
- super.onCreateOptionsMenu(menu, inflater);
- }
-
- /* Handles item selections */
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle presses on the action bar items
- switch (item.getItemId()) {
- case R.id.action_save_user_info:
- savePreferences();
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import edu.gatech.ppl.cycleatlanta.region.ObaRegionsLoader;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.utils.PreferenceUtils;
+
+public class FragmentUserInfo extends Fragment implements
+ LoaderManager.LoaderCallbacks> {
+
+ public final static int PREF_AGE = 1;
+ public final static int PREF_ZIPHOME = 2;
+ public final static int PREF_ZIPWORK = 3;
+ public final static int PREF_ZIPSCHOOL = 4;
+ public final static int PREF_EMAIL = 5;
+ public final static int PREF_GENDER = 6;
+ public final static int PREF_CYCLEFREQ = 7;
+ public final static int PREF_ETHNICITY = 8;
+ public final static int PREF_INCOME = 9;
+ public final static int PREF_RIDERTYPE = 10;
+ public final static int PREF_RIDERHISTORY = 11;
+
+ private static final String RELOAD = ".reload";
+
+ private Spinner regionSpinner;
+
+ private List mObaRegions;
+
+ private boolean mLoaderCheck = false;
+
+ public FragmentUserInfo() {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ View rootView = inflater.inflate(R.layout.activity_user_info,
+ container, false);
+ final Button getStarted = (Button) rootView
+ .findViewById(R.id.buttonGetStarted);
+ getStarted.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ ObaRegion currentRegion = Application.get().getCurrentRegion();
+
+ if (currentRegion != null) {
+ String tutorialUrl = currentRegion.getTutorialUrl();
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
+ .parse(tutorialUrl));
+ startActivity(browserIntent);
+ }
+ }
+ });
+
+ if (Application.get().getCurrentRegion() == null) {
+ getStarted.setVisibility(View.GONE);
+ }
+
+ SharedPreferences settings = getActivity().getSharedPreferences(
+ "PREFS", 0);
+ Map prefs = settings.getAll();
+ for (Entry p : prefs.entrySet()) {
+ int key = Integer.parseInt(p.getKey());
+
+ switch (key) {
+ case PREF_AGE:
+ ((Spinner) rootView.findViewById(R.id.ageSpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_ETHNICITY:
+ ((Spinner) rootView.findViewById(R.id.ethnicitySpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_INCOME:
+ ((Spinner) rootView.findViewById(R.id.incomeSpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_RIDERTYPE:
+ ((Spinner) rootView.findViewById(R.id.ridertypeSpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_RIDERHISTORY:
+ ((Spinner) rootView.findViewById(R.id.riderhistorySpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_ZIPHOME:
+ ((EditText) rootView.findViewById(R.id.TextZipHome))
+ .setText((CharSequence) p.getValue());
+ break;
+ case PREF_ZIPWORK:
+ ((EditText) rootView.findViewById(R.id.TextZipWork))
+ .setText((CharSequence) p.getValue());
+ break;
+ case PREF_ZIPSCHOOL:
+ ((EditText) rootView.findViewById(R.id.TextZipSchool))
+ .setText((CharSequence) p.getValue());
+ break;
+ case PREF_EMAIL:
+ ((EditText) rootView.findViewById(R.id.TextEmail))
+ .setText((CharSequence) p.getValue());
+ break;
+ case PREF_CYCLEFREQ:
+ ((Spinner) rootView.findViewById(R.id.cyclefreqSpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ case PREF_GENDER:
+ ((Spinner) rootView.findViewById(R.id.genderSpinner))
+ .setSelection(((Integer) p.getValue()).intValue());
+ break;
+ }
+ }
+
+ final EditText edittextEmail = (EditText) rootView
+ .findViewById(R.id.TextEmail);
+
+ edittextEmail.setImeOptions(EditorInfo.IME_ACTION_DONE);
+
+ setHasOptionsMenu(true);
+ return rootView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ initRegions();
+ }
+
+ private void initRegions() {
+ regionSpinner = (Spinner) getActivity().findViewById(R.id.regionsSpinner);
+
+ Bundle args = new Bundle();
+ args.putBoolean(RELOAD, false);
+ getLoaderManager().initLoader(0, args, this);
+
+ regionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (mLoaderCheck) {
+ mLoaderCheck = false;
+ return;
+ }
+ if (mObaRegions != null && position < mObaRegions.size()) {
+ ObaRegion selectedRegion = mObaRegions.get(position);
+ Application.get().setCurrentRegion(selectedRegion);
+ Application.get().setCustomApiUrl(null);
+ PreferenceUtils
+ .saveBoolean(getString(R.string.preference_key_auto_select_region), false);
+ } else if (mObaRegions != null && mObaRegions.size() == position) {
+ showCustomApiDialog();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+ });
+ }
+
+ private void showCustomApiDialog() {
+ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+ final EditText edittext = new EditText(getActivity());
+ alert.setTitle(getActivity().getString(R.string.custom_api_server_title));
+
+ alert.setView(edittext);
+
+ alert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String value = edittext.getText().toString();
+ String validValue = validateUrl(value);
+ if (validValue != null) {
+ setCustomApiUrl(validValue);
+ } else {
+ resetSelection();
+ Toast.makeText(getActivity(), getString(R.string.custom_api_url_error),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ resetSelection();
+ }
+ });
+
+ alert.show();
+ }
+
+ private void resetSelection() {
+ ArrayList arraySpinner = new ArrayList();
+ ObaRegion currentRegion = Application.get().getCurrentRegion();
+ int selection = 0;
+ int i = 0;
+
+ for (ObaRegion r : mObaRegions) {
+ arraySpinner.add(r.getName());
+ if (currentRegion != null && r.getId() == currentRegion.getId()) {
+ selection = i;
+ }
+ i++;
+ }
+
+ arraySpinner.add(getActivity().getString(R.string.custom_api_server));
+
+ ArrayAdapter adapter = new ArrayAdapter(getActivity(),
+ android.R.layout.simple_list_item_1, arraySpinner);
+ regionSpinner.setAdapter(adapter);
+ regionSpinner.setSelection(selection);
+ }
+
+ private void setCustomApiUrl(String value) {
+ Application.get().setCurrentRegion(null);
+ Application.get().setCustomApiUrl(value);
+
+ ArrayList arraySpinner = new ArrayList();
+
+ for (ObaRegion r : mObaRegions) {
+ arraySpinner.add(r.getName());
+ }
+
+ arraySpinner.add(getActivity().getString(R.string.custom_api_server));
+
+ arraySpinner.add(value);
+
+ ArrayAdapter adapter = new ArrayAdapter(getActivity(),
+ android.R.layout.simple_list_item_1, arraySpinner);
+ regionSpinner.setAdapter(adapter);
+ regionSpinner.setSelection(arraySpinner.size() - 1);
+ }
+
+ /**
+ * Returns true if the provided apiUrl could be a valid URL, false if it could not
+ *
+ * @param apiUrl the URL to validate
+ * @return true if the provided apiUrl could be a valid URL, false if it could not
+ */
+ private String validateUrl(String apiUrl) {
+ if (apiUrl == null) return null;
+
+ try {
+ // URI.parse() doesn't tell us if the scheme is missing, so use URL() instead (#126)
+ URL url = new URL(apiUrl);
+ } catch (MalformedURLException e) {
+ // Assume HTTP scheme if none is provided
+ apiUrl = getString(R.string.http_prefix) + apiUrl;
+ return apiUrl;
+ }
+ if (Patterns.WEB_URL.matcher(apiUrl).matches())
+ return apiUrl;
+ else
+ return null;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ }
+
+ public void savePreferences() {
+ SharedPreferences settings = getActivity().getSharedPreferences(
+ "PREFS", 0);
+ SharedPreferences.Editor editor = settings.edit();
+
+ editor.putInt("" + PREF_AGE,
+ ((Spinner) getActivity().findViewById(R.id.ageSpinner))
+ .getSelectedItemPosition());
+ editor.putInt("" + PREF_ETHNICITY, ((Spinner) getActivity()
+ .findViewById(R.id.ethnicitySpinner)).getSelectedItemPosition());
+ editor.putInt("" + PREF_INCOME,
+ ((Spinner) getActivity().findViewById(R.id.incomeSpinner))
+ .getSelectedItemPosition());
+ editor.putInt("" + PREF_RIDERTYPE, ((Spinner) getActivity()
+ .findViewById(R.id.ridertypeSpinner)).getSelectedItemPosition());
+ editor.putInt("" + PREF_RIDERHISTORY, ((Spinner) getActivity()
+ .findViewById(R.id.riderhistorySpinner))
+ .getSelectedItemPosition());
+
+ editor.putString("" + PREF_ZIPHOME, ((EditText) getActivity()
+ .findViewById(R.id.TextZipHome)).getText().toString());
+ editor.putString("" + PREF_ZIPWORK, ((EditText) getActivity()
+ .findViewById(R.id.TextZipWork)).getText().toString());
+ editor.putString("" + PREF_ZIPSCHOOL, ((EditText) getActivity()
+ .findViewById(R.id.TextZipSchool)).getText().toString());
+ editor.putString("" + PREF_EMAIL, ((EditText) getActivity()
+ .findViewById(R.id.TextEmail)).getText().toString());
+
+ editor.putInt("" + PREF_CYCLEFREQ, ((Spinner) getActivity()
+ .findViewById(R.id.cyclefreqSpinner)).getSelectedItemPosition());
+
+ editor.putInt("" + PREF_GENDER,
+ ((Spinner) getActivity().findViewById(R.id.genderSpinner))
+ .getSelectedItemPosition());
+
+ // Don't forget to commit your edits!!!
+ editor.commit();
+ Toast.makeText(getActivity(), "User information saved.",
+ Toast.LENGTH_SHORT).show();
+ }
+
+ /* Creates the menu items */
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ // Inflate the menu items for use in the action bar
+ inflater.inflate(R.menu.user_info, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ /* Handles item selections */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle presses on the action bar items
+ switch (item.getItemId()) {
+ case R.id.action_save_user_info:
+ savePreferences();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public Loader> onCreateLoader(int id, Bundle args) {
+ boolean refresh = args.getBoolean(RELOAD);
+ return new ObaRegionsLoader(getActivity(), refresh);
+ }
+
+ @Override
+ public void onLoadFinished(Loader> loader, ArrayList data) {
+ mObaRegions = data;
+ mLoaderCheck = true;
+
+ ArrayList arraySpinner = new ArrayList();
+ ObaRegion currentRegion = Application.get().getCurrentRegion();
+ String customApiUrl = Application.get().getCustomApiUrl();
+ int selection = 0;
+ int i = 0;
+
+ for (ObaRegion r : data) {
+ arraySpinner.add(r.getName());
+ if (currentRegion != null && r.getId() == currentRegion.getId()) {
+ selection = i;
+ }
+ i++;
+ }
+
+ arraySpinner.add(getActivity().getString(R.string.custom_api_server));
+
+ if (currentRegion == null && customApiUrl != null) {
+ // Add the custom api to beginning of the list
+ arraySpinner.add(customApiUrl);
+ selection = arraySpinner.size() - 1;
+ }
+
+
+ ArrayAdapter adapter = new ArrayAdapter(getActivity(),
+ android.R.layout.simple_list_item_1, arraySpinner);
+ regionSpinner.setAdapter(adapter);
+ regionSpinner.setSelection(selection);
+ }
+
+ @Override
+ public void onLoaderReset(Loader> loader) {
+
+ }
}
\ No newline at end of file
diff --git a/src/edu/gatech/ppl/cycleatlanta/NoteDetailActivity.java b/src/edu/gatech/ppl/cycleatlanta/NoteDetailActivity.java
index fff3b5a..094959e 100755
--- a/src/edu/gatech/ppl/cycleatlanta/NoteDetailActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/NoteDetailActivity.java
@@ -109,10 +109,6 @@ void submit(String noteDetailsToUpload, byte[] noteImage) {
note.updateNoteStatus(NoteData.STATUS_COMPLETE);
- // Now create the MainInput Activity so BACK btn works properly
- // Should not use this.
-
- // TODO: note uploader
if (note.notestatus < NoteData.STATUS_SENT) {
// And upload to the cloud database, too! W00t W00t!
NoteUploader uploader = new NoteUploader(NoteDetailActivity.this);
@@ -124,7 +120,7 @@ void submit(String noteDetailsToUpload, byte[] noteImage) {
Intent i = new Intent(getApplicationContext(), TabsConfig.class);
startActivity(i);
- // And, show the map!
+ // And, show the mMap!
xi.putExtra("shownote", note.noteid);
xi.putExtra("uploadNote", true);
Log.v("Jason", "Noteid: " + String.valueOf(note.noteid));
diff --git a/src/edu/gatech/ppl/cycleatlanta/NoteMapActivity.java b/src/edu/gatech/ppl/cycleatlanta/NoteMapActivity.java
index 1a84363..ca4498e 100755
--- a/src/edu/gatech/ppl/cycleatlanta/NoteMapActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/NoteMapActivity.java
@@ -69,7 +69,7 @@ protected void onCreate(Bundle savedInstanceState) {
t2.setText(note.notedetails);
t3.setText(note.notefancystart);
- // Center & zoom the map
+ // Center & zoom the mMap
LatLng center = new LatLng(note.latitude * 1E-6,
note.longitude * 1E-6);
@@ -150,14 +150,14 @@ public boolean onOptionsItemSelected(MenuItem item) {
// close -> go back to FragmentMainInput
onBackPressed();
case R.id.action_switch_note_view:
- // animation for map and image..
+ // animation for mMap and image..
if (saveMenuItem.getTitle().equals("image")) {
- saveMenuItem.setTitle("map");
+ saveMenuItem.setTitle("mMap");
Animation animFadeIn = AnimationUtils.loadAnimation(
getApplicationContext(), android.R.anim.fade_in);
imageView.setAnimation(animFadeIn);
imageView.setVisibility(View.VISIBLE);
- } else if (saveMenuItem.getTitle().equals("map")) {
+ } else if (saveMenuItem.getTitle().equals("mMap")) {
saveMenuItem.setTitle("image");
Animation animFadeOut = AnimationUtils.loadAnimation(
getApplicationContext(), android.R.anim.fade_out);
diff --git a/src/edu/gatech/ppl/cycleatlanta/NoteTypeActivity.java b/src/edu/gatech/ppl/cycleatlanta/NoteTypeActivity.java
index 2816dc8..e7f1cc3 100755
--- a/src/edu/gatech/ppl/cycleatlanta/NoteTypeActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/NoteTypeActivity.java
@@ -40,12 +40,12 @@ void prepareNoteTypeButtons() {
// Note Issue
noteTypeDescriptions
.put(0,
- "HereÕs a spot where the road needs to be repaired (pothole, rough concrete, gravel in the road, manhole cover, sewer grate).");
+ "Here-s a spot where the road needs to be repaired (pothole, rough concrete, gravel in the road, manhole cover, sewer grate).");
noteTypeDescriptions.put(1,
- "HereÕs a signal that you canÕt activate with your bike.");
+ "Here-s a signal that you can-t activate with your bike.");
noteTypeDescriptions
.put(2,
- "The bike lane is always blocked here, cars disobey \"no right on red\"É anything where the cops can help make cycling safer.");
+ "The bike lane is always blocked here, cars disobey \"no right on red\"- anything where the cops can help make cycling safer.");
noteTypeDescriptions.put(3,
"You need a bike rack to secure your bike here.");
noteTypeDescriptions
@@ -64,16 +64,16 @@ void prepareNoteTypeButtons() {
"Have a flat, a broken chain, or spongy brakes? Or do you need a bike to jump into this world of cycling in the first place? Here's a shop ready to help.");
noteTypeDescriptions
.put(8,
- "Help us make cycling mainstreamÉ hereÕs a place to refresh yourself before you re-enter the fashionable world of Atlanta.");
+ "Help us make cycling mainstream- here-s a place to refresh yourself before you re-enter the fashionable world of Atlanta.");
noteTypeDescriptions
.put(9,
"Here's an access point under the tracks, through the park, onto a trail, or over a ravine.");
noteTypeDescriptions
.put(10,
- "HereÕs a spot to fill your bottle on those hot summer daysÉ stay hydrated, people. We need you.");
+ "Here-s a spot to fill your bottle on those hot summer days- stay hydrated, people. We need you.");
noteTypeDescriptions
.put(11,
- "Anything else we should map to help your fellow cyclists? Share the details.");
+ "Anything else we should mMap to help your fellow cyclists? Share the details.");
}
@Override
@@ -117,9 +117,6 @@ public void clearSelection() {
@Override
public void onItemClick(AdapterView> parent, View view,
int position, long id) {
- // TODO Auto-generated method stub
- // view.setSelected(true);
- // view.setBackgroundDrawable(parent.getResources().getDrawable(R.drawable.bg_key));
clearSelection();
oldSelection = view;
view.setBackgroundColor(Color.parseColor("#ff33b5e5"));
diff --git a/src/edu/gatech/ppl/cycleatlanta/NoteUploader.java b/src/edu/gatech/ppl/cycleatlanta/NoteUploader.java
index 4a42c48..590c398 100644
--- a/src/edu/gatech/ppl/cycleatlanta/NoteUploader.java
+++ b/src/edu/gatech/ppl/cycleatlanta/NoteUploader.java
@@ -50,6 +50,8 @@
import android.widget.ListView;
import android.widget.Toast;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+
public class NoteUploader extends AsyncTask {
Context mCtx;
DbAdapter mDb;
@@ -232,27 +234,13 @@ public String getDeviceId() {
boolean uploadOneNote(long currentNoteId) {
boolean result = false;
- final String postUrl = "http://cycleatlanta.org/post_dev/";
-
- // byte[] postBodyDataZipped;
- //
- // BasicHttpEntity postBodyEntity;
- //
- // List nameValuePairs;
- // try {
- // postBodyEntity = getPostData(currentNoteId);
- // } catch (JSONException e) {
- // e.printStackTrace();
- // return result;
- // } catch (IOException e) {
- // e.printStackTrace();
- // return result;
- // }
- //
- // HttpClient client = new DefaultHttpClient();
- // // TODO: Server URL
- // final String postUrl = "http://cycleatlanta.org/post_dev/";
- // HttpPost postRequest = new HttpPost(postUrl);
+ ObaRegion currentRegion = Application.get().getCurrentRegion();
+ String postUrl;
+ if (currentRegion != null) {
+ postUrl = currentRegion.getBaseUrl();
+ } else {
+ postUrl = Application.get().getCustomApiUrl();
+ }
try {
diff --git a/src/edu/gatech/ppl/cycleatlanta/TabsConfig.java b/src/edu/gatech/ppl/cycleatlanta/TabsConfig.java
index 7c18477..39c50ee 100755
--- a/src/edu/gatech/ppl/cycleatlanta/TabsConfig.java
+++ b/src/edu/gatech/ppl/cycleatlanta/TabsConfig.java
@@ -141,25 +141,21 @@ public void onTabUnselected(ActionBar.Tab tab,
mViewPager.startActionMode(new ActionMode.Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- // TODO Auto-generated method stub
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
- // TODO Auto-generated method stub
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- // TODO Auto-generated method stub
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- // TODO Auto-generated method stub
return false;
}
});
diff --git a/src/edu/gatech/ppl/cycleatlanta/TripDetailActivity.java b/src/edu/gatech/ppl/cycleatlanta/TripDetailActivity.java
index bb346fb..8af1ac2 100755
--- a/src/edu/gatech/ppl/cycleatlanta/TripDetailActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/TripDetailActivity.java
@@ -68,7 +68,7 @@ void submit(String notesToUpload) {
Intent i = new Intent(getApplicationContext(), TabsConfig.class);
startActivity(i);
- // And, show the map!
+ // And, show the mMap!
xi.putExtra("showtrip", trip.tripid);
xi.putExtra("uploadTrip", true);
Log.v("Jason", "Tripid: " + String.valueOf(trip.tripid));
diff --git a/src/edu/gatech/ppl/cycleatlanta/TripMapActivity.java b/src/edu/gatech/ppl/cycleatlanta/TripMapActivity.java
index d604c96..981f4e2 100755
--- a/src/edu/gatech/ppl/cycleatlanta/TripMapActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/TripMapActivity.java
@@ -74,7 +74,7 @@ public void onCreate(Bundle savedInstanceState) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- // Toast.makeText(this, "trip map", Toast.LENGTH_LONG).show();
+ // Toast.makeText(this, "trip mMap", Toast.LENGTH_LONG).show();
try {
// Set zoom controls
@@ -100,12 +100,12 @@ public void onCreate(Bundle savedInstanceState) {
t2.setText(trip.info);
t3.setText(trip.fancystart);
- // Center & zoom the map
+ // Center & zoom the mMap
// int latcenter = (trip.lathigh + trip.latlow) / 2;
// int lgtcenter = (trip.lgthigh + trip.lgtlow) / 2;
// LatLng center = new LatLng(latcenter, lgtcenter);
- // map.animateCamera(CameraUpdateFactory.newLatLngZoom(center,16));
+ // mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(center,16));
// trip = trips[0]; // always get just the first trip
@@ -153,14 +153,14 @@ public void onCreate(Bundle savedInstanceState) {
Log.v("Jason", String.valueOf(gpspoints.size()));
// //startpoint
- // map.addMarker(new MarkerOptions()
+ // mMap.addMarker(new MarkerOptions()
// .icon(BitmapDescriptorFactory.fromResource(R.drawable.pingreen))
// .anchor(0.0f, 1.0f) // Anchors the marker on the bottom left
// .position(new LatLng(gpspoints.get(0).latitude*1E-6,
// gpspoints.get(0).longitude*1E-6)));
//
// //endpoint
- // map.addMarker(new MarkerOptions()
+ // mMap.addMarker(new MarkerOptions()
// .icon(BitmapDescriptorFactory.fromResource(R.drawable.pinpurple))
// .anchor(0.0f, 1.0f) // Anchors the marker on the bottom left
// .position(new
@@ -178,7 +178,7 @@ public void onCreate(Bundle savedInstanceState) {
polyline = map.addPolyline(rectOptions);
- // map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(),
+ // mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(),
// 480, 320, 10));
map.setOnCameraChangeListener(new OnCameraChangeListener() {
@@ -195,7 +195,7 @@ public void onCameraChange(CameraPosition arg0) {
// MapController mc = mapView.getController();
// mc.animateTo(center);
- // Add 500 to map span, to guarantee pins fit on map
+ // Add 500 to mMap span, to guarantee pins fit on mMap
// mc.zoomToSpan(500+trip.lathigh - trip.latlow, 500+trip.lgthigh -
// trip.lgtlow);
@@ -228,7 +228,7 @@ public void onCameraChange(CameraPosition arg0) {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && map != null) {
- // map.getOverlays().clear();
+ // mMap.getOverlays().clear();
polyline.remove();
}
return super.onKeyDown(keyCode, event);
@@ -300,7 +300,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
// mapOverlays.add(new PushPinOverlay(trip.endpoint, R.drawable.pinpurple));
// }
//
- // // Redraw the map
+ // // Redraw the mMap
// mapView.invalidate();
// }
// }
diff --git a/src/edu/gatech/ppl/cycleatlanta/TripPurposeActivity.java b/src/edu/gatech/ppl/cycleatlanta/TripPurposeActivity.java
index 337c4c5..432e31c 100755
--- a/src/edu/gatech/ppl/cycleatlanta/TripPurposeActivity.java
+++ b/src/edu/gatech/ppl/cycleatlanta/TripPurposeActivity.java
@@ -147,9 +147,6 @@ public void clearSelection() {
@Override
public void onItemClick(AdapterView> parent, View view,
int position, long id) {
- // TODO Auto-generated method stub
- // view.setSelected(true);
- // view.setBackgroundDrawable(parent.getResources().getDrawable(R.drawable.bg_key));
clearSelection();
oldSelection = view;
view.setBackgroundColor(Color.parseColor("#ff33b5e5"));
diff --git a/src/edu/gatech/ppl/cycleatlanta/TripUploader.java b/src/edu/gatech/ppl/cycleatlanta/TripUploader.java
index cf01ae3..b4776e2 100755
--- a/src/edu/gatech/ppl/cycleatlanta/TripUploader.java
+++ b/src/edu/gatech/ppl/cycleatlanta/TripUploader.java
@@ -66,6 +66,8 @@
import android.widget.ListView;
import android.widget.Toast;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+
public class TripUploader extends AsyncTask {
Context mCtx;
DbAdapter mDb;
@@ -116,7 +118,7 @@ private JSONObject getCoordsJSON(long tripId) throws JSONException {
mDb.openReadOnly();
Cursor tripCoordsCursor = mDb.fetchAllCoordsForTrip(tripId);
- // Build the map between JSON fieldname and phone db fieldname:
+ // Build the mMap between JSON fieldname and phone db fieldname:
Map fieldMap = new HashMap();
fieldMap.put(TRIP_COORDS_TIME,
tripCoordsCursor.getColumnIndex(DbAdapter.K_POINT_TIME));
@@ -371,8 +373,14 @@ boolean uploadOneTrip(long currentTripId) {
}
HttpClient client = new DefaultHttpClient();
- // TODO: Server URL
- final String postUrl = "http://cycleatlanta.org/post_dev/";
+ ObaRegion currentRegion = Application.get().getCurrentRegion();
+ String postUrl;
+ if (currentRegion != null) {
+ postUrl = currentRegion.getBaseUrl();
+ } else {
+ postUrl = Application.get().getCustomApiUrl();
+ }
+
HttpPost postRequest = new HttpPost(postUrl);
try {
diff --git a/src/edu/gatech/ppl/cycleatlanta/provider/ObaContract.java b/src/edu/gatech/ppl/cycleatlanta/provider/ObaContract.java
new file mode 100644
index 0000000..ce8a9a8
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/provider/ObaContract.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2010-2015 Paul Watts (paulcwatts@gmail.com),
+ * University of South Florida (sjbarbeau@gmail.com),
+ * Benjamin Du (bendu@me.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import edu.gatech.ppl.cycleatlanta.BuildConfig;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegionElement;
+
+/**
+ * The contract between clients and the ObaProvider.
+ *
+ * This really needs to be documented better.
+ *
+ * NOTE: The AUTHORITY names in this class cannot be changed. They need to stay under the
+ * BuildConfig.DATABASE_AUTHORITY namespace (for the original OBA brand, "com.joulespersecond.oba")
+ * namespace to support backwards compatibility with existing installed apps
+ *
+ * @author paulw
+ */
+public final class ObaContract {
+
+ public static final String TAG = "ObaContract";
+
+ /** The authority portion of the URI - defined in build.gradle */
+ public static final String AUTHORITY = BuildConfig.DATABASE_AUTHORITY;
+
+ /** The base URI for the Oba provider */
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ protected interface RegionsColumns {
+
+ /**
+ * The name of the region.
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The base OBA URL.
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String BASE_URL = "oba_base_url";
+
+ /**
+ * The email of the person responsible for this server.
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String CONTACT_EMAIL = "contact_email";
+
+
+ /**
+ * The Twitter URL for the region.
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String TWITTER_URL = "twitter_url";
+
+ public static final String FACEBOOK_URL = "facebook_url";
+
+ /**
+ * Whether or not the server is experimental (i.e., not production).
+ *
+ * Type: BOOLEAN
+ *
+ */
+ public static final String EXPERIMENTAL = "experimental";
+
+ /**
+ * The StopInfo URL for the region (see #103)
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String TUTORIAL_URL = "tutorial_url";
+ }
+
+ protected interface RegionBoundsColumns {
+
+ /**
+ * The region ID
+ *
+ * Type: INTEGER
+ *
+ */
+ public static final String REGION_ID = "region_id";
+
+ /**
+ * The latitude center of the agencies coverage area
+ *
+ * Type: REAL
+ *
+ */
+ public static final String LOWER_LEFT_LATITUDE = "lowerLeftLatitude";
+
+ /**
+ * The longitude center of the agencies coverage area
+ *
+ * Type: REAL
+ *
+ */
+ public static final String LOWER_LEFT_LONGITUDE = "lowerLeftLongitude";
+
+ /**
+ * The height of the agencies bounding box
+ *
+ * Type: REAL
+ *
+ */
+ public static final String UPPER_RIGHT_LATITUDE = "upperRightLatitude";
+
+ /**
+ * The width of the agencies bounding box
+ *
+ * Type: REAL
+ *
+ */
+ public static final String UPPER_RIGHT_LONGITUDE = "upperRightLongitude";
+
+ }
+
+ protected interface RegionOpen311ServersColumns {
+
+ /**
+ * The region ID
+ *
+ * Type: INTEGER
+ *
+ */
+ public static final String REGION_ID = "region_id";
+
+ /**
+ * The jurisdiction id of the open311 server
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String JURISDICTION = "jurisdiction";
+
+ /**
+ * The api key of the open311 server
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String API_KEY = "api_key";
+
+ /**
+ * The url of the open311 server
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String BASE_URL = "open311_base_url";
+
+ }
+
+ public static class Regions implements BaseColumns, RegionsColumns {
+
+ // Cannot be instantiated
+ private Regions() {
+ }
+
+ /** The URI path portion for this table */
+ public static final String PATH = "regions";
+
+ /**
+ * The content:// style URI for this table URI is of the form
+ * content:///regions/
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ AUTHORITY_URI, PATH);
+
+ public static final String CONTENT_TYPE
+ = "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".region";
+
+ public static final String CONTENT_DIR_TYPE
+ = "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".region";
+
+ public static final Uri buildUri(int id) {
+ return ContentUris.withAppendedId(CONTENT_URI, id);
+ }
+
+ public static Uri insertOrUpdate(Context context,
+ int id,
+ ContentValues values) {
+ return insertOrUpdate(context.getContentResolver(), id, values);
+ }
+
+ public static Uri insertOrUpdate(ContentResolver cr,
+ int id,
+ ContentValues values) {
+ final Uri uri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(id));
+ Cursor c = cr.query(uri, new String[]{}, null, null, null);
+ Uri result;
+ if (c != null && c.getCount() > 0) {
+ cr.update(uri, values, null, null);
+ result = uri;
+ } else {
+ values.put(_ID, id);
+ result = cr.insert(CONTENT_URI, values);
+ }
+ if (c != null) {
+ c.close();
+ }
+ return result;
+ }
+
+ public static ObaRegion get(Context context, int id) {
+ return get(context.getContentResolver(), id);
+ }
+
+ public static ObaRegion get(ContentResolver cr, int id) {
+ final String[] PROJECTION = {
+ _ID,
+ NAME,
+ BASE_URL,
+ CONTACT_EMAIL,
+ TWITTER_URL,
+ FACEBOOK_URL,
+ EXPERIMENTAL,
+ TUTORIAL_URL
+
+ };
+
+ Cursor c = cr.query(buildUri((int) id), PROJECTION, null, null, null);
+ if (c != null) {
+ try {
+ if (c.getCount() == 0) {
+ return null;
+ }
+ c.moveToFirst();
+ return new ObaRegionElement(id, // id
+ c.getString(1), // Name
+ true, // Active
+ c.getString(2), // OBA Base URL
+ RegionBounds.getRegion(cr, id), // Bounds
+ RegionOpen311Servers.getOpen311Server(cr, id), // Open311 servers
+ c.getString(3), // Contact Email
+ c.getString(4), // Twitter URL
+ c.getString(5), // Fb URL
+ c.getInt(6) > 0, // Experimental
+ c.getString(7)
+ );
+ } finally {
+ c.close();
+ }
+ }
+ return null;
+ }
+ }
+
+ public static class RegionBounds implements BaseColumns, RegionBoundsColumns {
+
+ // Cannot be instantiated
+ private RegionBounds() {
+ }
+
+ /** The URI path portion for this table */
+ public static final String PATH = "region_bounds";
+
+ /**
+ * The content:// style URI for this table URI is of the form
+ * content:///region_bounds/
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ AUTHORITY_URI, PATH);
+
+ public static final String CONTENT_TYPE
+ = "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".region_bounds";
+
+ public static final String CONTENT_DIR_TYPE
+ = "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".region_bounds";
+
+ public static final Uri buildUri(int id) {
+ return ContentUris.withAppendedId(CONTENT_URI, id);
+ }
+
+ public static ObaRegionElement.Bounds[] getRegion(ContentResolver cr, int regionId) {
+ final String[] PROJECTION = {
+ LOWER_LEFT_LATITUDE,
+ UPPER_RIGHT_LATITUDE,
+ LOWER_LEFT_LONGITUDE,
+ UPPER_RIGHT_LONGITUDE
+ };
+ Cursor c = cr.query(CONTENT_URI, PROJECTION,
+ "(" + RegionBounds.REGION_ID + " = " + regionId + ")",
+ null, null);
+ if (c != null) {
+ try {
+ ObaRegionElement.Bounds[] results = new ObaRegionElement.Bounds[c.getCount()];
+ if (c.getCount() == 0) {
+ return results;
+ }
+
+ int i = 0;
+ c.moveToFirst();
+ do {
+ results[i] = new ObaRegionElement.Bounds(
+ c.getDouble(0),
+ c.getDouble(1),
+ c.getDouble(2),
+ c.getDouble(3));
+ i++;
+ } while (c.moveToNext());
+
+ return results;
+ } finally {
+ c.close();
+ }
+ }
+ return null;
+ }
+ }
+
+ public static class RegionOpen311Servers implements BaseColumns, RegionOpen311ServersColumns {
+
+ // Cannot be instantiated
+ private RegionOpen311Servers() {
+ }
+
+ /** The URI path portion for this table */
+ public static final String PATH = "open311_servers";
+
+ /**
+ * The content:// style URI for this table URI is of the form
+ * content:///region_open311_servers/
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ AUTHORITY_URI, PATH);
+
+ public static final String CONTENT_TYPE
+ = "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".open311_servers";
+
+ public static final String CONTENT_DIR_TYPE
+ = "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".open311_servers";
+
+ public static final Uri buildUri(int id) {
+ return ContentUris.withAppendedId(CONTENT_URI, id);
+ }
+
+ public static ObaRegionElement.Open311Servers[] getOpen311Server
+ (ContentResolver cr, int regionId) {
+ final String[] PROJECTION = {
+ JURISDICTION,
+ API_KEY,
+ BASE_URL
+ };
+ Cursor c = cr.query(CONTENT_URI, PROJECTION,
+ "(" + RegionOpen311Servers.REGION_ID + " = " + regionId + ")",
+ null, null);
+ if (c != null) {
+ try {
+ ObaRegionElement.Open311Servers[] results = new ObaRegionElement.
+ Open311Servers[c.getCount()];
+ if (c.getCount() == 0) {
+ return results;
+ }
+
+ int i = 0;
+ c.moveToFirst();
+ do {
+ results[i] = new ObaRegionElement.Open311Servers(
+ c.getString(0),
+ c.getString(1),
+ c.getString(2));
+ i++;
+ } while (c.moveToNext());
+
+ return results;
+ } finally {
+ c.close();
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/provider/ObaProvider.java b/src/edu/gatech/ppl/cycleatlanta/provider/ObaProvider.java
new file mode 100644
index 0000000..30669ae
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/provider/ObaProvider.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2010-2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+
+import java.io.File;
+import java.util.HashMap;
+
+import edu.gatech.ppl.cycleatlanta.BuildConfig;
+
+public class ObaProvider extends ContentProvider {
+
+ /**
+ * The database name cannot be changed. It needs to remain the same to support backwards
+ * compatibility with existing installed apps
+ */
+ private static final String DATABASE_NAME = BuildConfig.APPLICATION_ID + ".db";
+
+ private class OpenHelper extends SQLiteOpenHelper {
+
+ private static final int DATABASE_VERSION = 1;
+
+ public OpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+// bootstrapDatabase(db);
+ onUpgrade(db, 12, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(
+ "CREATE TABLE " +
+ ObaContract.Regions.PATH + " (" +
+ ObaContract.Regions._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ ObaContract.Regions.NAME + " VARCHAR NOT NULL, " +
+ ObaContract.Regions.BASE_URL + " VARCHAR NOT NULL, " +
+ ObaContract.Regions.CONTACT_EMAIL + " VARCHAR NOT NULL, " +
+ ObaContract.Regions.TWITTER_URL + " VARCHAR NOT NULL, " +
+ ObaContract.Regions.FACEBOOK_URL + " VARCHAR NOT NULL, " +
+ ObaContract.Regions.EXPERIMENTAL + " INTEGER NOT NULL, " +
+ ObaContract.Regions.TUTORIAL_URL + " VARCHAR NOT NULL " +
+ ");");
+ db.execSQL(
+ "CREATE TABLE " +
+ ObaContract.RegionBounds.PATH + " (" +
+ ObaContract.RegionBounds._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ ObaContract.RegionBounds.REGION_ID + " INTEGER NOT NULL, " +
+ ObaContract.RegionBounds.LOWER_LEFT_LATITUDE + " REAL NOT NULL, " +
+ ObaContract.RegionBounds.UPPER_RIGHT_LATITUDE + " REAL NOT NULL, " +
+ ObaContract.RegionBounds.LOWER_LEFT_LONGITUDE + " REAL NOT NULL, " +
+ ObaContract.RegionBounds.UPPER_RIGHT_LONGITUDE + " REAL NOT NULL " +
+ ");");
+ db.execSQL(
+ "CREATE TABLE " +
+ ObaContract.RegionOpen311Servers.PATH + " (" +
+ ObaContract.RegionOpen311Servers._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ ObaContract.RegionOpen311Servers.REGION_ID + " INTEGER NOT NULL, " +
+ ObaContract.RegionOpen311Servers.JURISDICTION + " VARCHAR, " +
+ ObaContract.RegionOpen311Servers.API_KEY + " VARCHAR NOT NULL, " +
+ ObaContract.RegionOpen311Servers.BASE_URL + " VARCHAR NOT NULL " +
+ ");");
+ db.execSQL("DROP TRIGGER IF EXISTS region_bounds_cleanup");
+ db.execSQL(
+ "CREATE TRIGGER region_bounds_cleanup DELETE ON " + ObaContract.Regions.PATH
+ +
+ " BEGIN " +
+ "DELETE FROM " + ObaContract.RegionBounds.PATH +
+ " WHERE " + ObaContract.RegionBounds.REGION_ID + " = old."
+ + ObaContract.Regions._ID +
+ ";" +
+ "END");
+ }
+
+ private void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + ObaContract.Regions.PATH);
+ db.execSQL("DROP TABLE IF EXISTS " + ObaContract.RegionBounds.PATH);
+ db.execSQL("DROP TABLE IF EXISTS " + ObaContract.RegionOpen311Servers.PATH);
+ }
+ }
+
+ private static final int REGIONS = 12;
+
+ private static final int REGIONS_ID = 13;
+
+ private static final int REGION_BOUNDS = 14;
+
+ private static final int REGION_BOUNDS_ID = 15;
+
+ private static final int REGION_OPEN311_SERVERS = 17;
+
+ private static final int REGION_OPEN311_SERVERS_ID = 18;
+
+ private static final UriMatcher sUriMatcher;
+
+ private static final HashMap sRegionsProjectionMap;
+
+ private static final HashMap sRegionBoundsProjectionMap;
+
+ private static final HashMap sRegionOpen311ProjectionMap;
+
+ // Insert helpers are useful.
+ private DatabaseUtils.InsertHelper mRegionsInserter;
+
+ private DatabaseUtils.InsertHelper mRegionBoundsInserter;
+
+ private DatabaseUtils.InsertHelper mRegionOpen311ServersInserter;
+
+ static {
+ sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.Regions.PATH, REGIONS);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.Regions.PATH + "/#", REGIONS_ID);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.RegionBounds.PATH, REGION_BOUNDS);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.RegionBounds.PATH + "/#",
+ REGION_BOUNDS_ID);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.RegionOpen311Servers.PATH, REGION_OPEN311_SERVERS);
+ sUriMatcher.addURI(ObaContract.AUTHORITY, ObaContract.RegionOpen311Servers.PATH + "/#",
+ REGION_OPEN311_SERVERS_ID);
+
+
+ sRegionsProjectionMap = new HashMap();
+ sRegionsProjectionMap.put(ObaContract.Regions._ID, ObaContract.Regions._ID);
+ sRegionsProjectionMap.put(ObaContract.Regions.NAME, ObaContract.Regions.NAME);
+ sRegionsProjectionMap
+ .put(ObaContract.Regions.BASE_URL, ObaContract.Regions.BASE_URL);
+ sRegionsProjectionMap
+ .put(ObaContract.Regions.CONTACT_EMAIL, ObaContract.Regions.CONTACT_EMAIL);
+ sRegionsProjectionMap.put(ObaContract.Regions.TWITTER_URL, ObaContract.Regions.TWITTER_URL);
+ sRegionsProjectionMap.put(ObaContract.Regions.FACEBOOK_URL, ObaContract.Regions.FACEBOOK_URL);
+ sRegionsProjectionMap.put(ObaContract.Regions.TUTORIAL_URL, ObaContract.Regions.TUTORIAL_URL);
+ sRegionsProjectionMap
+ .put(ObaContract.Regions.EXPERIMENTAL, ObaContract.Regions.EXPERIMENTAL);
+
+ sRegionBoundsProjectionMap = new HashMap();
+ sRegionBoundsProjectionMap.put(ObaContract.RegionBounds._ID, ObaContract.RegionBounds._ID);
+ sRegionBoundsProjectionMap
+ .put(ObaContract.RegionBounds.REGION_ID, ObaContract.RegionBounds.REGION_ID);
+ sRegionBoundsProjectionMap
+ .put(ObaContract.RegionBounds.LOWER_LEFT_LATITUDE, ObaContract.RegionBounds.LOWER_LEFT_LATITUDE);
+ sRegionBoundsProjectionMap
+ .put(ObaContract.RegionBounds.UPPER_RIGHT_LATITUDE, ObaContract.RegionBounds.UPPER_RIGHT_LATITUDE);
+ sRegionBoundsProjectionMap
+ .put(ObaContract.RegionBounds.LOWER_LEFT_LONGITUDE, ObaContract.RegionBounds.LOWER_LEFT_LONGITUDE);
+ sRegionBoundsProjectionMap
+ .put(ObaContract.RegionBounds.UPPER_RIGHT_LONGITUDE, ObaContract.RegionBounds.UPPER_RIGHT_LONGITUDE);
+
+ sRegionOpen311ProjectionMap = new HashMap();
+ sRegionOpen311ProjectionMap
+ .put(ObaContract.RegionOpen311Servers._ID, ObaContract.RegionOpen311Servers._ID);
+ sRegionOpen311ProjectionMap
+ .put(ObaContract.RegionOpen311Servers.REGION_ID, ObaContract.RegionOpen311Servers.REGION_ID);
+ sRegionOpen311ProjectionMap
+ .put(ObaContract.RegionOpen311Servers.JURISDICTION, ObaContract.RegionOpen311Servers.JURISDICTION);
+ sRegionOpen311ProjectionMap
+ .put(ObaContract.RegionOpen311Servers.API_KEY, ObaContract.RegionOpen311Servers.API_KEY);
+ sRegionOpen311ProjectionMap
+ .put(ObaContract.RegionOpen311Servers.BASE_URL, ObaContract.RegionOpen311Servers.BASE_URL);
+
+ }
+
+ private SQLiteDatabase mDb;
+
+ private OpenHelper mOpenHelper;
+
+ public static File getDatabasePath(Context context) {
+ return context.getDatabasePath(DATABASE_NAME);
+ }
+
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new OpenHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = sUriMatcher.match(uri);
+ switch (match) {
+ case REGIONS:
+ return ObaContract.Regions.CONTENT_DIR_TYPE;
+ case REGIONS_ID:
+ return ObaContract.Regions.CONTENT_TYPE;
+ case REGION_BOUNDS:
+ return ObaContract.RegionBounds.CONTENT_DIR_TYPE;
+ case REGION_BOUNDS_ID:
+ return ObaContract.RegionBounds.CONTENT_TYPE;
+ case REGION_OPEN311_SERVERS:
+ return ObaContract.RegionOpen311Servers.CONTENT_DIR_TYPE;
+ case REGION_OPEN311_SERVERS_ID:
+ return ObaContract.RegionOpen311Servers.CONTENT_TYPE;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = getDatabase();
+ db.beginTransaction();
+ try {
+ Uri result = insertInternal(db, uri, values);
+ getContext().getContentResolver().notifyChange(uri, null);
+ db.setTransactionSuccessful();
+ return result;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ final SQLiteDatabase db = getDatabase();
+ return queryInternal(db, uri, projection, selection, selectionArgs, sortOrder);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ final SQLiteDatabase db = getDatabase();
+ db.beginTransaction();
+ try {
+ int result = updateInternal(db, uri, values, selection, selectionArgs);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+ db.setTransactionSuccessful();
+ return result;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = getDatabase();
+ db.beginTransaction();
+ try {
+ int result = deleteInternal(db, uri, selection, selectionArgs);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+ db.setTransactionSuccessful();
+ return result;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private Uri insertInternal(SQLiteDatabase db, Uri uri, ContentValues values) {
+ final int match = sUriMatcher.match(uri);
+ String id;
+ Uri result;
+ long longId;
+
+ switch (match) {
+
+ case REGIONS:
+ longId = mRegionsInserter.insert(values);
+ result = ContentUris.withAppendedId(ObaContract.Regions.CONTENT_URI, longId);
+ return result;
+
+ case REGION_BOUNDS:
+ longId = mRegionBoundsInserter.insert(values);
+ result = ContentUris.withAppendedId(ObaContract.RegionBounds.CONTENT_URI, longId);
+ return result;
+
+ case REGION_OPEN311_SERVERS:
+ longId = mRegionOpen311ServersInserter.insert(values);
+ result = ContentUris.withAppendedId(ObaContract.RegionOpen311Servers.CONTENT_URI, longId);
+ return result;
+
+ // What would these mean, anyway??
+ case REGIONS_ID:
+ case REGION_BOUNDS_ID:
+ throw new UnsupportedOperationException("Cannot insert to this URI: " + uri);
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ private Cursor queryInternal(SQLiteDatabase db,
+ Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ final int match = sUriMatcher.match(uri);
+ final String limit = uri.getQueryParameter("limit");
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ switch (match) {
+
+ case REGIONS:
+ qb.setTables(ObaContract.Regions.PATH);
+ qb.setProjectionMap(sRegionsProjectionMap);
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ case REGIONS_ID:
+ qb.setTables(ObaContract.Regions.PATH);
+ qb.setProjectionMap(sRegionsProjectionMap);
+ qb.appendWhere(ObaContract.Regions._ID);
+ qb.appendWhere("=");
+ qb.appendWhere(String.valueOf(ContentUris.parseId(uri)));
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ case REGION_BOUNDS:
+ qb.setTables(ObaContract.RegionBounds.PATH);
+ qb.setProjectionMap(sRegionBoundsProjectionMap);
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ case REGION_BOUNDS_ID:
+ qb.setTables(ObaContract.RegionBounds.PATH);
+ qb.setProjectionMap(sRegionBoundsProjectionMap);
+ qb.appendWhere(ObaContract.RegionBounds._ID);
+ qb.appendWhere("=");
+ qb.appendWhere(String.valueOf(ContentUris.parseId(uri)));
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ case REGION_OPEN311_SERVERS:
+ qb.setTables(ObaContract.RegionOpen311Servers.PATH);
+ qb.setProjectionMap(sRegionOpen311ProjectionMap);
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ case REGION_OPEN311_SERVERS_ID:
+ qb.setTables(ObaContract.RegionOpen311Servers.PATH);
+ qb.setProjectionMap(sRegionOpen311ProjectionMap);
+ qb.appendWhere(ObaContract.RegionOpen311Servers._ID);
+ qb.appendWhere("=");
+ qb.appendWhere(String.valueOf(ContentUris.parseId(uri)));
+ return qb.query(mDb, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ private int updateInternal(SQLiteDatabase db,
+ Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case REGIONS:
+ return db.update(ObaContract.Regions.PATH, values, selection, selectionArgs);
+
+ case REGIONS_ID:
+ return db.update(ObaContract.Regions.PATH, values,
+ whereLong(ObaContract.Regions._ID, uri), selectionArgs);
+
+ case REGION_BOUNDS:
+ return db.update(ObaContract.RegionBounds.PATH, values, selection, selectionArgs);
+
+ case REGION_BOUNDS_ID:
+ return db.update(ObaContract.RegionBounds.PATH, values,
+ whereLong(ObaContract.RegionBounds._ID, uri), selectionArgs);
+
+ case REGION_OPEN311_SERVERS:
+ return db.update(ObaContract.RegionOpen311Servers.PATH, values, selection, selectionArgs);
+
+ case REGION_OPEN311_SERVERS_ID:
+ return db.update(ObaContract.RegionOpen311Servers.PATH, values,
+ whereLong(ObaContract.RegionOpen311Servers._ID, uri), selectionArgs);
+
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ private int deleteInternal(SQLiteDatabase db,
+ Uri uri, String selection, String[] selectionArgs) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+
+ case REGIONS:
+ return db.delete(ObaContract.Regions.PATH, selection, selectionArgs);
+
+ case REGIONS_ID:
+ return db.delete(ObaContract.Regions.PATH,
+ whereLong(ObaContract.Regions._ID, uri), selectionArgs);
+
+ case REGION_BOUNDS:
+ return db.delete(ObaContract.RegionBounds.PATH, selection, selectionArgs);
+
+ case REGION_BOUNDS_ID:
+ return db.delete(ObaContract.RegionBounds.PATH,
+ whereLong(ObaContract.RegionBounds._ID, uri), selectionArgs);
+
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ private String where(String column, Uri uri) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(column);
+ sb.append('=');
+ DatabaseUtils.appendValueToSql(sb, uri.getLastPathSegment());
+ return sb.toString();
+ }
+
+ private String whereLong(String column, Uri uri) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(column);
+ sb.append('=');
+ sb.append(String.valueOf(ContentUris.parseId(uri)));
+ return sb.toString();
+ }
+
+ private SQLiteDatabase getDatabase() {
+ if (mDb == null) {
+ mDb = mOpenHelper.getWritableDatabase();
+ // Initialize the insert helpers
+ mRegionsInserter = new DatabaseUtils.InsertHelper(mDb, ObaContract.Regions.PATH);
+ mRegionBoundsInserter = new DatabaseUtils.InsertHelper(mDb,
+ ObaContract.RegionBounds.PATH);
+ mRegionOpen311ServersInserter = new DatabaseUtils.InsertHelper(mDb,
+ ObaContract.RegionOpen311Servers.PATH);
+ }
+ return mDb;
+ }
+
+ //
+ // Closes the database
+ //
+ public void closeDB() {
+ mOpenHelper.close();
+ mDb = null;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/JacksonSerializer.java b/src/edu/gatech/ppl/cycleatlanta/region/JacksonSerializer.java
new file mode 100644
index 0000000..e4d9b86
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/JacksonSerializer.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010-2012 Paul Watts (paulcwatts@gmail.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 edu.gatech.ppl.cycleatlanta.region;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.MappingJsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.fasterxml.jackson.databind.node.TreeTraversingParser;
+
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+
+public class JacksonSerializer implements ObaApi.SerializationHandler {
+
+ private static final String TAG = "JacksonSerializer";
+
+ private static class SingletonHolder {
+
+ public static final JacksonSerializer INSTANCE = new JacksonSerializer();
+ }
+
+ private static final ObjectMapper mMapper = new ObjectMapper();
+
+ static {
+ mMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ mMapper.setVisibilityChecker(
+ VisibilityChecker.Std.defaultInstance()
+ .withFieldVisibility(JsonAutoDetect.Visibility.ANY));
+ }
+
+ private JacksonSerializer() { /* singleton */ }
+
+ /**
+ * Make the singleton instance available
+ */
+ public static ObaApi.SerializationHandler getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ private static JsonParser getJsonParser(Reader reader)
+ throws IOException, JsonProcessingException {
+ TreeTraversingParser parser = new TreeTraversingParser(mMapper.readTree(reader));
+ parser.setCodec(mMapper);
+ return parser;
+ }
+
+ public String toJson(String input) {
+ TextNode node = JsonNodeFactory.instance.textNode(input);
+ return node.toString();
+ }
+
+ @Override
+ public T createFromError(Class cls, int code, String error) {
+ // This is not very efficient, but it's an error case and it's easier
+ // than instantiating one ourselves.
+ final String jsonErr = toJson(error);
+ final String json = getErrorJson(code, jsonErr);
+
+ try {
+ // Hopefully this never returns null or throws.
+ return mMapper.readValue(json, cls);
+ } catch (JsonParseException e) {
+ Log.e(TAG, e.toString());
+ } catch (JsonMappingException e) {
+ Log.e(TAG, e.toString());
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ }
+ return null;
+ }
+
+ private String getErrorJson(int code, final String jsonErr) {
+ return String.format("{\"code\": %d,\"version\":\"2\",\"text\":%s}", code, jsonErr);
+ }
+
+ public T deserialize(Reader reader, Class cls) {
+ try {
+ T t = getJsonParser(reader).readValueAs(cls);
+ if (t == null) {
+ t = createFromError(cls, ObaApi.OBA_INTERNAL_ERROR, "Json error");
+ }
+ return t;
+ } catch (FileNotFoundException e) {
+ return createFromError(cls, ObaApi.OBA_NOT_FOUND, e.toString());
+ } catch (JsonProcessingException e) {
+ return createFromError(cls, ObaApi.OBA_INTERNAL_ERROR, e.toString());
+ } catch (IOException e) {
+ return createFromError(cls, ObaApi.OBA_IO_EXCEPTION, e.toString());
+ }
+ }
+
+ public String serialize(Object obj) {
+ StringWriter writer = new StringWriter();
+ JsonGenerator jsonGenerator;
+
+ try {
+ jsonGenerator = new MappingJsonFactory().createJsonGenerator(writer);
+ mMapper.writeValue(jsonGenerator, obj);
+
+ return writer.toString();
+
+ } catch (JsonGenerationException e) {
+ Log.e(TAG, e.toString());
+ return getErrorJson(ObaApi.OBA_INTERNAL_ERROR, e.toString());
+ } catch (JsonMappingException e) {
+ Log.e(TAG, e.toString());
+ return getErrorJson(ObaApi.OBA_INTERNAL_ERROR, e.toString());
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ return getErrorJson(ObaApi.OBA_IO_EXCEPTION, e.toString());
+ }
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaApi.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaApi.java
new file mode 100644
index 0000000..5592808
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaApi.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import java.io.Reader;
+
+public final class ObaApi {
+
+ private ObaApi() {
+ throw new AssertionError();
+ }
+
+ public static final int OBA_OK = 200;
+
+ public static final int OBA_NOT_FOUND = 404;
+
+ public static final int OBA_INTERNAL_ERROR = 500;
+
+ public static final int OBA_IO_EXCEPTION = 700;
+
+ public static final String VERSION1 = "1";
+
+ private static final ObaContext mDefaultContext = new ObaContext();
+
+ public static ObaContext getDefaultContext() {
+ return mDefaultContext;
+ }
+
+ public interface SerializationHandler {
+
+ T deserialize(Reader reader, Class cls);
+
+ String serialize(Object obj);
+
+ T createFromError(Class cls, int code, String error);
+ }
+
+ public static final SerializationHandler getSerializer(Class cls) {
+ return JacksonSerializer.getInstance();
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaConnection.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaConnection.java
new file mode 100644
index 0000000..d1f520f
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaConnection.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * Implements a basic connection object for ObaRequests.
+ * These are created by the ObaConnectionFactory class.
+ *
+ * Under normal circumstances this is always implemented by
+ * the ObaDefaultConnection class. In the unit tests, it is
+ * replaced by the ObaMockConnection class.
+ *
+ * @author paulw
+ */
+public interface ObaConnection {
+
+ public void disconnect();
+
+ public Reader get() throws IOException;
+
+ public Reader post(String string) throws IOException;
+
+ public int getResponseCode() throws IOException;
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaConnectionFactory.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaConnectionFactory.java
new file mode 100644
index 0000000..3f6c367
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaConnectionFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.net.Uri;
+
+import java.io.IOException;
+
+public interface ObaConnectionFactory {
+
+ public ObaConnection newConnection(Uri uri) throws IOException;
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaContext.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaContext.java
new file mode 100644
index 0000000..c485824
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaContext.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+import edu.gatech.ppl.cycleatlanta.R;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+
+public class ObaContext {
+
+ private static final String TAG = "ObaContext";
+
+ private String mApiKey = "v1_BktoDJ2gJlu6nLM6LsT9H8IUbWc=cGF1bGN3YXR0c0BnbWFpbC5jb20=";
+
+ private int mAppVer = 0;
+
+ private String mAppUid = null;
+
+ private ObaConnectionFactory mConnectionFactory = ObaDefaultConnectionFactory.getInstance();
+
+ private ObaRegion mRegion;
+
+ public ObaContext() {
+ }
+
+ public void setAppInfo(int version, String uuid) {
+ mAppVer = version;
+ mAppUid = uuid;
+ }
+
+ public void setAppInfo(Uri.Builder builder) {
+ if (mAppVer != 0) {
+ builder.appendQueryParameter("app_ver", String.valueOf(mAppVer));
+ }
+ if (mAppUid != null) {
+ builder.appendQueryParameter("app_uid", mAppUid);
+ }
+ }
+
+ public void setApiKey(String apiKey) {
+ mApiKey = apiKey;
+ }
+
+ public String getApiKey() {
+ return mApiKey;
+ }
+
+ public void setRegion(ObaRegion region) {
+ mRegion = region;
+ }
+
+ public ObaRegion getRegion() {
+ return mRegion;
+ }
+
+ /**
+ * Connection factory
+ *
+ */
+ public ObaConnectionFactory setConnectionFactory(ObaConnectionFactory factory) {
+ ObaConnectionFactory prev = mConnectionFactory;
+ mConnectionFactory = factory;
+ return prev;
+ }
+
+ public ObaConnectionFactory getConnectionFactory() {
+ return mConnectionFactory;
+ }
+
+ public void setBaseUrl(Context context, Uri.Builder builder) {
+ // If there is a custom preference, then use that.
+ String serverName = Application.get().getCustomApiUrl();
+
+ if (!TextUtils.isEmpty(serverName) || mRegion != null) {
+ Uri baseUrl = null;
+ if (!TextUtils.isEmpty(serverName)) {
+ Log.d(TAG, "Using custom API URL set by user '" + serverName + "'.");
+
+ try {
+ // URI.parse() doesn't tell us if the scheme is missing, so use URL() instead (#126)
+ URL url = new URL(serverName);
+ } catch (MalformedURLException e) {
+ // Assume HTTP scheme, since without a scheme the Uri won't parse the authority
+ serverName = context.getString(R.string.http_prefix) + serverName;
+ }
+
+ baseUrl = Uri.parse(serverName);
+ } else if (mRegion != null) {
+ Log.d(TAG, "Using region base URL '" + mRegion.getBaseUrl() + "'.");
+
+ baseUrl = Uri.parse(mRegion.getBaseUrl());
+ }
+
+ // Copy partial path (if one exists) from the base URL
+ Uri.Builder path = new Uri.Builder();
+ path.encodedPath(baseUrl.getEncodedPath());
+
+ // Then, tack on the rest of the REST API method path from the Uri.Builder that was passed in
+ path.appendEncodedPath(builder.build().getPath());
+
+ // Finally, overwrite builder that was passed in with the full URL
+ builder.scheme(baseUrl.getScheme());
+ builder.encodedAuthority(baseUrl.getEncodedAuthority());
+ builder.encodedPath(path.build().getEncodedPath());
+ } else {
+ String fallBack = "api.pugetsound.onebusaway.org";
+ Log.e(TAG, "Accessing default fallback '" + fallBack + "' ...this is wrong!!");
+ // Current fallback for existing users?
+ builder.scheme("http");
+ builder.authority(fallBack);
+ }
+ }
+
+ @Override
+ public ObaContext clone() {
+ ObaContext result = new ObaContext();
+ result.setApiKey(mApiKey);
+ result.setAppInfo(mAppVer, mAppUid);
+ result.setConnectionFactory(mConnectionFactory);
+ return result;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnection.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnection.java
new file mode 100644
index 0000000..4219850
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnection.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010-2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public final class ObaDefaultConnection implements ObaConnection {
+
+ private static final String TAG = "ObaDefaultConnection";
+
+ private HttpURLConnection mConnection;
+
+ ObaDefaultConnection(Uri uri) throws IOException {
+ Log.d(TAG, uri.toString());
+ URL url = new URL(uri.toString());
+ mConnection = (HttpURLConnection) url.openConnection();
+ mConnection.setReadTimeout(30 * 1000);
+ }
+
+ @Override
+ public void disconnect() {
+ mConnection.disconnect();
+ }
+
+ @Override
+ public Reader get() throws IOException {
+ return new InputStreamReader(
+ new BufferedInputStream(mConnection.getInputStream(), 8 * 1024));
+ }
+
+ @Override
+ public Reader post(String string) throws IOException {
+ byte[] data = string.getBytes();
+
+ mConnection.setDoOutput(true);
+ mConnection.setFixedLengthStreamingMode(data.length);
+ mConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+
+ // Set the output stream
+ OutputStream stream = mConnection.getOutputStream();
+ stream.write(data);
+ stream.flush();
+ stream.close();
+
+ return new InputStreamReader(
+ new BufferedInputStream(mConnection.getInputStream(), 8 * 1024));
+ }
+
+ @Override
+ public int getResponseCode() throws IOException {
+ return mConnection.getResponseCode();
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnectionFactory.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnectionFactory.java
new file mode 100644
index 0000000..d155662
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaDefaultConnectionFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.net.Uri;
+
+import java.io.IOException;
+
+public class ObaDefaultConnectionFactory implements ObaConnectionFactory {
+
+ private ObaDefaultConnectionFactory() {
+ }
+
+ private static class SingletonHolder {
+
+ public static final ObaDefaultConnectionFactory INSTANCE
+ = new ObaDefaultConnectionFactory();
+ }
+
+ public static ObaDefaultConnectionFactory getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ @Override
+ public ObaConnection newConnection(Uri uri) throws IOException {
+ return new ObaDefaultConnection(uri);
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsLoader.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsLoader.java
new file mode 100644
index 0000000..5aaa962
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsLoader.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012-2013 Paul Watts (paulcwatts@gmail.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 edu.gatech.ppl.cycleatlanta.region;
+
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+import java.util.ArrayList;
+
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.utils.RegionUtils;
+
+public class ObaRegionsLoader extends AsyncTaskLoader> {
+ //private static final String TAG = "ObaRegionsLoader";
+
+ private Context mContext;
+
+ private ArrayList mResults;
+
+ private final boolean mForceReload;
+
+ public ObaRegionsLoader(Context context) {
+ super(context);
+ this.mContext = context;
+ mForceReload = false;
+ }
+
+ /**
+ * @param context The context.
+ * @param force Forces loading the regions from the remote repository.
+ */
+ public ObaRegionsLoader(Context context, boolean force) {
+ super(context);
+ this.mContext = context;
+ mForceReload = force;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mResults != null) {
+ deliverResult(mResults);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public ArrayList loadInBackground() {
+ return RegionUtils.getRegions(mContext, mForceReload);
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsRequest.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsRequest.java
new file mode 100644
index 0000000..a318214
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsRequest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 Paul Watts (paulcwatts@gmail.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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.Callable;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+import edu.gatech.ppl.cycleatlanta.R;
+
+/**
+ * Retrieves the current list of OneBusAway regions.
+ * {https://github.com/OneBusAway/onebusaway/wiki/Multi-Region#regions-rest-api}
+ *
+ * @author Paul Watts (paulcwatts@gmail.com)
+ */
+public final class ObaRegionsRequest extends RequestBase implements
+ Callable {
+
+ protected ObaRegionsRequest(Uri uri) {
+ super(uri);
+ }
+
+ //
+ // This currently has a very simple builder because you can't do much with this "API"
+ //
+ public static class Builder {
+
+ private static Uri URI = Uri
+ .parse(Application.get().getResources().getString(R.string.regions_api_url));
+
+ public Builder(Context context) {
+ }
+
+ public Builder(Context context, Uri uri) {
+ URI = uri;
+ }
+
+ public ObaRegionsRequest build() {
+ return new ObaRegionsRequest(URI);
+ }
+ }
+
+ /**
+ * Helper method for constructing new instances.
+ *
+ * @param context The package context.
+ * @return The new request instance.
+ */
+ public static ObaRegionsRequest newRequest(Context context) {
+ return new Builder(context).build();
+ }
+
+ /**
+ * Helper method for constructing new instances, allowing
+ * the requester to set the URI to retrieve the regions info
+ * from
+ *
+ * @param context The package context.
+ * @param uri URI to the regions file
+ * @return The new request instance.
+ */
+ public static ObaRegionsRequest newRequest(Context context, Uri uri) {
+ return new Builder(context, uri).build();
+ }
+
+ @Override
+ public ObaRegionsResponse call() {
+ //If the URI is for an Android resource then get from resource, otherwise get from Region REST API
+ if (mUri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
+ return getRegionFromResource();
+ } else {
+ return call(ObaRegionsResponse.class);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ObaRegionsRequest [mUri=" + mUri + "]";
+ }
+
+ private ObaRegionsResponse getRegionFromResource() {
+ ObaRegionsResponse response = null;
+
+ InputStream is = Application.get().getApplicationContext().getResources()
+ .openRawResource(R.raw.regions_v3);
+ ObaApi.SerializationHandler handler = ObaApi.getSerializer(ObaRegionsResponse.class);
+ response = handler.deserialize(new InputStreamReader(is), ObaRegionsResponse.class);
+ if (response == null) {
+ response = handler.createFromError(ObaRegionsResponse.class, ObaApi.OBA_INTERNAL_ERROR,
+ "Json error");
+ }
+
+ return response;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsResponse.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsResponse.java
new file mode 100644
index 0000000..51e5c93
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsResponse.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegionElement;
+
+public class ObaRegionsResponse extends ObaResponse {
+
+ private final ObaRegionElement[] list = ObaRegionElement.EMPTY_ARRAY;
+
+ public ObaRegion[] getRegions() {
+ return list;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsTask.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsTask.java
new file mode 100644
index 0000000..d49a49e
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaRegionsTask.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2012-2015 Paul Watts (paulcwatts@gmail.com), University of South Florida
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.api.GoogleApiClient;
+
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.location.Location;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+import edu.gatech.ppl.cycleatlanta.R;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.utils.LocationUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.RegionUtils;
+import edu.gatech.ppl.cycleatlanta.region.utils.UIUtils;
+
+/**
+ * AsyncTask used to refresh region info from the Regions REST API.
+ *
+ * Classes utilizing this task can request a callback via MapModeController.Callback.setMyLocation()
+ * by passing in class implementing MapModeController.Callback in the constructor
+ *
+ * @author barbeau
+ */
+public class ObaRegionsTask extends AsyncTask> {
+
+ public interface Callback {
+
+ /**
+ * Called when the ObaRegionsTask is complete
+ *
+ * @param currentRegionChanged true if the current region changed as a result of the task,
+ * false if it didn't change
+ */
+ public void onRegionTaskFinished(boolean currentRegionChanged);
+ }
+
+ private static final String TAG = "ObaRegionsTask";
+
+ private final int CALLBACK_DELAY = 100; //in milliseconds
+
+ private Context mContext;
+
+ private ProgressDialog mProgressDialog;
+
+ private ObaRegionsTask.Callback mCallback;
+
+ private final boolean mForceReload;
+
+ private final boolean mShowProgressDialog;
+
+ /**
+ * GoogleApiClient being used for Location Services
+ */
+ GoogleApiClient mGoogleApiClient;
+
+ /**
+ * @param callback a callback will be made via this interface after the task is complete
+ * (null if no callback is requested)
+ */
+ public ObaRegionsTask(Context context, ObaRegionsTask.Callback callback) {
+ this.mContext = context;
+ this.mCallback = callback;
+ mForceReload = false;
+ mShowProgressDialog = true;
+ }
+
+ /**
+ * @param callback a callback will be made via this interface after the task is
+ * complete
+ * (null if no callback is requested)
+ * @param force true if the task should be forced to update region info from the
+ * server, false if it can return local info
+ * @param showProgressDialog true if a progress dialog should be shown to the user during the
+ * task, false if it should not
+ */
+ public ObaRegionsTask(Context context, ObaRegionsTask.Callback callback, boolean force,
+ boolean showProgressDialog) {
+ this.mContext = context;
+ this.mCallback = callback;
+ mForceReload = force;
+ mShowProgressDialog = showProgressDialog;
+ if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(context)
+ == ConnectionResult.SUCCESS) {
+ mGoogleApiClient = LocationUtils.getGoogleApiClientWithCallbacks(context);
+ mGoogleApiClient.connect();
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ if (mShowProgressDialog && UIUtils.canManageDialog(mContext)) {
+ mProgressDialog = ProgressDialog.show(mContext, "",
+ mContext.getString(R.string.region_detecting_server), true);
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ super.onPreExecute();
+ }
+
+ @Override
+ protected ArrayList doInBackground(Void... params) {
+ return RegionUtils.getRegions(mContext, mForceReload);
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList results) {
+ if (results == null) {
+ //This is a catastrophic failure to load region info from all sources
+ return;
+ }
+
+ // Dismiss the dialog before calling the callbacks to avoid errors referencing the dialog later
+ if (mShowProgressDialog && UIUtils.canManageDialog(mContext) && mProgressDialog
+ .isShowing()) {
+ mProgressDialog.dismiss();
+ }
+
+ SharedPreferences settings = Application.getPrefs();
+
+ if (settings
+ .getBoolean(mContext.getString(R.string.preference_key_auto_select_region), true)) {
+ // Pass in the GoogleApiClient initialized in constructor
+ Location myLocation = Application.getLastKnownLocation(mContext, mGoogleApiClient);
+
+ ObaRegion closestRegion = RegionUtils.getClosestRegion(results, myLocation, true);
+
+ if (Application.get().getCurrentRegion() == null) {
+ if (closestRegion != null) {
+ //No region has been set, so set region application-wide to closest region
+ Application.get().setCurrentRegion(closestRegion);
+ Log.d(TAG, "Detected closest region '" + closestRegion.getName() + "'");
+
+ doCallback(true);
+ } else {
+ //No region has been set, and we couldn't find a usable region based on RegionUtil.isRegionUsable()
+ //or we couldn't find a closest a region, so ask the user to pick the region
+ haveUserChooseRegion(results);
+ }
+ } else if (Application.get().getCurrentRegion() != null && closestRegion != null
+ && !Application.get().getCurrentRegion().equals(closestRegion)) {
+ //User is closer to a different region than the current region, so change to the closest region
+ String oldRegionName = Application.get().getCurrentRegion().getName();
+ Application.get().setCurrentRegion(closestRegion);
+ Log.d(TAG, "Detected closer region '" + closestRegion.getName()
+ + "', changed to this region.");
+
+ doCallback(true);
+ } else if (Application.get().getCurrentRegion() != null && closestRegion != null
+ && Application.get().getCurrentRegion().equals(closestRegion)) {
+ //User is closer to a different region than the current region, so change to the closest region
+ Application.get().setCurrentRegion(closestRegion);
+ doCallback(false);
+ } else {
+ doCallback(false);
+ }
+ } else {
+ if (Application.get().getCurrentRegion() == null) {
+ //We don't have a region selected, and the user chose not to auto-select one, so make them pick one
+ haveUserChooseRegion(results);
+ } else {
+ doCallback(false);
+ }
+ }
+
+ // Tear down Location Services client
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.disconnect();
+ }
+
+ super.onPostExecute(results);
+ }
+
+ private void haveUserChooseRegion(final ArrayList result) {
+ if (!UIUtils.canManageDialog(mContext)) {
+ return;
+ }
+
+ // Create dialog for user to choose
+ List serverNames = new ArrayList();
+ for (ObaRegion region : result) {
+ if (RegionUtils.isRegionUsable(region)) {
+ serverNames.add(region.getName());
+ }
+ }
+
+ Collections.sort(serverNames);
+
+ final CharSequence[] items = serverNames
+ .toArray(new CharSequence[serverNames.size()]);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(mContext.getString(R.string.region_choose_region));
+ builder.setCancelable(false);
+ builder.setItems(items, new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int item) {
+ for (ObaRegion region : result) {
+ if (region.getName().equals(items[item])) {
+ //Set the region application-wide
+ Application.get().setCurrentRegion(region);
+ Log.d(TAG, "User chose region '" + items[item] + "'.");
+ doCallback(true);
+ break;
+ }
+ }
+ }
+ });
+
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ private void doCallback(final boolean currentRegionChanged) {
+ //If we execute on same thread immediately after setting Region, map UI may try to call
+ //OBA REST API before the new region info is set in Application. So, pause briefly.
+ final Handler mPauseForCallbackHandler = new Handler();
+ final Runnable mPauseForCallback = new Runnable() {
+ public void run() {
+ //Map may not have triggered call to OBA REST API, so we force one here
+ if (mCallback != null) {
+ mCallback.onRegionTaskFinished(currentRegionChanged);
+ }
+ }
+ };
+ mPauseForCallbackHandler.postDelayed(mPauseForCallback,
+ CALLBACK_DELAY);
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/ObaResponse.java b/src/edu/gatech/ppl/cycleatlanta/region/ObaResponse.java
new file mode 100644
index 0000000..0549706
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/ObaResponse.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+/**
+ * Base class for response objects.
+ *
+ * @author Paul Watts (paulcwatts@gmail.com)
+ */
+public class ObaResponse {
+
+ private final String version;
+
+ private final int code;
+
+ private final long currentTime;
+
+ private final String text;
+
+ protected ObaResponse() {
+ version = "1";
+ code = 0;
+ currentTime = 0;
+ text = "ERROR";
+ }
+
+ /**
+ * @return The version of this response.
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * @return The status code (one of the ObaApi.OBA_ constants)
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * @return The status text.
+ */
+
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * @return The current system time on the API server
+ * as milliseconds since the epoch.
+ */
+ public long getCurrentTime() {
+ return currentTime;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/RequestBase.java b/src/edu/gatech/ppl/cycleatlanta/region/RequestBase.java
new file mode 100644
index 0000000..80c9c98
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/RequestBase.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2010-2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+
+/**
+ * The base class for Oba requests.
+ *
+ * @author Paul Watts (paulcwatts@gmail.com)
+ */
+public class RequestBase {
+
+ private static final String TAG = "RequestBase";
+
+ protected final Uri mUri;
+
+ protected final String mPostData;
+
+ protected RequestBase(Uri uri) {
+ mUri = uri;
+ mPostData = null;
+ }
+
+ protected RequestBase(Uri uri, String postData) {
+ mUri = uri;
+ mPostData = postData;
+ }
+
+ public static class BuilderBase {
+
+ protected static final String BASE_PATH = "api/where";
+
+ protected final Uri.Builder mBuilder;
+
+ protected ObaContext mObaContext;
+
+ protected Context mContext;
+
+ protected BuilderBase(Context context, String path) {
+ this(context, null, path);
+ }
+
+ protected BuilderBase(Context context, ObaContext obaContext, String path) {
+ mContext = context;
+ mObaContext = obaContext;
+ mBuilder = new Uri.Builder();
+ mBuilder.path(path);
+ }
+
+ protected static String getPathWithId(String pathElement, String id) {
+ StringBuilder builder = new StringBuilder(BASE_PATH);
+ builder.append(pathElement);
+ builder.append(Uri.encode(id));
+ builder.append(".json");
+ return builder.toString();
+ }
+
+ protected Uri buildUri() {
+ ObaContext context = (mObaContext != null) ? mObaContext : ObaApi.getDefaultContext();
+ context.setBaseUrl(mContext, mBuilder);
+ context.setAppInfo(mBuilder);
+ mBuilder.appendQueryParameter("version", "2");
+ mBuilder.appendQueryParameter("key", context.getApiKey());
+ return mBuilder.build();
+ }
+
+ public ObaContext getObaContext() {
+ if (mObaContext == null) {
+ mObaContext = ObaApi.getDefaultContext().clone();
+ }
+ return mObaContext;
+ }
+ }
+
+ /**
+ * Subclass for BuilderBase that can handle post data as well.
+ *
+ * @author paulw
+ */
+ public static class PostBuilderBase extends BuilderBase {
+
+ protected final Uri.Builder mPostData;
+
+ protected PostBuilderBase(Context context, String path) {
+ super(context, path);
+ mPostData = new Uri.Builder();
+ }
+
+ public String buildPostData() {
+ return mPostData.build().getEncodedQuery();
+ }
+ }
+
+ protected T call(Class cls) {
+ ObaApi.SerializationHandler handler = ObaApi.getSerializer(cls);
+ ObaConnection conn = null;
+ try {
+ conn = ObaApi.getDefaultContext().getConnectionFactory().newConnection(mUri);
+ Reader reader;
+ if (mPostData != null) {
+ reader = conn.post(mPostData);
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ // Theoretically you can't call ResponseCode before calling
+ // getInputStream, but you can't read from the input stream
+ // before you read the response???
+ int responseCode = conn.getResponseCode();
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ return handler.createFromError(cls, responseCode, "");
+ }
+ }
+
+ reader = conn.get();
+ }
+ T t = handler.deserialize(reader, cls);
+ if (t == null) {
+ t = handler.createFromError(cls, ObaApi.OBA_INTERNAL_ERROR, "Json error");
+ }
+ return t;
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, e.toString());
+ return handler.createFromError(cls, ObaApi.OBA_NOT_FOUND, e.toString());
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ return handler.createFromError(cls, ObaApi.OBA_IO_EXCEPTION, e.toString());
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ protected T callPostHack(Class cls) {
+ ObaApi.SerializationHandler handler = ObaApi.getSerializer(cls);
+ ObaConnection conn = null;
+ try {
+ conn = ObaApi.getDefaultContext().getConnectionFactory().newConnection(mUri);
+ BufferedReader reader = new BufferedReader(conn.post(mPostData), 8 * 1024);
+
+ String line;
+ StringBuffer text = new StringBuffer();
+ while ((line = reader.readLine()) != null) {
+ text.append(line + "\n");
+ }
+
+ String response = text.toString();
+ if (TextUtils.isEmpty(response)) {
+ return handler.createFromError(cls, ObaApi.OBA_OK, "OK");
+ } else {
+ return handler.createFromError(cls, ObaApi.OBA_INTERNAL_ERROR, response);
+ }
+
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, e.toString());
+ return handler.createFromError(cls, ObaApi.OBA_NOT_FOUND, e.toString());
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ return handler.createFromError(cls, ObaApi.OBA_IO_EXCEPTION, e.toString());
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegion.java b/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegion.java
new file mode 100644
index 0000000..adbc2d7
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegion.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2013 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region.elements;
+
+
+/**
+ * Specifies a region in the OneBusAway multi-region system.
+ */
+public interface ObaRegion {
+
+ /**
+ * Specifies a single bound rectangle within this region.
+ */
+ public interface Bounds {
+
+ public double getLowerLeftLatitude();
+
+ public double getLowerLeftLongitude();
+
+ public double getUpperRightLatitude();
+
+ public double getUpperRightLongitude();
+ }
+
+ public interface Open311Servers {
+
+ public String getJuridisctionId();
+
+ public String getApiKey();
+
+ public String getBaseUrl();
+ }
+
+ /**
+ * @return The ID of this region.
+ */
+ public long getId();
+
+ /**
+ * @return The name of the region.
+ */
+ public String getName();
+
+ /**
+ * @return true if this server is active and should be presented in a list of working servers,
+ * false otherwise.
+ */
+ public boolean getActive();
+
+ /**
+ * @return The base OBA URL for this region, or null if it doesn't have a base OBA URL.
+ */
+ public String getBaseUrl();
+
+
+ /**
+ * @return An array of bounding boxes for the region.
+ */
+ public Bounds[] getBounds();
+
+ /**
+ * @return The email of the party responsible for this region's OBA server.
+ */
+ public String getContactEmail();
+
+ public Open311Servers[] getOpen311Servers();
+
+ /**
+ * @return The Twitter URL for the region
+ */
+ public String getTwitterUrl();
+
+ public String getFacebookUrl();
+ /**
+ * @return true if this server is experimental, false if its production.
+ */
+ public boolean getExperimental();
+
+ public String getTutorialUrl();
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegionElement.java b/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegionElement.java
new file mode 100644
index 0000000..7b30464
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/elements/ObaRegionElement.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2012-2013 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region.elements;
+
+
+import java.util.Arrays;
+
+public class ObaRegionElement implements ObaRegion {
+
+ public static final ObaRegionElement[] EMPTY_ARRAY = new ObaRegionElement[]{};
+
+ public static class Bounds implements ObaRegion.Bounds {
+
+ public static final Bounds[] EMPTY_ARRAY = new Bounds[]{};
+
+ private final double lowerLeftLatitude;
+
+ private final double upperRightLatitude;
+
+ private final double lowerLeftLongitude;
+
+ private final double upperRightLongitude;
+
+ Bounds() {
+ lowerLeftLatitude = 0;
+ upperRightLatitude = 0;
+ lowerLeftLongitude = 0;
+ upperRightLongitude = 0;
+ }
+
+ public Bounds(double lowerLeftLatitude,
+ double upperRightLatitude,
+ double lowerLeftLongitude,
+ double upperRightLongitude) {
+ this.lowerLeftLatitude = lowerLeftLatitude;
+ this.upperRightLatitude = upperRightLatitude;
+ this.lowerLeftLongitude = lowerLeftLongitude;
+ this.upperRightLongitude = upperRightLongitude;
+ }
+
+
+ @Override
+ public double getLowerLeftLatitude() {
+ return lowerLeftLatitude;
+ }
+
+ @Override
+ public double getLowerLeftLongitude() {
+ return lowerLeftLongitude;
+ }
+
+ @Override
+ public double getUpperRightLatitude() {
+ return upperRightLatitude;
+ }
+
+ @Override
+ public double getUpperRightLongitude() {
+ return upperRightLongitude;
+ }
+ }
+
+ public static class Open311Servers implements ObaRegion.Open311Servers {
+
+ public static final Open311Servers[] EMPTY_ARRAY = new Open311Servers[]{};
+
+ private final String jurisdictionId;
+
+ private final String apiKey;
+
+ private final String baseUrl;
+
+ Open311Servers() {
+ jurisdictionId = "";
+ apiKey = "";
+ baseUrl = "";
+ }
+
+ public Open311Servers(String jurisdictionId, String apiKey, String baseUrl) {
+
+ this.jurisdictionId = jurisdictionId;
+ this.apiKey = apiKey;
+ this.baseUrl = baseUrl;
+ }
+
+ @Override
+ public String getJuridisctionId() {
+ return jurisdictionId;
+ }
+
+ @Override
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+ }
+
+ private final long id;
+
+ private final String regionName;
+
+ private final boolean active;
+
+ private final String baseUrl;
+
+ private final Bounds[] bounds;
+
+ private final Open311Servers[] open311Servers;
+
+ private final String contactEmail;
+
+ private final String twitterUrl;
+
+ private final String facebookUrl;
+
+ private final boolean experimental;
+
+ private final String tutorialUrl;
+
+ ObaRegionElement() {
+ id = 0;
+ regionName = "";
+ baseUrl = null;
+ active = false;
+ bounds = Bounds.EMPTY_ARRAY;
+ open311Servers = Open311Servers.EMPTY_ARRAY;
+ contactEmail = "";
+ twitterUrl = "";
+ facebookUrl = "";
+ experimental = true;
+ tutorialUrl = "";
+ }
+
+ public ObaRegionElement(long id,
+ String name,
+ boolean active,
+ String baseUrl,
+ Bounds[] bounds,
+ Open311Servers[] open311Servers,
+ String contactEmail,
+ String twitterUrl,
+ String facebookUrl,
+ boolean experimental,
+ String tutorialUrl) {
+ this.id = id;
+ this.regionName = name;
+ this.active = active;
+ this.baseUrl = baseUrl;
+ this.bounds = bounds;
+ this.open311Servers = open311Servers;
+ this.contactEmail = contactEmail;
+ this.twitterUrl = twitterUrl;
+ this.facebookUrl = facebookUrl;
+ this.experimental = experimental;
+ this.tutorialUrl = tutorialUrl;
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public String getName() {
+ return regionName;
+ }
+
+ @Override
+ public boolean getActive() {
+ return active;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ @Override
+ public Bounds[] getBounds() {
+ return bounds;
+ }
+
+ @Override
+ public String getContactEmail() {
+ return contactEmail;
+ }
+
+ @Override
+ public Open311Servers[] getOpen311Servers() {
+ return open311Servers;
+ }
+
+ @Override
+ public String getTwitterUrl() {
+ return twitterUrl;
+ }
+
+ @Override
+ public String getFacebookUrl() {
+ return facebookUrl;
+ }
+
+ @Override
+ public boolean getExperimental() {
+ return experimental;
+ }
+
+ @Override
+ public String getTutorialUrl() {
+ return tutorialUrl;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == 0) ? 0 : Long.valueOf(id).hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof ObaRegionElement)) {
+ return false;
+ }
+ ObaRegionElement other = (ObaRegionElement) obj;
+ if (id == 0) {
+ if (other.getId() != 0) {
+ return false;
+ }
+ } else if (id != other.getId()) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "ObaRegionElement{" +
+ "id=" + id +
+ ", regionName='" + regionName + '\'' +
+ ", active=" + active +
+ ", BaseUrl='" + baseUrl + '\'' +
+ ", bounds=" + Arrays.toString(bounds) +
+ ", contactEmail='" + contactEmail + '\'' +
+ ", twitterUrl='" + twitterUrl + '\'' +
+ ", experimental=" + experimental +
+ '}';
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationHelper.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationHelper.java
new file mode 100644
index 0000000..fc23042
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationHelper.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2014 Sean J. Barbeau (sjbarbeau@gmail.com), 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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+
+import static com.google.android.gms.location.LocationServices.FusedLocationApi;
+
+/**
+ * A helper class that keeps listeners updated with the best location available from
+ * multiple providers
+ */
+public class LocationHelper implements com.google.android.gms.location.LocationListener,
+ android.location.LocationListener, GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+
+ public interface Listener {
+
+ /**
+ * Called every time there is an update to the best location available
+ */
+ void onLocationChanged(Location location);
+ }
+
+ static final String TAG = "LocationHelper";
+
+ Context mContext;
+
+ LocationManager mLocationManager;
+
+ ArrayList mListeners = new ArrayList();
+
+ /**
+ * GoogleApiClient being used for Location Services
+ */
+ protected GoogleApiClient mGoogleApiClient;
+
+ LocationRequest mLocationRequest;
+
+ private static final int MILLISECONDS_PER_SECOND = 1000;
+
+ public static final int UPDATE_INTERVAL_IN_SECONDS = 5;
+
+ private static final long UPDATE_INTERVAL =
+ MILLISECONDS_PER_SECOND * UPDATE_INTERVAL_IN_SECONDS;
+
+ private static final int FASTEST_INTERVAL_IN_SECONDS = 1;
+
+ private static final long FASTEST_INTERVAL =
+ MILLISECONDS_PER_SECOND * FASTEST_INTERVAL_IN_SECONDS;
+
+ public LocationHelper(Context context) {
+ mContext = context;
+ mLocationManager = (LocationManager) Application.get().getBaseContext()
+ .getSystemService(Context.LOCATION_SERVICE);
+ setupGooglePlayServices();
+ }
+
+ public synchronized void registerListener(Listener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+
+ // If this is the first listener, make sure we're monitoring the sensors to provide updates
+ if (mListeners.size() == 1) {
+ // Listen for location
+ registerAllProviders();
+ }
+ }
+
+ public synchronized void unregisterListener(Listener listener) {
+ if (mListeners.contains(listener)) {
+ mListeners.remove(listener);
+ }
+
+ if (mListeners.size() == 0) {
+ mLocationManager.removeUpdates(this);
+ }
+ }
+
+ /**
+ * Returns the GoogleApiClient being used for fused provider location updates
+ *
+ * @return the GoogleApiClient being used for fused provider location updates
+ */
+ public GoogleApiClient getGoogleApiClient() {
+ return mGoogleApiClient;
+ }
+
+ public synchronized void onResume() {
+ registerAllProviders();
+ }
+
+ public synchronized void onPause() {
+ mLocationManager.removeUpdates(this);
+
+ // Tear down GoogleApiClient
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ // Offer this location to the centralized location store, it case its better than currently
+ // stored location
+ Application.setLastKnownLocation(location);
+ // Notify listeners with the newest location from the central store (which could be the one
+ // that was just generated above)
+ Location lastLocation = Application.getLastKnownLocation(mContext, mGoogleApiClient);
+ if (lastLocation != null) {
+ // We need to copy the location, it case this object is reset in Application
+ Location locationForListeners = new Location("for listeners");
+ locationForListeners.set(lastLocation);
+ for (Listener l : mListeners) {
+ l.onLocationChanged(locationForListeners);
+ }
+ }
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+
+ }
+
+ private void registerAllProviders() {
+ List providers = mLocationManager.getProviders(true);
+ for (Iterator i = providers.iterator(); i.hasNext(); ) {
+ mLocationManager.requestLocationUpdates(i.next(), 0, 0, this);
+ }
+
+ // Make sure GoogleApiClient is connected, if available
+ if (mGoogleApiClient != null && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ }
+
+ private void setupGooglePlayServices() {
+ // Create the LocationRequest object
+ mLocationRequest = LocationRequest.create();
+ // Use high accuracy
+ mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+ // Set the update interval to 5 seconds
+ mLocationRequest.setInterval(UPDATE_INTERVAL);
+ // Set the fastest update interval to 1 second
+ mLocationRequest.setFastestInterval(FASTEST_INTERVAL);
+
+ // Init Google Play Services as early as possible in the Fragment lifecycle to give it time
+ if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(mContext)
+ == ConnectionResult.SUCCESS) {
+ mGoogleApiClient = new GoogleApiClient.Builder(mContext)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ mGoogleApiClient.connect();
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ Log.d(TAG, "Location Services connected");
+ // Request location updates
+ FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationUtils.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationUtils.java
new file mode 100644
index 0000000..e258a06
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/LocationUtils.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2014 University of South Florida (sjbarbeau@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.location.LocationServices;
+
+import android.content.Context;
+import android.location.Location;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.concurrent.TimeUnit;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+
+/**
+ * Utilities to help obtain and process location data
+ *
+ * @author barbeau
+ */
+public class LocationUtils {
+
+ public static final String TAG = "LocationUtil";
+
+ public static final int DEFAULT_SEARCH_RADIUS = 40000;
+
+ private static final float FUZZY_EQUALS_THRESHOLD = 15.0f;
+
+ public static final float ACC_THRESHOLD = 50f; // 50 meters
+
+ public static final long TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(10); // 10 minutes
+
+ /**
+ * Compares Location A to Location B - prefers a non-null location that is more recent. Does
+ * NOT take estimated accuracy into account.
+ * @param a first location to compare
+ * @param b second location to compare
+ * @return true if Location a is "better" than b, or false if b is "better" than a
+ */
+ public static boolean compareLocationsByTime(Location a, Location b) {
+ return (a != null && (b == null || a.getTime() > b.getTime()));
+ }
+
+ /**
+ * Compares Location A to Location B, considering timestamps and accuracy of locations.
+ * Typically
+ * this is used to compare a new location delivered by a LocationListener (Location A) to
+ * a previously saved location (Location B).
+ *
+ * @param a location to compare
+ * @param b location to compare against
+ * @return true if Location a is "better" than b, or false if b is "better" than a
+ */
+ public static boolean compareLocations(Location a, Location b) {
+ if (a == null) {
+ // New location isn't valid, return false
+ return false;
+ }
+ // If the new location is the first location, save it
+ if (b == null) {
+ return true;
+ }
+
+ // If the last location is older than TIME_THRESHOLD minutes, and the new location is more recent,
+ // save the new location, even if the accuracy for new location is worse
+ if (System.currentTimeMillis() - b.getTime() > TIME_THRESHOLD
+ && compareLocationsByTime(a, b)) {
+ return true;
+ }
+
+ // If the new location has an accuracy better than ACC_THRESHOLD and is newer than the last location, save it
+ if (a.getAccuracy() < ACC_THRESHOLD && compareLocationsByTime(a, b)) {
+ return true;
+ }
+
+ // If we get this far, A isn't better than B
+ return false;
+ }
+
+ /**
+ * Converts a latitude/longitude to a Location.
+ *
+ * @param lat The latitude.
+ * @param lon The longitude.
+ * @return A Location representing this latitude/longitude.
+ */
+ public static final Location makeLocation(double lat, double lon) {
+ Location l = new Location("");
+ l.setLatitude(lat);
+ l.setLongitude(lon);
+ return l;
+ }
+
+ /**
+ * Returns true if the locations are approximately equal (i.e., within a certain distance
+ * threshold)
+ *
+ * @param a first location
+ * @param b second location
+ * @return true if the locations are approximately equal, false if they are not
+ */
+ public static boolean fuzzyEquals(Location a, Location b) {
+ return a.distanceTo(b) <= FUZZY_EQUALS_THRESHOLD;
+ }
+
+ /**
+ * Returns true if the user has enabled location services on their device, false if they have
+ * not
+ *
+ * from http://stackoverflow.com/a/22980843/937715
+ *
+ * @return true if the user has enabled location services on their device, false if they have
+ * not
+ */
+ public static boolean isLocationEnabled(Context context) {
+ int locationMode = Settings.Secure.LOCATION_MODE_OFF;
+ String locationProviders;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ try {
+ locationMode = Settings.Secure
+ .getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE);
+ } catch (Settings.SettingNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return locationMode != Settings.Secure.LOCATION_MODE_OFF;
+ } else {
+ locationProviders = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
+ return !TextUtils.isEmpty(locationProviders);
+ }
+ }
+
+ /**
+ * Returns the human-readable details of a Location (provider, lat/long, accuracy, timestamp)
+ *
+ * @return the details of a Location (provider, lat/long, accuracy, timestamp) in a string
+ */
+ public static String printLocationDetails(Location loc) {
+ if (loc == null) {
+ return "";
+ }
+
+ long timeDiff;
+ double timeDiffSec;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ timeDiff = SystemClock.elapsedRealtimeNanos() - loc.getElapsedRealtimeNanos();
+ // Convert to seconds
+ timeDiffSec = timeDiff / 1E9;
+ } else {
+ timeDiff = System.currentTimeMillis() - loc.getTime();
+ timeDiffSec = timeDiff / 1E3;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(loc.getProvider());
+ sb.append(' ');
+ sb.append(loc.getLatitude());
+ sb.append(',');
+ sb.append(loc.getLongitude());
+ if (loc.hasAccuracy()) {
+ sb.append(' ');
+ sb.append(loc.getAccuracy());
+ }
+ sb.append(", ");
+ sb.append(String.format("%.0f", timeDiffSec) + " second(s) ago");
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns a new GoogleApiClient which includes LocationServicesCallbacks
+ */
+ public static GoogleApiClient getGoogleApiClientWithCallbacks(Context context) {
+ LocationServicesCallback locCallback = new LocationServicesCallback();
+ return new GoogleApiClient.Builder(context)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(locCallback)
+ .addOnConnectionFailedListener(locCallback)
+ .build();
+ }
+
+
+ /**
+ * Class to handle Google Play Location Services callbacks
+ */
+ public static class LocationServicesCallback
+ implements GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+
+ private static final String TAG = "LocationServicesCallback";
+
+ @Override
+ public void onConnected(Bundle bundle) {
+
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+
+ }
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/MapHelpV2.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/MapHelpV2.java
new file mode 100644
index 0000000..96dea55
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/MapHelpV2.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2014 University of South Florida (sjbarbeau@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+
+/**
+ * Utilities to help process data for Android Maps API v1
+ */
+public class MapHelpV2 {
+
+ public static final String TAG = "MapHelpV2";
+
+ /**
+ * Converts a latitude/longitude to a LatLng.
+ *
+ * @param lat The latitude.
+ * @param lon The longitude.
+ * @return A LatLng representing this latitude/longitude.
+ */
+ public static final LatLng makeLatLng(double lat, double lon) {
+ return new LatLng(lat, lon);
+ }
+
+
+ /**
+ * Returns the bounds for the entire region.
+ *
+ * @return LatLngBounds for the region
+ */
+ public static LatLngBounds getRegionBounds(ObaRegion region) {
+ if (region == null) {
+ throw new IllegalArgumentException("Region is null");
+ }
+ double latMin = 90;
+ double latMax = -90;
+ double lonMin = 180;
+ double lonMax = -180;
+
+ // This is fairly simplistic
+ for (ObaRegion.Bounds bound : region.getBounds()) {
+ // Get the top bound
+ double lat1 = bound.getLowerLeftLatitude();
+ double lat2 = bound.getUpperRightLatitude();
+ if (lat1 < latMin) {
+ latMin = lat1;
+ }
+ if (lat2 > latMax) {
+ latMax = lat2;
+ }
+
+ double lon1 = bound.getLowerLeftLongitude();
+ double lon2 = bound.getUpperRightLongitude();
+ if (lon1 < lonMin) {
+ lonMin = lon1;
+ }
+ if (lon2 > lonMax) {
+ lonMax = lon2;
+ }
+ }
+
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+ builder.include(MapHelpV2.makeLatLng(latMin, lonMin));
+ builder.include(MapHelpV2.makeLatLng(latMax, lonMax));
+
+ return builder.build();
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/PreferenceUtils.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/PreferenceUtils.java
new file mode 100644
index 0000000..d36aa86
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/PreferenceUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 Paul Watts (paulcwatts@gmail.com)
+ *
+ * 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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import android.annotation.TargetApi;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+
+/**
+ * A class containing utility methods related to preferences
+ */
+public class PreferenceUtils {
+
+ @TargetApi(9)
+ public static void saveString(SharedPreferences prefs, String key, String value) {
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putString(key, value);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ edit.apply();
+ } else {
+ edit.commit();
+ }
+ }
+
+ public static void saveString(String key, String value) {
+ saveString(Application.getPrefs(), key, value);
+ }
+
+ @TargetApi(9)
+ public static void saveInt(SharedPreferences prefs, String key, int value) {
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putInt(key, value);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ edit.apply();
+ } else {
+ edit.commit();
+ }
+ }
+
+ public static void saveInt(String key, int value) {
+ saveInt(Application.getPrefs(), key, value);
+ }
+
+ @TargetApi(9)
+ public static void saveLong(SharedPreferences prefs, String key, long value) {
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putLong(key, value);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ edit.apply();
+ } else {
+ edit.commit();
+ }
+ }
+
+ public static void saveLong(String key, long value) {
+ saveLong(Application.getPrefs(), key, value);
+ }
+
+ @TargetApi(9)
+ public static void saveBoolean(SharedPreferences prefs, String key, boolean value) {
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putBoolean(key, value);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ edit.apply();
+ } else {
+ edit.commit();
+ }
+ }
+
+ public static void saveBoolean(String key, boolean value) {
+ saveBoolean(Application.getPrefs(), key, value);
+ }
+
+ public static String getString(String key) {
+ return Application.getPrefs().getString(key, null);
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/RegionUtils.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/RegionUtils.java
new file mode 100644
index 0000000..d9bfebc
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/RegionUtils.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2012-2013 Paul Watts (paulcwatts@gmail.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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import com.google.android.gms.maps.model.LatLng;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.location.Location;
+import android.net.Uri;
+import android.util.Log;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+
+import edu.gatech.ppl.cycleatlanta.Application;
+import edu.gatech.ppl.cycleatlanta.BuildConfig;
+import edu.gatech.ppl.cycleatlanta.R;
+import edu.gatech.ppl.cycleatlanta.provider.ObaContract;
+import edu.gatech.ppl.cycleatlanta.region.ObaRegionsRequest;
+import edu.gatech.ppl.cycleatlanta.region.ObaRegionsResponse;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegion;
+import edu.gatech.ppl.cycleatlanta.region.elements.ObaRegionElement;
+
+/**
+ * A class containing utility methods related to handling multiple regions in OneBusAway
+ */
+public class RegionUtils {
+
+ private static final String TAG = "RegionUtils";
+
+ public static final double METERS_TO_MILES = 0.000621371;
+
+ private static final int DISTANCE_LIMITER = 100; // miles
+
+ /**
+ * Get the closest region from a list of regions and a given location
+ *
+ * This method also enforces the constraints in isRegionUsable() to
+ * ensure the returned region is actually usable by the app
+ *
+ * @param regions list of regions
+ * @param loc location
+ * @param enforceThreshold true if the DISTANCE_LIMITER threshold should be enforced, false if
+ * it should not
+ * @return the closest region to the given location from the list of regions, or null if a
+ * enforceThreshold is true and the closest region exceeded DISTANCE_LIMITER threshold or a
+ * region couldn't be found
+ */
+ public static ObaRegion getClosestRegion(ArrayList regions, Location loc,
+ boolean enforceThreshold) {
+ if (loc == null) {
+ return null;
+ }
+ float minDist = Float.MAX_VALUE;
+ ObaRegion closestRegion = null;
+ Float distToRegion;
+
+ NumberFormat fmt = NumberFormat.getInstance();
+ if (fmt instanceof DecimalFormat) {
+ ((DecimalFormat) fmt).setMaximumFractionDigits(1);
+ }
+ double miles;
+
+ Log.d(TAG, "Finding region closest to " + loc.getLatitude() + "," + loc.getLongitude());
+
+ for (ObaRegion region : regions) {
+ if (!isRegionUsable(region)) {
+ Log.d(TAG,
+ "Excluding '" + region.getName() + "' from 'closest region' consideration");
+ continue;
+ }
+
+ distToRegion = getDistanceAway(region, loc.getLatitude(), loc.getLongitude());
+ if (distToRegion == null) {
+ Log.e(TAG, "Couldn't measure distance to region '" + region.getName() + "'");
+ continue;
+ }
+ miles = distToRegion * METERS_TO_MILES;
+ Log.d(TAG, "Region '" + region.getName() + "' is " + fmt.format(miles) + " miles away");
+ if (distToRegion < minDist) {
+ closestRegion = region;
+ minDist = distToRegion;
+ }
+ }
+
+ if (enforceThreshold) {
+ if (minDist * METERS_TO_MILES < DISTANCE_LIMITER) {
+ return closestRegion;
+ } else {
+ return null;
+ }
+ }
+ return closestRegion;
+ }
+
+ /**
+ * Returns the distance from the specified location
+ * to the center of the closest bound in this region.
+ *
+ * @return distance from the specified location to the center of the closest bound in this
+ * region, in meters
+ */
+ public static Float getDistanceAway(ObaRegion region, double lat, double lon) {
+ ObaRegion.Bounds[] bounds = region.getBounds();
+ if (bounds == null) {
+ return null;
+ }
+ float[] results = new float[1];
+ float minDistance = Float.MAX_VALUE;
+ for (ObaRegion.Bounds bound : bounds) {
+
+ LatLng midpoint = midPoint(bound.getLowerLeftLatitude(), bound.getLowerLeftLongitude(),
+ bound.getUpperRightLatitude(), bound.getUpperRightLongitude());
+ Location.distanceBetween(lat, lon, midpoint.latitude, midpoint.longitude, results);
+ if (results[0] < minDistance) {
+ minDistance = results[0];
+ }
+ }
+ return minDistance;
+ }
+
+ public static Float getDistanceAway(ObaRegion region, Location loc) {
+ return getDistanceAway(region, loc.getLatitude(), loc.getLongitude());
+ }
+
+ public static LatLng midPoint(double lat1,double lon1,double lat2,double lon2){
+
+ double dLon = Math.toRadians(lon2 - lon1);
+
+ //convert to radians
+ lat1 = Math.toRadians(lat1);
+ lat2 = Math.toRadians(lat2);
+ lon1 = Math.toRadians(lon1);
+
+ double Bx = Math.cos(lat2) * Math.cos(dLon);
+ double By = Math.cos(lat2) * Math.sin(dLon);
+ double lat3 = Math.atan2(Math.sin(lat1) + Math.sin(lat2), Math.sqrt((Math.cos(lat1) + Bx) *
+ (Math.cos(lat1) + Bx) + By * By));
+ double lon3 = lon1 + Math.atan2(By, Math.cos(lat1) + Bx);
+
+ return new LatLng(Math.toDegrees(lat3), Math.toDegrees(lon3));
+ }
+
+
+ /**
+ * Checks if the given region is usable by the app, based on what this app supports
+ * - Is the region active?
+ * - Does the region support the OBA Discovery APIs?
+ * - Does the region support the OBA Realtime APIs?
+ * - Is the region experimental, and if so, did the user opt-in via preferences?
+ *
+ * @param region region to be checked
+ * @return true if the region is usable by this application, false if it is not
+ */
+ public static boolean isRegionUsable(ObaRegion region) {
+ if (!region.getActive()) {
+ Log.d(TAG, "Region '" + region.getName() + "' is not active.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gets regions from either the server, local provider, or if both fails the regions file
+ * packaged
+ * with the APK. Includes fail-over logic to prefer sources in above order, with server being
+ * the first preference.
+ *
+ * @param forceReload true if a reload from the server should be forced, false if it should not
+ * @return a list of regions from either the server, the local provider, or the packaged
+ * resource file
+ */
+ public synchronized static ArrayList getRegions(Context context,
+ boolean forceReload) {
+ ArrayList results;
+ if (!forceReload) {
+ //
+ // Check the DB
+ //
+ results = RegionUtils.getRegionsFromProvider(context);
+ if (results != null) {
+ Log.d(TAG, "Retrieved regions from database.");
+ return results;
+ }
+ Log.d(TAG, "Regions list retrieved from database was null.");
+ }
+
+ results = RegionUtils.getRegionsFromServer(context);
+ if (results == null || results.isEmpty()) {
+ Log.d(TAG, "Regions list retrieved from server was null or empty.");
+
+ if (forceReload) {
+ //If we tried to force a reload from the server, then we haven't tried to reload from local provider yet
+ results = RegionUtils.getRegionsFromProvider(context);
+ if (results != null) {
+ Log.d(TAG, "Retrieved regions from database.");
+ return results;
+ } else {
+ Log.d(TAG, "Regions list retrieved from database was null.");
+ }
+ }
+
+ //If we reach this point, the call to the Regions REST API failed and no results were
+ //available locally from a prior server request.
+ //Fetch regions from local resource file as last resort (otherwise user can't use app)
+ results = RegionUtils.getRegionsFromResources(context);
+
+ if (results == null) {
+ //This is a complete failure to load region info from all sources, app will be useless
+ Log.d(TAG, "Regions list retrieved from local resource file was null.");
+ return results;
+ }
+
+ Log.d(TAG, "Retrieved regions from local resource file.");
+ } else {
+ Log.d(TAG, "Retrieved regions list from server.");
+ //Update local time for when the last region info was retrieved from the server
+ Application.get().setLastRegionUpdateDate(new Date().getTime());
+ }
+
+ //If the region info came from the server or local resource file, we need to save it to the local provider
+ RegionUtils.saveToProvider(context, results);
+ return results;
+ }
+
+ public static ArrayList getRegionsFromProvider(Context context) {
+ // Prefetch the bounds to limit the number of DB calls.
+ HashMap> allBounds = getBoundsFromProvider(
+ context);
+
+ HashMap> allOpen311Servers =
+ getOpen311ServersFromProvider(context);
+
+ Cursor c = null;
+ try {
+ final String[] PROJECTION = {
+ ObaContract.Regions._ID,
+ ObaContract.Regions.NAME,
+ ObaContract.Regions.BASE_URL,
+ ObaContract.Regions.CONTACT_EMAIL,
+ ObaContract.Regions.TWITTER_URL,
+ ObaContract.Regions.FACEBOOK_URL,
+ ObaContract.Regions.EXPERIMENTAL,
+ ObaContract.Regions.TUTORIAL_URL
+ };
+
+ ContentResolver cr = context.getContentResolver();
+ c = cr.query(
+ ObaContract.Regions.CONTENT_URI, PROJECTION, null, null,
+ ObaContract.Regions._ID);
+ if (c == null) {
+ return null;
+ }
+ if (c.getCount() == 0) {
+ c.close();
+ return null;
+ }
+ ArrayList results = new ArrayList();
+
+ c.moveToFirst();
+ do {
+ long id = c.getLong(0);
+ ArrayList bounds = allBounds.get(id);
+ ObaRegionElement.Bounds[] bounds2 = (bounds != null) ?
+ bounds.toArray(new ObaRegionElement.Bounds[]{}) :
+ null;
+
+ ArrayList open311Servers = allOpen311Servers.get(id);
+ ObaRegionElement.Open311Servers[] open311Servers2 = (open311Servers != null) ?
+ open311Servers.toArray(new ObaRegionElement.Open311Servers[]{}) :
+ null;
+
+ results.add(new ObaRegionElement(id, // id
+ c.getString(1), // Name
+ true, // Active
+ c.getString(2), // OBA Base URL
+ bounds2, // Bounds
+ open311Servers2, // Open311 servers
+ c.getString(3), // Contact Email
+ c.getString(4), // Twitter URL
+ c.getString(5), // FB URL
+ c.getInt(6) > 0, // Experimental
+ c.getString(7)
+ ));
+
+ } while (c.moveToNext());
+
+ return results;
+
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private static HashMap> getBoundsFromProvider(
+ Context context) {
+ // Prefetch the bounds to limit the number of DB calls.
+ Cursor c = null;
+ try {
+ final String[] PROJECTION = {
+ ObaContract.RegionBounds.REGION_ID,
+ ObaContract.RegionBounds.LOWER_LEFT_LATITUDE,
+ ObaContract.RegionBounds.UPPER_RIGHT_LATITUDE,
+ ObaContract.RegionBounds.LOWER_LEFT_LONGITUDE,
+ ObaContract.RegionBounds.UPPER_RIGHT_LONGITUDE
+ };
+ HashMap> results
+ = new HashMap>();
+
+ ContentResolver cr = context.getContentResolver();
+ c = cr.query(ObaContract.RegionBounds.CONTENT_URI, PROJECTION, null, null, null);
+ if (c == null) {
+ return results;
+ }
+ if (c.getCount() == 0) {
+ c.close();
+ return results;
+ }
+ c.moveToFirst();
+ do {
+ long regionId = c.getLong(0);
+ ArrayList bounds = results.get(regionId);
+ ObaRegionElement.Bounds b = new ObaRegionElement.Bounds(
+ c.getDouble(1),
+ c.getDouble(2),
+ c.getDouble(3),
+ c.getDouble(4));
+ if (bounds != null) {
+ bounds.add(b);
+ } else {
+ bounds = new ArrayList();
+ bounds.add(b);
+ results.put(regionId, bounds);
+ }
+
+ } while (c.moveToNext());
+
+ return results;
+
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private static HashMap> getOpen311ServersFromProvider(
+ Context context) {
+ // Prefetch the bounds to limit the number of DB calls.
+ Cursor c = null;
+ try {
+ final String[] PROJECTION = {
+ ObaContract.RegionOpen311Servers.REGION_ID,
+ ObaContract.RegionOpen311Servers.JURISDICTION,
+ ObaContract.RegionOpen311Servers.API_KEY,
+ ObaContract.RegionOpen311Servers.BASE_URL
+ };
+ HashMap> results
+ = new HashMap>();
+
+ ContentResolver cr = context.getContentResolver();
+ c = cr.query(ObaContract.RegionOpen311Servers.CONTENT_URI, PROJECTION, null, null, null);
+ if (c == null) {
+ return results;
+ }
+ if (c.getCount() == 0) {
+ c.close();
+ return results;
+ }
+ c.moveToFirst();
+ do {
+ long regionId = c.getLong(0);
+ ArrayList open311Servers = results.get(regionId);
+ ObaRegionElement.Open311Servers b = new ObaRegionElement.Open311Servers(
+ c.getString(1),
+ c.getString(2),
+ c.getString(3));
+ if (open311Servers != null) {
+ open311Servers.add(b);
+ } else {
+ open311Servers = new ArrayList();
+ open311Servers.add(b);
+ results.put(regionId, open311Servers);
+ }
+
+ } while (c.moveToNext());
+
+ return results;
+
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private synchronized static ArrayList getRegionsFromServer(Context context) {
+ ObaRegionsResponse response = ObaRegionsRequest.newRequest(context).call();
+ return new ArrayList(Arrays.asList(response.getRegions()));
+ }
+
+ /**
+ * Retrieves region information from a regions file bundled within the app APK
+ *
+ * IMPORTANT - this should be a last resort, and we should always try to pull regions
+ * info from the local provider or Regions REST API instead of from the bundled file.
+ *
+ * This method is only intended to be a fail-safe in case the Regions REST API goes
+ * offline and a user downloads and installs OBA Android during that period
+ * (i.e., local OBA servers are available, but Regions REST API failure would block initial
+ * execution of the app). This avoids a potential central point of failure for OBA
+ * Android installations on devices in multiple regions.
+ *
+ * @return list of regions retrieved from the regions file in app resources
+ */
+ public static ArrayList getRegionsFromResources(Context context) {
+ final Uri.Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE);
+ builder.authority(context.getPackageName());
+ builder.path(Integer.toString(R.raw.regions_v3));
+ ObaRegionsResponse response = ObaRegionsRequest.newRequest(context, builder.build()).call();
+ return new ArrayList(Arrays.asList(response.getRegions()));
+ }
+
+ /**
+ * Retrieves hard-coded region information from the build flavor defined in build.gradle.
+ * If a fixed region is defined in a build flavor, it does not allow region roaming.
+ *
+ * @return hard-coded region information from the build flavor defined in build.gradle
+ */
+ public static ObaRegion getRegionFromBuildFlavor() {
+ final int regionId = Integer.MAX_VALUE; // This doesn't get used, but needs to be positive
+ ObaRegionElement.Bounds[] boundsArray = new ObaRegionElement.Bounds[1];
+ ObaRegionElement.Bounds bounds = new ObaRegionElement.Bounds(
+ BuildConfig.FIXED_REGION_BOUNDS_LAT, BuildConfig.FIXED_REGION_BOUNDS_LON,
+ BuildConfig.FIXED_REGION_BOUNDS_LAT_SPAN, BuildConfig.FIXED_REGION_BOUNDS_LON_SPAN);
+ boundsArray[0] = bounds;
+ ObaRegionElement region = new ObaRegionElement(regionId,
+ BuildConfig.FIXED_REGION_NAME, true,
+ BuildConfig.FIXED_REGION_OBA_BASE_URL,
+ boundsArray, new ObaRegionElement.Open311Servers[0],
+ BuildConfig.FIXED_REGION_CONTACT_EMAIL,
+ BuildConfig.FIXED_REGION_TWITTER_URL,BuildConfig.FIXED_REGION_TWITTER_URL, false,
+ null);
+ return region;
+ }
+
+ //
+ // Saving
+ //
+ public synchronized static void saveToProvider(Context context, List regions) {
+ // Delete all the existing regions
+ ContentResolver cr = context.getContentResolver();
+
+ cr.delete(ObaContract.Regions.CONTENT_URI, null, null);
+ // Should be a no-op?
+ cr.delete(ObaContract.RegionBounds.CONTENT_URI, null, null);
+
+ for (ObaRegion region : regions) {
+ if (!isRegionUsable(region)) {
+ Log.d(TAG, "Skipping insert of '" + region.getName() + "' to provider...");
+ continue;
+ }
+
+ cr.insert(ObaContract.Regions.CONTENT_URI, toContentValues(region));
+ Log.d(TAG, "Saved region '" + region.getName() + "' to provider");
+ long regionId = region.getId();
+ // Bulk insert the bounds
+ ObaRegion.Bounds[] bounds = region.getBounds();
+ if (bounds != null) {
+ ContentValues[] values = new ContentValues[bounds.length];
+ for (int i = 0; i < bounds.length; ++i) {
+ values[i] = toContentValues(regionId, bounds[i]);
+ }
+ cr.bulkInsert(ObaContract.RegionBounds.CONTENT_URI, values);
+ }
+
+ ObaRegion.Open311Servers[] open311Servers = region.getOpen311Servers();
+
+ if (open311Servers != null) {
+ ContentValues[] values = new ContentValues[open311Servers.length];
+ for (int i = 0; i < open311Servers.length; ++i) {
+ values[i] = toContentValues(regionId, open311Servers[i]);
+ }
+ cr.bulkInsert(ObaContract.RegionOpen311Servers.CONTENT_URI, values);
+ }
+ }
+ }
+
+ private static ContentValues toContentValues(ObaRegion region) {
+ ContentValues values = new ContentValues();
+ values.put(ObaContract.Regions._ID, region.getId());
+ values.put(ObaContract.Regions.NAME, region.getName());
+ String obaUrl = region.getBaseUrl();
+ values.put(ObaContract.Regions.BASE_URL, obaUrl != null ? obaUrl : "");
+ values.put(ObaContract.Regions.CONTACT_EMAIL, region.getContactEmail());
+ values.put(ObaContract.Regions.TWITTER_URL, region.getTwitterUrl());
+ values.put(ObaContract.Regions.FACEBOOK_URL, region.getFacebookUrl());
+ values.put(ObaContract.Regions.EXPERIMENTAL, region.getExperimental());
+ values.put(ObaContract.Regions.TUTORIAL_URL, region.getTutorialUrl());
+ return values;
+ }
+
+ private static ContentValues toContentValues(long region, ObaRegion.Bounds bounds) {
+ ContentValues values = new ContentValues();
+ values.put(ObaContract.RegionBounds.REGION_ID, region);
+ values.put(ObaContract.RegionBounds.LOWER_LEFT_LATITUDE, bounds.getLowerLeftLatitude());
+ values.put(ObaContract.RegionBounds.UPPER_RIGHT_LATITUDE, bounds.getUpperRightLatitude());
+ values.put(ObaContract.RegionBounds.LOWER_LEFT_LONGITUDE, bounds.getLowerLeftLongitude());
+ values.put(ObaContract.RegionBounds.UPPER_RIGHT_LONGITUDE, bounds.getUpperRightLongitude());
+ return values;
+ }
+
+ private static ContentValues toContentValues(long region, ObaRegion.Open311Servers open311Servers) {
+ ContentValues values = new ContentValues();
+ values.put(ObaContract.RegionOpen311Servers.REGION_ID, region);
+ values.put(ObaContract.RegionOpen311Servers.BASE_URL, open311Servers.getBaseUrl());
+ values.put(ObaContract.RegionOpen311Servers.JURISDICTION, open311Servers.getJuridisctionId());
+ values.put(ObaContract.RegionOpen311Servers.API_KEY, open311Servers.getApiKey());
+ return values;
+ }
+}
diff --git a/src/edu/gatech/ppl/cycleatlanta/region/utils/UIUtils.java b/src/edu/gatech/ppl/cycleatlanta/region/utils/UIUtils.java
new file mode 100644
index 0000000..7ed38b5
--- /dev/null
+++ b/src/edu/gatech/ppl/cycleatlanta/region/utils/UIUtils.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2010-2013 Paul Watts (paulcwatts@gmail.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 edu.gatech.ppl.cycleatlanta.region.utils;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.view.View;
+
+/**
+ * A class containing utility methods related to the user interface
+ */
+public final class UIUtils {
+
+ private static final String TAG = "UIHelp";
+
+ /**
+ * Returns true if the activity is still active and dialogs can be managed (i.e., displayed
+ * or dismissed), or false if it is
+ * not
+ *
+ * @param activity Activity to check for displaying/dismissing a dialog
+ * @return true if the activity is still active and dialogs can be managed, or false if it is
+ * not
+ */
+ public static boolean canManageDialog(Activity activity) {
+ if (activity == null) {
+ return false;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return !activity.isFinishing() && !activity.isDestroyed();
+ } else {
+ return !activity.isFinishing();
+ }
+ }
+
+ /**
+ * Returns true if the context is an Activity and is still active and dialogs can be managed
+ * (i.e., displayed or dismissed) OR the context is not an Activity, or false if the Activity
+ * is
+ * no longer active.
+ *
+ * NOTE: We really shouldn't display dialogs from a Service - a notification is a better way
+ * to communicate with the user.
+ *
+ * @param context Context to check for displaying/dismissing a dialog
+ * @return true if the context is an Activity and is still active and dialogs can be managed
+ * (i.e., displayed or dismissed) OR the context is not an Activity, or false if the Activity
+ * is
+ * no longer active
+ */
+ public static boolean canManageDialog(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ if (context instanceof Activity) {
+ return canManageDialog((Activity) context);
+ } else {
+ // We really shouldn't be displaying dialogs from a Service, but if for some reason we
+ // need to do this, we don't have any way of checking whether its possible
+ return true;
+ }
+ }
+
+ /**
+ * Returns true if the API level supports animating Views using ViewPropertyAnimator, false if
+ * it doesn't
+ *
+ * @return true if the API level supports animating Views using ViewPropertyAnimator, false if
+ * it doesn't
+ */
+ public static boolean canAnimateViewModern() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
+ }
+
+ /**
+ * Returns true if the API level supports canceling existing animations via the
+ * ViewPropertyAnimator, and false if it does not
+ *
+ * @return true if the API level supports canceling existing animations via the
+ * ViewPropertyAnimator, and false if it does not
+ */
+ public static boolean canCancelAnimation() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+ }
+
+ /**
+ * Returns true if the API level supports our Arrival Info Style B (sort by route) views, false
+ * if it does not. See #350 and #275.
+ *
+ * @return true if the API level supports our Arrival Info Style B (sort by route) views, false
+ * if it does not
+ */
+ public static boolean canSupportArrivalInfoStyleB() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+ }
+
+ /**
+ * Shows a view, using animation if the platform supports it
+ *
+ * @param v View to show
+ * @param animationDuration duration of animation
+ */
+ @TargetApi(14)
+ public static void showViewWithAnimation(final View v, int animationDuration) {
+ // If we're on a legacy device, show the view without the animation
+ if (!canAnimateViewModern()) {
+ showViewWithoutAnimation(v);
+ return;
+ }
+
+ if (v.getVisibility() == View.VISIBLE && v.getAlpha() == 1) {
+ // View is already visible and not transparent, return without doing anything
+ return;
+ }
+
+ v.clearAnimation();
+ if (canCancelAnimation()) {
+ v.animate().cancel();
+ }
+
+ if (v.getVisibility() != View.VISIBLE) {
+ // Set the content view to 0% opacity but visible, so that it is visible
+ // (but fully transparent) during the animation.
+ v.setAlpha(0f);
+ v.setVisibility(View.VISIBLE);
+ }
+
+ // Animate the content view to 100% opacity, and clear any animation listener set on the view.
+ v.animate()
+ .alpha(1f)
+ .setDuration(animationDuration)
+ .setListener(null);
+ }
+
+ /**
+ * Shows a view without using animation
+ *
+ * @param v View to show
+ */
+ public static void showViewWithoutAnimation(final View v) {
+ if (v.getVisibility() == View.VISIBLE) {
+ // View is already visible, return without doing anything
+ return;
+ }
+ v.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Hides a view, using animation if the platform supports it
+ *
+ * @param v View to hide
+ * @param animationDuration duration of animation
+ */
+ @TargetApi(14)
+ public static void hideViewWithAnimation(final View v, int animationDuration) {
+ // If we're on a legacy device, hide the view without the animation
+ if (!canAnimateViewModern()) {
+ hideViewWithoutAnimation(v);
+ return;
+ }
+
+ if (v.getVisibility() == View.GONE) {
+ // View is already gone, return without doing anything
+ return;
+ }
+
+ v.clearAnimation();
+ if (canCancelAnimation()) {
+ v.animate().cancel();
+ }
+
+ // Animate the view to 0% opacity. After the animation ends, set its visibility to GONE as
+ // an optimization step (it won't participate in layout passes, etc.)
+ v.animate()
+ .alpha(0f)
+ .setDuration(animationDuration)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ /**
+ * Hides a view without using animation
+ *
+ * @param v View to hide
+ */
+ public static void hideViewWithoutAnimation(final View v) {
+ if (v.getVisibility() == View.GONE) {
+ // View is already gone, return without doing anything
+ return;
+ }
+ // Hide the view without animation
+ v.setVisibility(View.GONE);
+ }
+}