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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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" /> + + + + + + 300 150 + 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); + } +}