From 434cc249ed7ebf9882ecdf67f6770b44c9056034 Mon Sep 17 00:00:00 2001 From: dkeen Date: Mon, 23 May 2011 11:59:34 -0700 Subject: [PATCH] Seems like it works --- .classpath | 7 + .hgignore | 3 + .project | 33 +++ AndroidManifest.xml | 50 ++++ default.properties | 11 + proguard.cfg | 36 +++ res/drawable-hdpi/icon.png | Bin 0 -> 4147 bytes res/drawable-ldpi/icon.png | Bin 0 -> 1723 bytes res/drawable-mdpi/icon.png | Bin 0 -> 2574 bytes res/drawable/basic_rectangle.xml | 5 + res/layout/add_favorite.xml | 33 +++ res/layout/arrival_listing.xml | 12 + res/layout/favorite_listing.xml | 13 ++ res/layout/main.xml | 14 ++ res/layout/simple_spinner_item.xml | 8 + res/menu/favorites_menu.xml | 5 + res/values/colors.xml | 4 + res/values/strings.xml | 18 ++ res/values/styles.xml | 34 +++ .../dougkeen/bart/AddFavoriteActivity.java | 92 ++++++++ src/com/dougkeen/bart/Constants.java | 11 + src/com/dougkeen/bart/EtdContentHandler.java | 95 ++++++++ .../bart/FavoritesDashboardActivity.java | 83 +++++++ .../bart/GetRealTimeArrivalsTask.java | 109 +++++++++ src/com/dougkeen/bart/Line.java | 108 +++++++++ src/com/dougkeen/bart/Route.java | 66 ++++++ src/com/dougkeen/bart/Station.java | 165 +++++++++++++ .../dougkeen/bart/ViewArrivalsActivity.java | 195 ++++++++++++++++ src/com/dougkeen/bart/data/Arrival.java | 219 ++++++++++++++++++ .../bart/data/BartContentProvider.java | 188 +++++++++++++++ src/com/dougkeen/bart/data/CursorUtils.java | 15 ++ .../dougkeen/bart/data/DatabaseHelper.java | 29 +++ .../dougkeen/bart/data/FavoritesColumns.java | 20 ++ .../dougkeen/bart/data/RealTimeArrivals.java | 100 ++++++++ 34 files changed, 1781 insertions(+) create mode 100644 .classpath create mode 100644 .hgignore create mode 100644 .project create mode 100644 AndroidManifest.xml create mode 100644 default.properties create mode 100644 proguard.cfg create mode 100644 res/drawable-hdpi/icon.png create mode 100644 res/drawable-ldpi/icon.png create mode 100644 res/drawable-mdpi/icon.png create mode 100644 res/drawable/basic_rectangle.xml create mode 100644 res/layout/add_favorite.xml create mode 100644 res/layout/arrival_listing.xml create mode 100644 res/layout/favorite_listing.xml create mode 100644 res/layout/main.xml create mode 100644 res/layout/simple_spinner_item.xml create mode 100644 res/menu/favorites_menu.xml create mode 100644 res/values/colors.xml create mode 100644 res/values/strings.xml create mode 100644 res/values/styles.xml create mode 100644 src/com/dougkeen/bart/AddFavoriteActivity.java create mode 100644 src/com/dougkeen/bart/Constants.java create mode 100644 src/com/dougkeen/bart/EtdContentHandler.java create mode 100644 src/com/dougkeen/bart/FavoritesDashboardActivity.java create mode 100644 src/com/dougkeen/bart/GetRealTimeArrivalsTask.java create mode 100644 src/com/dougkeen/bart/Line.java create mode 100644 src/com/dougkeen/bart/Route.java create mode 100644 src/com/dougkeen/bart/Station.java create mode 100644 src/com/dougkeen/bart/ViewArrivalsActivity.java create mode 100644 src/com/dougkeen/bart/data/Arrival.java create mode 100644 src/com/dougkeen/bart/data/BartContentProvider.java create mode 100644 src/com/dougkeen/bart/data/CursorUtils.java create mode 100644 src/com/dougkeen/bart/data/DatabaseHelper.java create mode 100644 src/com/dougkeen/bart/data/FavoritesColumns.java create mode 100644 src/com/dougkeen/bart/data/RealTimeArrivals.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..6e9239f --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..60aefa5 --- /dev/null +++ b/.hgignore @@ -0,0 +1,3 @@ + +syntax: glob +bin/* \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..cdd9bf0 --- /dev/null +++ b/.project @@ -0,0 +1,33 @@ + + + DontMissTheBart + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..6032628 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/default.properties b/default.properties new file mode 100644 index 0000000..46769a7 --- /dev/null +++ b/default.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-7 diff --git a/proguard.cfg b/proguard.cfg new file mode 100644 index 0000000..12dd039 --- /dev/null +++ b/proguard.cfg @@ -0,0 +1,36 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8074c4c571b8cd19e27f4ee5545df367420686d7 GIT binary patch literal 4147 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 literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1095584ec21f71cd0afc9e0993aa2209671b590c GIT binary patch literal 1723 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 + + + \ No newline at end of file diff --git a/res/layout/add_favorite.xml b/res/layout/add_favorite.xml new file mode 100644 index 0000000..c4766af --- /dev/null +++ b/res/layout/add_favorite.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/res/layout/arrival_listing.xml b/res/layout/arrival_listing.xml new file mode 100644 index 0000000..c73fd2a --- /dev/null +++ b/res/layout/arrival_listing.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/res/layout/favorite_listing.xml b/res/layout/favorite_listing.xml new file mode 100644 index 0000000..e4e1422 --- /dev/null +++ b/res/layout/favorite_listing.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 0000000..9acda97 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/simple_spinner_item.xml b/res/layout/simple_spinner_item.xml new file mode 100644 index 0000000..feb52ff --- /dev/null +++ b/res/layout/simple_spinner_item.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/menu/favorites_menu.xml b/res/menu/favorites_menu.xml new file mode 100644 index 0000000..b233735 --- /dev/null +++ b/res/menu/favorites_menu.xml @@ -0,0 +1,5 @@ + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000..4faa818 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FF000000 + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..4be41e5 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,18 @@ + + + Don\'t Miss The Bart + Favorite Routes + Press the menu button and select "Add + favorite" to + add a route + Add favorite + Add a favorite route + Origin + Destination + Save + Cancel + The origin and destination stations must be different + You must select a destination station + You must select an origin station + Please wait while real time arrival data is loaded... + diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..ceb242f --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/src/com/dougkeen/bart/AddFavoriteActivity.java b/src/com/dougkeen/bart/AddFavoriteActivity.java new file mode 100644 index 0000000..b7a5e54 --- /dev/null +++ b/src/com/dougkeen/bart/AddFavoriteActivity.java @@ -0,0 +1,92 @@ +package com.dougkeen.bart; + +import com.dougkeen.bart.data.FavoritesColumns; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; +import android.widget.Toast; + +public class AddFavoriteActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + // TODO Auto-generated method stub + super.onCreate(savedInstanceState); + + setContentView(R.layout.add_favorite); + + SpinnerAdapter originSpinnerAdapter = new ArrayAdapter(this, + R.layout.simple_spinner_item, Station.values()); + ((Spinner) findViewById(R.id.origin_spinner)) + .setAdapter(originSpinnerAdapter); + + SpinnerAdapter destinationSpinnerAdapter = new ArrayAdapter( + this, + R.layout.simple_spinner_item, Station.values()); + ((Spinner) findViewById(R.id.destination_spinner)) + .setAdapter(destinationSpinnerAdapter); + + Button saveButton = (Button) findViewById(R.id.saveButton); + saveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onSaveButtonClick(); + } + + }); + + Button cancelButton = (Button) findViewById(R.id.cancelButton); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + } + + protected void onSaveButtonClick() { + Station origin = (Station) ((Spinner) findViewById(R.id.origin_spinner)) + .getSelectedItem(); + Station destination = (Station) ((Spinner) findViewById(R.id.destination_spinner)) + .getSelectedItem(); + + if (origin == null) { + Toast.makeText(this, com.dougkeen.bart.R.string.error_null_origin, + Toast.LENGTH_LONG); + return; + } + if (destination == null) { + Toast.makeText(this, + com.dougkeen.bart.R.string.error_null_destination, + Toast.LENGTH_LONG); + return; + } + if (origin.equals(destination)) { + Toast.makeText( + this, + com.dougkeen.bart.R.string.error_matching_origin_and_destination, + Toast.LENGTH_LONG); + return; + } + + ContentValues values = new ContentValues(); + values.put(FavoritesColumns.FROM_STATION.string, origin.abbreviation); + values.put(FavoritesColumns.TO_STATION.string, destination.abbreviation); + + Uri newUri = getContentResolver().insert( + Constants.FAVORITE_CONTENT_URI, values); + setResult(RESULT_OK, (new Intent()).setAction(newUri.toString())); + finish(); + } + +} diff --git a/src/com/dougkeen/bart/Constants.java b/src/com/dougkeen/bart/Constants.java new file mode 100644 index 0000000..62710d4 --- /dev/null +++ b/src/com/dougkeen/bart/Constants.java @@ -0,0 +1,11 @@ +package com.dougkeen.bart; + +import android.net.Uri; + +public class Constants { + public static final String AUTHORITY = "com.dougkeen.bart.dataprovider"; + public static final String FAVORITE_CONTENT_TYPE = "vnd.android.cursor.dir/com.dougkeen.bart.favorite"; + public static final String FAVORITE_CONTENT_ITEM_TYPE = "vnd.android.cursor.item/com.dougkeen.bart.favorite"; + public static final Uri FAVORITE_CONTENT_URI = Uri.parse("content://" + + AUTHORITY + "/favorites"); +} diff --git a/src/com/dougkeen/bart/EtdContentHandler.java b/src/com/dougkeen/bart/EtdContentHandler.java new file mode 100644 index 0000000..67765e2 --- /dev/null +++ b/src/com/dougkeen/bart/EtdContentHandler.java @@ -0,0 +1,95 @@ +package com.dougkeen.bart; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import com.dougkeen.bart.data.Arrival; +import com.dougkeen.bart.data.RealTimeArrivals; + +public class EtdContentHandler extends DefaultHandler { + public EtdContentHandler(Station origin, Station destination, + List routes) { + super(); + realTimeArrivals = new RealTimeArrivals(origin, destination, routes); + } + + private final static List TAGS = Arrays.asList("date", "time", + "abbreviation", "minutes", "platform", "direction", "length", + "hexcolor", "bikeflag"); + + private RealTimeArrivals realTimeArrivals; + + public RealTimeArrivals getRealTimeArrivals() { + return realTimeArrivals; + } + + private String date; + private String currentDestination; + private String currentValue; + private Arrival currentArrival; + private boolean isParsingTag; + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (isParsingTag) { + currentValue = new String(ch, start, length); + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + if (TAGS.contains(localName)) { + isParsingTag = true; + } + if (localName.equals("estimate")) { + currentArrival = new Arrival(); + currentArrival.setDestination(Station + .getByAbbreviation(currentDestination)); + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + if (localName.equals("date")) { + date = currentValue; + } else if (localName.equals("time")) { + realTimeArrivals.setTime(Date.parse(date + " " + currentValue)); + } else if (localName.equals("abbreviation")) { + currentDestination = currentValue; + } else if (localName.equals("minutes")) { + if (currentValue.equalsIgnoreCase("arrived")) { + currentArrival.setMinutes(0); + } else { + currentArrival.setMinutes(Integer.parseInt(currentValue)); + } + } else if (localName.equals("platform")) { + currentArrival.setPlatform(currentValue); + } else if (localName.equals("direction")) { + currentArrival.setDirection(currentValue); + } else if (localName.equals("length")) { + currentArrival.setTrainLength(Integer.parseInt(currentValue)); + } else if (localName.equals("hexcolor")) { + currentArrival.setDestinationColor("#ff" + + currentValue.substring(1)); + } else if (localName.equals("bikeflag")) { + currentArrival.setBikeAllowed(currentValue.equalsIgnoreCase("1")); + } else if (localName.equals("estimate")) { + realTimeArrivals.addArrival(currentArrival); + currentArrival = null; + } else if (localName.equals("etd")) { + currentDestination = null; + } else if (localName.equals("station")) { + realTimeArrivals.sortArrivals(); + } + isParsingTag = false; + currentValue = null; + } +} diff --git a/src/com/dougkeen/bart/FavoritesDashboardActivity.java b/src/com/dougkeen/bart/FavoritesDashboardActivity.java new file mode 100644 index 0000000..535ef97 --- /dev/null +++ b/src/com/dougkeen/bart/FavoritesDashboardActivity.java @@ -0,0 +1,83 @@ +package com.dougkeen.bart; + +import com.dougkeen.bart.data.FavoritesColumns; + +import android.app.ListActivity; +import android.content.ContentUris; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import android.widget.SimpleCursorAdapter; +import android.widget.SimpleCursorAdapter.ViewBinder; +import android.widget.TextView; + +public class FavoritesDashboardActivity extends ListActivity { + protected Cursor mQuery; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + ((TextView) findViewById(R.id.listTitle)) + .setText(R.string.favorite_routes); + ((TextView) findViewById(android.R.id.empty)) + .setText(R.string.empty_favorites_list_message); + + mQuery = managedQuery(Constants.FAVORITE_CONTENT_URI, new String[] { + FavoritesColumns._ID.string, + FavoritesColumns.FROM_STATION.string, + FavoritesColumns.TO_STATION.string }, null, null, + FavoritesColumns._ID.string); + + SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, + R.layout.favorite_listing, + mQuery, + new String[] { FavoritesColumns.FROM_STATION.string, + FavoritesColumns.TO_STATION.string }, + new int[] { R.id.originText, + R.id.destinationText }); + adapter.setViewBinder(new ViewBinder() { + @Override + public boolean setViewValue(View view, Cursor cursor, + int columnIndex) { + ((TextView) view).setText(Station.getByAbbreviation(cursor + .getString(columnIndex)).name); + return true; + } + }); + + setListAdapter(adapter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.favorites_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.add_favorite_menu_button) { + startActivity(new Intent(Intent.ACTION_INSERT, + Constants.FAVORITE_CONTENT_URI)); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + startActivity(new Intent(Intent.ACTION_VIEW, + ContentUris.withAppendedId(Constants.FAVORITE_CONTENT_URI, id))); + } +} \ No newline at end of file diff --git a/src/com/dougkeen/bart/GetRealTimeArrivalsTask.java b/src/com/dougkeen/bart/GetRealTimeArrivalsTask.java new file mode 100644 index 0000000..7cbb075 --- /dev/null +++ b/src/com/dougkeen/bart/GetRealTimeArrivalsTask.java @@ -0,0 +1,109 @@ +package com.dougkeen.bart; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.xml.sax.SAXException; + +import com.dougkeen.bart.data.RealTimeArrivals; + +import android.os.AsyncTask; +import android.util.Xml; + +public abstract class GetRealTimeArrivalsTask extends + AsyncTask { + + private final static String API_KEY = "5LD9-IAYI-TRAT-MHHW"; + private final static String API_URL = "http://api.bart.gov/api/etd.aspx?cmd=etd&key=" + + API_KEY + "&orig=%1$s&dir=%2$s"; + private final static int MAX_ATTEMPTS = 3; + + private IOException mIOException; + + private List mRoutes; + + @Override + protected RealTimeArrivals doInBackground(Params... paramsArray) { + // Always expect one param + Params params = paramsArray[0]; + + mRoutes = params.origin.getRoutesForDestination(params.destination); + + URL sourceUrl; + try { + sourceUrl = new URL(String.format(API_URL, + params.origin.abbreviation, mRoutes.get(0).getDirection())); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + + return getArrivalsFromNetwork(params, sourceUrl, 0); + } + + private RealTimeArrivals getArrivalsFromNetwork(Params params, + URL sourceUrl, int attemptNumber) { + try { + EtdContentHandler handler = new EtdContentHandler(params.origin, + params.destination, mRoutes); + Xml.parse(sourceUrl.openStream(), Xml.findEncodingByName("UTF-8"), + handler); + final RealTimeArrivals realTimeArrivals = handler + .getRealTimeArrivals(); + return realTimeArrivals; + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } catch (IOException e) { + if (attemptNumber < MAX_ATTEMPTS - 1) { + try { + Thread.sleep(5000); + } catch (InterruptedException interrupt) { + // Ignore... just go on to next attempt + } + return getArrivalsFromNetwork(params, sourceUrl, + attemptNumber++); + } else { + mIOException = e; + return null; + } + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + public static class Params { + public Params(Station origin, Station destination) { + super(); + this.origin = origin; + this.destination = destination; + } + + private Station origin; + private Station destination; + + public Station getOrigin() { + return origin; + } + + public Station getDestination() { + return destination; + } + } + + @Override + protected void onPostExecute(RealTimeArrivals result) { + if (result != null) { + onResult(result); + } else { + onNetworkError(mIOException); + } + } + + public abstract void onResult(RealTimeArrivals result); + + public abstract void onNetworkError(IOException e); +} diff --git a/src/com/dougkeen/bart/Line.java b/src/com/dougkeen/bart/Line.java new file mode 100644 index 0000000..6d1afd1 --- /dev/null +++ b/src/com/dougkeen/bart/Line.java @@ -0,0 +1,108 @@ +package com.dougkeen.bart; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +public enum Line { + RED(false, Station.MLBR, Station.SBRN, Station.SSAN, Station.COLM, + Station.DALY, Station.BALB, Station.GLEN, Station._24TH, + Station._16TH, Station.CIVC, Station.POWL, Station.MONT, + Station.EMBR, Station.WOAK, Station._12TH, Station._19TH, + Station.MCAR, Station.ASHB, Station.DBRK, Station.NBRK, + Station.PLZA, Station.DELN, Station.RICH), + ORANGE(false, Station.FRMT, Station.UCTY, Station.SHAY, + Station.HAYW, Station.BAYF, Station.SANL, Station.COLS, + Station.FTVL, Station.LAKE, Station._12TH, Station._19TH, + Station.MCAR, Station.ASHB, Station.DBRK, Station.NBRK, + Station.PLZA, Station.DELN, Station.RICH), + YELLOW(false, Station.MLBR, Station.SFIA, Station.SBRN, + Station.SSAN, Station.COLM, Station.DALY, Station.BALB, + Station.GLEN, Station._24TH, Station._16TH, Station.CIVC, + Station.POWL, Station.MONT, Station.EMBR, Station.WOAK, + Station._12TH, Station._19TH, Station.MCAR, Station.ROCK, + Station.ORIN, Station.LAFY, Station.WCRK, Station.PHIL, + Station.CONC, Station.NCON), + BLUE(true, Station.DALY, Station.BALB, Station.GLEN, Station._24TH, + Station._16TH, Station.CIVC, Station.POWL, Station.MONT, + Station.EMBR, Station.WOAK, Station.LAKE, Station.FTVL, + Station.COLS, Station.SANL, Station.BAYF, Station.CAST, + Station.WDUB, Station.DUBL), + GREEN(true, Station.DALY, Station.BALB, Station.GLEN, Station._24TH, + Station._16TH, Station.CIVC, Station.POWL, Station.MONT, + Station.EMBR, Station.WOAK, Station.LAKE, Station.FTVL, + Station.COLS, Station.SANL, Station.BAYF, Station.HAYW, + Station.SHAY, Station.UCTY, Station.FRMT), + YELLOW_ORANGE_TRANSFER(YELLOW, ORANGE, Station.MLBR, Station.SFIA, + Station.SBRN, Station.SSAN, Station.COLM, Station.DALY, + Station.BALB, Station.GLEN, Station._24TH, Station._16TH, + Station.CIVC, Station.POWL, Station.MONT, Station.EMBR, + Station.WOAK, Station._12TH, Station._19TH, + Station.MCAR, Station.ASHB, Station.DBRK, Station.NBRK, + Station.PLZA, Station.DELN, Station.RICH); + + public final List stations; + + protected final boolean directionMayInvert; + + protected final boolean requiresTransfer; + + protected final Line transferLine1; + + protected final Line transferLine2; + + private Line(boolean directionMayInvert, + Station... stationArray) { + this.requiresTransfer = false; + this.directionMayInvert = directionMayInvert; + stations = Arrays.asList(stationArray); + this.transferLine1 = null; + this.transferLine2 = null; + } + + private Line(Line transferLine1, Line transferLine2, + Station... stationArray) { + this.requiresTransfer = true; + this.directionMayInvert = false; + stations = Arrays.asList(stationArray); + this.transferLine1 = transferLine1; + this.transferLine2 = transferLine2; + } + + public static Collection getLinesForStation(Station station) { + Collection lines = new ArrayList(); + for (Line line : Line.values()) { + if (line.stations.contains(station)) { + lines.add(line); + } + } + return lines; + } + + public static Set getPotentialDestinations(Station station) { + Set destinations = new TreeSet(); + + for (Line line : getLinesForStation(station)) { + destinations.addAll(line.stations); + } + + destinations.remove(station); + + return destinations; + } + + public boolean trainDestinationIsApplicable(Station station) { + if (transferLine1 != null && transferLine1.stations.contains(station)) { + return true; + } else if (transferLine2 != null + && transferLine2.stations.contains(station)) { + return true; + } else { + return stations.contains(station); + } + } + +} diff --git a/src/com/dougkeen/bart/Route.java b/src/com/dougkeen/bart/Route.java new file mode 100644 index 0000000..3ac9c00 --- /dev/null +++ b/src/com/dougkeen/bart/Route.java @@ -0,0 +1,66 @@ +package com.dougkeen.bart; + +public class Route { + private Station origin; + private Station destination; + private Line line; + private boolean requiresTransfer; + private String direction; + + public Station getOrigin() { + return origin; + } + + public void setOrigin(Station origin) { + this.origin = origin; + } + + public Station getDestination() { + return destination; + } + + public void setDestination(Station destination) { + this.destination = destination; + } + + public Line getLine() { + return line; + } + + public void setLine(Line line) { + this.line = line; + } + + public boolean hasTransfer() { + return requiresTransfer; + } + + public void setTransfer(boolean requiresTransfer) { + this.requiresTransfer = requiresTransfer; + } + + public String getDirection() { + return direction; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Route [origin="); + builder.append(origin); + builder.append(", destination="); + builder.append(destination); + builder.append(", line="); + builder.append(line); + builder.append(", requiresTransfer="); + builder.append(requiresTransfer); + builder.append(", direction="); + builder.append(direction); + builder.append("]"); + return builder.toString(); + } +} diff --git a/src/com/dougkeen/bart/Station.java b/src/com/dougkeen/bart/Station.java new file mode 100644 index 0000000..984928d --- /dev/null +++ b/src/com/dougkeen/bart/Station.java @@ -0,0 +1,165 @@ +package com.dougkeen.bart; + +import java.util.ArrayList; +import java.util.List; + +public enum Station { + _12TH("12th", "12th St./Oakland City Center", false, "bayf"), + _16TH("16th", "16th St. Mission", false), + _19TH("19th", "19th St./Oakland", false, "bayf"), + _24TH("24th", "24th St. Mission", false), + ASHB("ashb", "Ashby", false, "mcar"), + BALB("balb", "Balboa Park", false), + BAYF("bayf", "Bay Fair", true, "mcar"), + CAST("cast", "Castro Valley", false, "bayf"), + CIVC("civc", "Civic Center", false), + COLS("cols", "Coliseum/Oakland Airport", true, "mcar"), + COLM("colm", "Colma", false, "balb", "balb"), + CONC("conc", "Concord", false, "mcar"), + DALY("daly", "Daly City", false), + DBRK("dbrk", "Downtown Berkeley", false, "mcar"), + DUBL("dubl", "Dublin/Pleasanton", false, "bayf"), + DELN("deln", "El Cerrito del Norte", false, "mcar"), + PLZA("plza", "El Cerrito Plaza", false, "mcar"), + EMBR("embr", "Embarcadero", false), + FRMT("frmt", "Fremont", true, "bayf"), + FTVL("ftvl", "Fruitvale", true, "mcar"), + GLEN("glen", "Glen Park", false), + HAYW("hayw", "Hayward", true, "bayf"), + LAFY("lafy", "Lafayette", false, "mcar"), + LAKE("lake", "Lake Merritt", true, "mcar"), + MCAR("mcar", "MacArthur", false, "bayf"), + MLBR("mlbr", "Millbrae", false, "balb", "balb"), + MONT("mont", "Montgomery St.", false), + NBRK("nbrk", "North Berkeley", false, "mcar"), + NCON("ncon", "North Concord/Martinez", false, "mcar"), + ORIN("orin", "Orinda", false, "mcar"), + PITT("pitt", "Pittsburg/Bay Point", false, "mcar"), + PHIL("phil", "Pleasant Hill", false, "mcar"), + POWL("powl", "Powell St.", false), + RICH("rich", "Richmond", false, "mcar"), + ROCK("rock", "Rockridge", false, "mcar"), + SBRN("sbrn", "San Bruno", false, "balb", "balb"), + SANL("sanl", "San Leandro", true, "mcar"), + SFIA("sfia", "SFO Airport", false, "sbrn", "balb"), + SHAY("shay", "South Hayward", true, "bayf"), + SSAN("ssan", "South San Francisco", false, "balb", "balb"), + UCTY("ucty", "Union City", true, "bayf"), + WCRK("wcrk", "Walnut Creek", false, "mcar"), + WDUB("wdub", "West Dublin/Pleasanton", false, "bayf"), + WOAK("woak", "West Oakland", false); + + public final String abbreviation; + public final String name; + public final boolean invertDirection; + protected final String inboundTransferStation; + protected final String outboundTransferStation; + + private Station(String abbreviation, String name, boolean invertDirection) { + this.abbreviation = abbreviation; + this.name = name; + this.invertDirection = invertDirection; + this.inboundTransferStation = null; + this.outboundTransferStation = null; + } + + private Station(String abbreviation, String name, boolean invertDirection, + String transferStation) { + this.abbreviation = abbreviation; + this.name = name; + this.invertDirection = invertDirection; + this.inboundTransferStation = transferStation; + this.outboundTransferStation = null; + } + + private Station(String abbreviation, String name, boolean invertDirection, + String inboundTransferStation, String outboundTransferStation) { + this.abbreviation = abbreviation; + this.name = name; + this.invertDirection = invertDirection; + this.inboundTransferStation = inboundTransferStation; + this.outboundTransferStation = outboundTransferStation; + } + + public static Station getByAbbreviation(String abbr) { + if (abbr == null) { + return null; + } else if (Character.isDigit(abbr.charAt(0))) { + return Station.valueOf("_" + abbr.toUpperCase()); + } else { + return Station.valueOf(abbr.toUpperCase()); + } + } + + public Station getInboundTransferStation() { + return getByAbbreviation(inboundTransferStation); + } + + public Station getOutboundTransferStation() { + return getByAbbreviation(outboundTransferStation); + } + + public boolean isValidEndpointForDestination(Station dest, Station endpoint) { + for (Line line : Line.values()) { + int origIndex = line.stations.indexOf(this); + if (origIndex < 0) + continue; + int destIndex = line.stations.indexOf(dest); + if (destIndex < 0) + continue; + int endpointIndex = line.stations.indexOf(endpoint); + if (endpointIndex >= 0) + return true; + } + return false; + } + + public List getRoutesForDestination(Station dest) { + return getRoutesForDestination(dest, false); + } + + public List getRoutesForDestination(Station dest, boolean isTransfer) { + if (dest == null) + return null; + Boolean isNorth = null; + List returnList = new ArrayList(); + for (Line line : Line.values()) { + int origIndex = line.stations.indexOf(this); + if (origIndex < 0) + continue; + int destIndex = line.stations.indexOf(dest); + if (destIndex < 0) + continue; + + isNorth = (origIndex < destIndex); + if (line.directionMayInvert && this.invertDirection) { + isNorth = !isNorth; + } + Route route = new Route(); + route.setOrigin(this); + route.setDestination(dest); + route.setDirection(isNorth ? "n" : "s"); + route.setLine(line); + if (isTransfer || line.requiresTransfer) { + route.setTransfer(true); + } else { + route.setTransfer(false); + } + returnList.add(route); + } + if (isNorth == null) { + if (outboundTransferStation != null) { + returnList.addAll(getOutboundTransferStation() + .getRoutesForDestination(dest, true)); + } else { + returnList.addAll(getRoutesForDestination(dest + .getInboundTransferStation(), true)); + } + } + return returnList; + } + + public String toString() { + return name; + } +} diff --git a/src/com/dougkeen/bart/ViewArrivalsActivity.java b/src/com/dougkeen/bart/ViewArrivalsActivity.java new file mode 100644 index 0000000..cfcf1fd --- /dev/null +++ b/src/com/dougkeen/bart/ViewArrivalsActivity.java @@ -0,0 +1,195 @@ +package com.dougkeen.bart; + +import java.io.IOException; +import java.util.List; + +import android.app.ListActivity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.PowerManager; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import com.dougkeen.bart.GetRealTimeArrivalsTask.Params; +import com.dougkeen.bart.data.Arrival; +import com.dougkeen.bart.data.FavoritesColumns; +import com.dougkeen.bart.data.RealTimeArrivals; + +public class ViewArrivalsActivity extends ListActivity { + + private Uri mUri; + + private Station mOrigin; + private Station mDestination; + + private ArrayAdapter mArrivalsAdapter; + + private TextView mListTitleView; + + private AsyncTask mGetArrivalsTask; + + private boolean mIsAutoUpdating = false; + + private final Runnable AUTO_UPDATE_RUNNABLE = new Runnable() { + @Override + public void run() { + runAutoUpdate(); + } + }; + + private PowerManager.WakeLock mWakeLock; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // TODO Auto-generated method stub + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + final Intent intent = getIntent(); + + String action = intent.getAction(); + + if (Intent.ACTION_VIEW.equals(action)) { + mUri = intent.getData(); + } + + Cursor cursor = managedQuery(mUri, new String[] { + FavoritesColumns.FROM_STATION.string, + FavoritesColumns.TO_STATION.string }, null, null, null); + + if (!cursor.moveToFirst()) { + throw new IllegalStateException("URI not found: " + mUri.toString()); + } + mOrigin = Station.getByAbbreviation(cursor.getString(0)); + mDestination = Station.getByAbbreviation(cursor.getString(1)); + + String header = mOrigin.name + " to " + mDestination.name; + + mListTitleView = (TextView) findViewById(R.id.listTitle); + mListTitleView.setText(header); + ((TextView) findViewById(android.R.id.empty)) + .setText(R.string.arrival_wait_message); + + mArrivalsAdapter = new ArrayAdapter( + this, R.layout.simple_spinner_item); + setListAdapter(mArrivalsAdapter); + + fetchLatestArrivals(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + PowerManager powerManaer = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManaer.newWakeLock( + PowerManager.SCREEN_DIM_WAKE_LOCK, "ViewArrivalsActivity"); + mWakeLock.acquire(); + } else if (mWakeLock != null) { + mWakeLock.release(); + } + } + + private void fetchLatestArrivals() { + mGetArrivalsTask = new GetRealTimeArrivalsTask() { + @Override + public void onResult(RealTimeArrivals result) { + processLatestArrivals(result); + } + + @Override + public void onNetworkError(IOException e) { + Toast.makeText(ViewArrivalsActivity.this, e.getMessage(), + Toast.LENGTH_SHORT).show(); + } + }; + mGetArrivalsTask.execute(new GetRealTimeArrivalsTask.Params(mOrigin, + mDestination)); + } + + protected void processLatestArrivals(RealTimeArrivals result) { + Arrival firstArrival = null; + final List arrivals = result.getArrivals(); + if (mArrivalsAdapter.getCount() > 0) { + int adapterIndex = -1; + for (Arrival arrival : arrivals) { + adapterIndex++; + Arrival existingArrival = null; + if (adapterIndex < mArrivalsAdapter.getCount()) { + existingArrival = mArrivalsAdapter.getItem(adapterIndex); + } + while (existingArrival != null + && !arrival.equals(existingArrival)) { + mArrivalsAdapter.remove(existingArrival); + if (adapterIndex < mArrivalsAdapter.getCount()) { + existingArrival = mArrivalsAdapter + .getItem(adapterIndex); + } else { + existingArrival = null; + } + } + if (existingArrival != null) { + existingArrival.mergeEstimate(arrival); + } else { + mArrivalsAdapter.add(arrival); + existingArrival = arrival; + } + if (firstArrival == null) { + firstArrival = existingArrival; + } + } + } else { + for (Arrival arrival : arrivals) { + if (firstArrival == null) { + firstArrival = arrival; + } + mArrivalsAdapter.add(arrival); + } + } + mArrivalsAdapter.notifyDataSetChanged(); + + if (hasWindowFocus() && firstArrival != null) { + if (firstArrival.getUncertaintySeconds() > 17 + || firstArrival.getMinutes() == 0) { + // Get more data in 20s + mListTitleView.postDelayed(new Runnable() { + @Override + public void run() { + fetchLatestArrivals(); + } + }, 20000); + } else { + // Get more when next train arrives + mListTitleView.postDelayed(new Runnable() { + @Override + public void run() { + fetchLatestArrivals(); + } + }, firstArrival.getMinSecondsLeft() * 1000); + } + if (!mIsAutoUpdating) { + mIsAutoUpdating = true; + runAutoUpdate(); + } + } else { + mIsAutoUpdating = false; + } + + } + + private void runAutoUpdate() { + if (mIsAutoUpdating) { + mArrivalsAdapter.notifyDataSetChanged(); + } + if (hasWindowFocus()) { + mListTitleView.postDelayed(AUTO_UPDATE_RUNNABLE, 1000); + } else { + mIsAutoUpdating = false; + } + } +} diff --git a/src/com/dougkeen/bart/data/Arrival.java b/src/com/dougkeen/bart/data/Arrival.java new file mode 100644 index 0000000..291e21c --- /dev/null +++ b/src/com/dougkeen/bart/data/Arrival.java @@ -0,0 +1,219 @@ +package com.dougkeen.bart.data; + +import com.dougkeen.bart.Station; + +public class Arrival implements Comparable { + public Arrival() { + super(); + } + + public Arrival(String destinationAbbr, String destinationColor, + String platform, String direction, boolean bikeAllowed, + int trainLength, int minutes) { + super(); + this.destination = Station.getByAbbreviation(destinationAbbr); + this.destinationColor = destinationColor; + this.platform = platform; + this.direction = direction; + this.bikeAllowed = bikeAllowed; + this.trainLength = trainLength; + this.minutes = minutes; + } + + private Station destination; + private String destinationColor; + private String platform; + private String direction; + private boolean bikeAllowed; + private int trainLength; + private boolean requiresTransfer; + + private int minutes; + + private long minEstimate; + private long maxEstimate; + + public Station getDestination() { + return destination; + } + + public void setDestination(Station destination) { + this.destination = destination; + } + + public String getDestinationName() { + if (destination != null) + return destination.name; + return null; + } + + public String getDestinationAbbreviation() { + if (destination != null) + return destination.abbreviation; + return null; + } + + public String getDestinationColor() { + return destinationColor; + } + + public void setDestinationColor(String destinationColor) { + this.destinationColor = destinationColor; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getDirection() { + return direction; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + public boolean isBikeAllowed() { + return bikeAllowed; + } + + public void setBikeAllowed(boolean bikeAllowed) { + this.bikeAllowed = bikeAllowed; + } + + public int getTrainLength() { + return trainLength; + } + + public void setTrainLength(int trainLength) { + this.trainLength = trainLength; + } + + public boolean getRequiresTransfer() { + return requiresTransfer; + } + + public void setRequiresTransfer(boolean requiresTransfer) { + this.requiresTransfer = requiresTransfer; + } + + public int getMinutes() { + return minutes; + } + + public void setMinutes(int minutes) { + this.minutes = minutes; + } + + public long getMinEstimate() { + return minEstimate; + } + + public void setMinEstimate(long minEstimate) { + this.minEstimate = minEstimate; + } + + public long getMaxEstimate() { + return maxEstimate; + } + + public void setMaxEstimate(long maxEstimate) { + this.maxEstimate = maxEstimate; + } + + public int getUncertaintySeconds() { + return (int) (maxEstimate - minEstimate + 1000) / 2000; + } + + public int getMinSecondsLeft() { + return (int) ((getMinEstimate() - System.currentTimeMillis()) / 1000); + } + + public int getMaxSecondsLeft() { + return (int) ((getMaxEstimate() - System.currentTimeMillis()) / 1000); + } + + public int getMeanSecondsLeft() { + return (int) (((getMinEstimate() + getMaxEstimate()) / 2 - System + .currentTimeMillis()) / 1000); + } + + public void calculateEstimates(long originalEstimateTime) { + setMinEstimate(originalEstimateTime + (getMinutes() * 60 * 1000)); + setMaxEstimate(getMinEstimate() + (59 * 1000)); + } + + public void mergeEstimate(Arrival arrival) { + setMinEstimate(Math.max(getMinEstimate(), arrival.getMinEstimate())); + setMaxEstimate(Math.min(getMaxEstimate(), arrival.getMaxEstimate())); + } + + @Override + public int compareTo(Arrival another) { + return (this.getMinutes() > another.getMinutes()) ? 1 : ( + (this.getMinutes() == another.getMinutes()) ? 0 : -1); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Arrival other = (Arrival) obj; + if (bikeAllowed != other.bikeAllowed) + return false; + if (destination != other.destination) + return false; + if (destinationColor == null) { + if (other.destinationColor != null) + return false; + } else if (!destinationColor.equals(other.destinationColor)) + return false; + if (direction == null) { + if (other.direction != null) + return false; + } else if (!direction.equals(other.direction)) + return false; + if (platform == null) { + if (other.platform != null) + return false; + } else if (!platform.equals(other.platform)) + return false; + if (trainLength != other.trainLength) + return false; + + long delta = (getMinEstimate() - other.getMinEstimate()); + return delta > -60000 && delta < 60000; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(destination); + if(requiresTransfer) { + builder.append(" (w/ xfer)"); + } + builder.append(", "); + builder.append(trainLength); + int secondsLeft = getMeanSecondsLeft(); + if (getMinutes() == 0 || secondsLeft < 0) { + builder.append(" car train has arrived"); + } else { + builder.append(" car train in "); + builder.append(secondsLeft / 60); + builder.append("m, "); + builder.append(secondsLeft % 60); + builder.append("s, ±"); + builder.append(getUncertaintySeconds()); + builder.append("s"); + } + return builder.toString(); + } +} diff --git a/src/com/dougkeen/bart/data/BartContentProvider.java b/src/com/dougkeen/bart/data/BartContentProvider.java new file mode 100644 index 0000000..fb43be6 --- /dev/null +++ b/src/com/dougkeen/bart/data/BartContentProvider.java @@ -0,0 +1,188 @@ +package com.dougkeen.bart.data; + +import java.util.HashMap; + +import com.dougkeen.bart.Constants; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.text.TextUtils; + +public class BartContentProvider extends ContentProvider { + + private static final UriMatcher sUriMatcher; + private static HashMap sFavoritesProjectionMap; + + private static final int FAVORITES = 1; + private static final int FAVORITE_ID = 2; + + /** + * The default sort order for events + */ + private static final String DEFAULT_SORT_ORDER = FavoritesColumns.FROM_STATION.string + + " DESC"; + + static { + sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + sUriMatcher.addURI(Constants.AUTHORITY, "favorites", FAVORITES); + sUriMatcher.addURI(Constants.AUTHORITY, "favorites/#", FAVORITE_ID); + + sFavoritesProjectionMap = new HashMap(); + sFavoritesProjectionMap.put(FavoritesColumns._ID.string, + FavoritesColumns._ID.string); + sFavoritesProjectionMap.put(FavoritesColumns.FROM_STATION.string, + FavoritesColumns.FROM_STATION.string); + sFavoritesProjectionMap.put(FavoritesColumns.TO_STATION.string, + FavoritesColumns.TO_STATION.string); + } + + private DatabaseHelper mDatabaseHelper; + + @Override + public boolean onCreate() { + mDatabaseHelper = new DatabaseHelper(getContext()); + return true; + } + + @Override + public String getType(Uri uri) { + int match = sUriMatcher.match(uri); + if (match == FAVORITES) { + return Constants.FAVORITE_CONTENT_TYPE; + } else if (match == FAVORITE_ID) { + return Constants.FAVORITE_CONTENT_ITEM_TYPE; + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, + String sortOrder) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + + SQLiteDatabase db = mDatabaseHelper.getReadableDatabase(); + + String orderBy = sortOrder; + + int match = sUriMatcher.match(uri); + + if (match == FAVORITES) { + qb.setTables(DatabaseHelper.FAVORITES_TABLE_NAME); + qb.setProjectionMap(sFavoritesProjectionMap); + } else if (match == FAVORITE_ID) { + qb.setTables(DatabaseHelper.FAVORITES_TABLE_NAME); + qb.setProjectionMap(sFavoritesProjectionMap); + qb.appendWhere(FavoritesColumns._ID + " = " + + uri.getPathSegments().get(1)); + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // If no sort order is specified use the default + if (TextUtils.isEmpty(orderBy)) { + orderBy = DEFAULT_SORT_ORDER; + } + + // Get the database and run the query + Cursor cursor = qb.query(db, projection, selection, selectionArgs, + null, null, orderBy); + + // Tell the cursor what uri to watch, so it knows when its source data + // changes + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues initialValues) { + // TODO: Hook this up to the REST service? + + ContentValues values; + if (initialValues != null) { + values = new ContentValues(initialValues); + } else { + values = new ContentValues(); + } + + SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + + // Validate the requested uri + int match = sUriMatcher.match(uri); + if (match == FAVORITES) { + long rowId = -1; + Cursor cursor = db + .query(DatabaseHelper.FAVORITES_TABLE_NAME, + new String[] { FavoritesColumns._ID.string }, + FavoritesColumns.FROM_STATION + "=? AND " + + FavoritesColumns.TO_STATION + "=?", + new String[] { + values.getAsString(FavoritesColumns.FROM_STATION.string), + values.getAsString(FavoritesColumns.TO_STATION.string) }, + null, + null, + null); + try { + if (cursor.moveToFirst()) { + rowId = cursor.getLong(0); + } + } finally { + CursorUtils.closeCursorQuietly(cursor); + } + if (rowId < 0) { + rowId = db.insert(DatabaseHelper.FAVORITES_TABLE_NAME, + FavoritesColumns.FROM_STATION.string, values); + } + if (rowId > 0) { + Uri eventUri = ContentUris.withAppendedId( + Constants.FAVORITE_CONTENT_URI, rowId); + getContext().getContentResolver().notifyChange(eventUri, null, + false); + return eventUri; + } + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + + throw new SQLException("Failed to insert row into " + uri); + } + + @Override + public int update(Uri uri, ContentValues values, String where, + String[] whereArgs) { + return 0; + // No updating supported yet + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + // TODO: Sync with REST service? + SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + int count; + int match = sUriMatcher.match(uri); + if (match == FAVORITES) { + count = db.delete(DatabaseHelper.FAVORITES_TABLE_NAME, where, + whereArgs); + } else if (match == FAVORITE_ID) { + String favoriteId = uri.getPathSegments().get(1); + count = db.delete(DatabaseHelper.FAVORITES_TABLE_NAME, + FavoritesColumns._ID + " = " + + favoriteId + + (!TextUtils.isEmpty(where) ? " AND (" + where + + ')' : ""), whereArgs); + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + + getContext().getContentResolver().notifyChange(uri, null); + return count; + } +} diff --git a/src/com/dougkeen/bart/data/CursorUtils.java b/src/com/dougkeen/bart/data/CursorUtils.java new file mode 100644 index 0000000..593d6e3 --- /dev/null +++ b/src/com/dougkeen/bart/data/CursorUtils.java @@ -0,0 +1,15 @@ +package com.dougkeen.bart.data; + +import android.database.Cursor; + +public final class CursorUtils { + private CursorUtils() { + // Static only class + } + + public static final void closeCursorQuietly(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } +} diff --git a/src/com/dougkeen/bart/data/DatabaseHelper.java b/src/com/dougkeen/bart/data/DatabaseHelper.java new file mode 100644 index 0000000..a9decb5 --- /dev/null +++ b/src/com/dougkeen/bart/data/DatabaseHelper.java @@ -0,0 +1,29 @@ +package com.dougkeen.bart.data; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DatabaseHelper extends SQLiteOpenHelper { + + private static final String DATABASE_NAME = "bart.dougkeen.db"; + private static final int DATABASE_VERSION = 1; + + public static final String FAVORITES_TABLE_NAME = "Favorites"; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + FAVORITES_TABLE_NAME + " (" + + FavoritesColumns._ID.getColumnDef() + " PRIMARY KEY, " + + FavoritesColumns.FROM_STATION.getColumnDef() + ", " + + FavoritesColumns.TO_STATION.getColumnDef() + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } +} diff --git a/src/com/dougkeen/bart/data/FavoritesColumns.java b/src/com/dougkeen/bart/data/FavoritesColumns.java new file mode 100644 index 0000000..fc3d53e --- /dev/null +++ b/src/com/dougkeen/bart/data/FavoritesColumns.java @@ -0,0 +1,20 @@ +package com.dougkeen.bart.data; + +public enum FavoritesColumns { + _ID("_id", "INTEGER"), + FROM_STATION("FROM_STATION", "TEXT"), + TO_STATION("TO_STATION", "TEXT"); + + // This class cannot be instantiated + private FavoritesColumns(String string, String type) { + this.string = string; + this.sqliteType = type; + } + + public final String string; + public final String sqliteType; + + protected String getColumnDef() { + return string + " " + sqliteType; + } +} diff --git a/src/com/dougkeen/bart/data/RealTimeArrivals.java b/src/com/dougkeen/bart/data/RealTimeArrivals.java new file mode 100644 index 0000000..db1e2bc --- /dev/null +++ b/src/com/dougkeen/bart/data/RealTimeArrivals.java @@ -0,0 +1,100 @@ +package com.dougkeen.bart.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.dougkeen.bart.Route; +import com.dougkeen.bart.Station; + +public class RealTimeArrivals { + public RealTimeArrivals(Station origin, Station destination, + List routes) { + this.origin = origin; + this.destination = destination; + this.routes = routes; + } + + private Station origin; + private Station destination; + private long time; + + private List arrivals; + + private List routes; + + public Station getOrigin() { + return origin; + } + + public void setOrigin(Station origin) { + this.origin = origin; + } + + public Station getDestination() { + return destination; + } + + public void setDestination(Station destination) { + this.destination = destination; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public List getArrivals() { + if (arrivals == null) { + arrivals = new ArrayList(); + } + return arrivals; + } + + public void setArrivals(List arrivals) { + this.arrivals = arrivals; + } + + public void addArrival(Arrival arrival) { + Station destination = Station.getByAbbreviation(arrival + .getDestinationAbbreviation()); + for (Route route : routes) { + if (route.getLine().trainDestinationIsApplicable(destination)) { + arrival.setRequiresTransfer(route.hasTransfer()); + getArrivals().add(arrival); + arrival.calculateEstimates(time); + return; + } + } + } + + public void sortArrivals() { + Collections.sort(getArrivals()); + } + + public List getRoutes() { + return routes; + } + + public void setRoutes(List routes) { + this.routes = routes; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RealTimeArrivals [origin="); + builder.append(origin); + builder.append(", destination="); + builder.append(destination); + builder.append(", time="); + builder.append(time); + builder.append(", arrivals="); + builder.append(arrivals); + builder.append("]"); + return builder.toString(); + } +}