/*
* 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 org.onebusaway.android.provider;
import org.onebusaway.android.BuildConfig;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaAnalytics;
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.io.elements.ObaRegionElement;
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 android.text.format.Time;
import java.util.ArrayList;
/**
* 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 StopsColumns {
/**
* The code for the stop, e.g. 001_123
* <P>
* Type: TEXT
* </P>
*/
public static final String CODE = "code";
/**
* The user specified name of the stop, e.g. "13th Ave E & John St"
* <P>
* Type: TEXT
* </P>
*/
public static final String NAME = "name";
/**
* The stop direction, one of: N, NE, NW, E, SE, SW, S, W or null
* <P>
* Type: TEXT
* </P>
*/
public static final String DIRECTION = "direction";
/**
* The latitude of the stop location.
* <P>
* Type: DOUBLE
* </P>
*/
public static final String LATITUDE = "latitude";
/**
* The longitude of the stop location.
* <P>
* Type: DOUBLE
* </P>
*/
public static final String LONGITUDE = "longitude";
/**
* The region ID
* <P>
* Type: INTEGER
* </P>
*/
public static final String REGION_ID = "region_id";
}
protected interface RoutesColumns {
/**
* The short name of the route, e.g. "10"
* <P>
* Type: TEXT
* </P>
*/
public static final String SHORTNAME = "short_name";
/**
* The long name of the route, e.g "Downtown to U-District"
* <P>
* Type: TEXT
* </P>
*/
public static final String LONGNAME = "long_name";
/**
* Returns the URL of the route schedule.
* <P>
* Type: TEXT
* </P>
*/
public static final String URL = "url";
/**
* The region ID
* <P>
* Type: INTEGER
* </P>
*/
public static final String REGION_ID = "region_id";
}
protected interface StopRouteKeyColumns {
/**
* The referenced Stop ID. This may or may not represent a key in the
* Stops table.
* <P>
* Type: TEXT
* </P>
*/
public static final String STOP_ID = "stop_id";
/**
* The referenced Route ID. This may or may not represent a key in the
* Routes table.
* <P>
* Type: TEXT
* </P>
*/
public static final String ROUTE_ID = "route_id";
}
protected interface StopRouteFilterColumns extends StopRouteKeyColumns {
// No additional columns
}
protected interface TripsColumns extends StopRouteKeyColumns {
/**
* The scheduled departure time for the trip in milliseconds.
* <P>
* Type: INTEGER
* </P>
*/
public static final String DEPARTURE = "departure";
/**
* The headsign of the trip, e.g., "Capitol Hill"
* <P>
* Type: TEXT
* </P>
*/
public static final String HEADSIGN = "headsign";
/**
* The user specified name of the trip.
* <P>
* Type: TEXT
* </P>
*/
public static final String NAME = "name";
/**
* The number of minutes before the arrival to notify the user
* <P>
* Type: INTEGER
* </P>
*/
public static final String REMINDER = "reminder";
/**
* A bitmask representing the days the reminder should be used.
* <P>
* Type: INTEGER
* </P>
*/
public static final String DAYS = "days";
}
protected interface TripAlertsColumns {
/**
* The trip_id key of the corresponding trip.
* <P>
* Type: TEXT
* </P>
*/
public static final String TRIP_ID = "trip_id";
/**
* The stop_id key of the corresponding trip.
* <P>
* Type: TEXT
* </P>
*/
public static final String STOP_ID = "stop_id";
/**
* The time in milliseconds to begin the polling. Unlike the "reminder"
* time in the Trips columns, this represents a specific time.
* <P>
* Type: INTEGER
* </P>
*/
public static final String START_TIME = "start_time";
/**
* The state of the the alert. Can be SCHEDULED, POLLING, NOTIFY,
* CANCELED
* <P>
* Type: INTEGER
* </P>
*/
public static final String STATE = "state";
}
protected interface UserColumns {
/**
* The number of times this resource has been accessed by the user.
* <P>
* Type: INTEGER
* </P>
*/
public static final String USE_COUNT = "use_count";
/**
* The user specified name given to this resource.
* <P>
* Type: TEXT
* </P>
*/
public static final String USER_NAME = "user_name";
/**
* The last time the user accessed the resource.
* <P>
* Type: INTEGER
* </P>
*/
public static final String ACCESS_TIME = "access_time";
/**
* Whether or not the resource is marked as a favorite (starred)
* <P>
* Type: INTEGER (1 or 0)
* </P>
*/
public static final String FAVORITE = "favorite";
/**
* This returns the user specified name, or the default name.
* <P>
* Type: TEXT
* </P>
*/
public static final String UI_NAME = "ui_name";
}
protected interface ServiceAlertsColumns {
/**
* The time it was marked as read.
* <P>
* Type: TEXT
* </P>
*/
public static final String MARKED_READ_TIME = "marked_read_time";
/**
* Whether or not the alert has been hidden by the user.
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String HIDDEN = "hidden";
}
protected interface RegionsColumns {
/**
* The name of the region.
* <P>
* Type: TEXT
* </P>
*/
public static final String NAME = "name";
/**
* The base OBA URL.
* <P>
* Type: TEXT
* </P>
*/
public static final String OBA_BASE_URL = "oba_base_url";
/**
* The base SIRI URL.
* <P>
* Type: TEXT
* </P>
*/
public static final String SIRI_BASE_URL = "siri_base_url";
/**
* The locale of the API server.
* <P>
* Type: TEXT
* </P>
*/
public static final String LANGUAGE = "lang";
/**
* The email of the person responsible for this server.
* <P>
* Type: TEXT
* </P>
*/
public static final String CONTACT_EMAIL = "contact_email";
/**
* Whether or not the server supports OBA discovery APIs.
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String SUPPORTS_OBA_DISCOVERY = "supports_api_discovery";
/**
* Whether or not the server supports OBA realtime APIs.
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String SUPPORTS_OBA_REALTIME = "supports_api_realtime";
/**
* Whether or not the server supports SIRI realtime APIs.
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String SUPPORTS_SIRI_REALTIME = "supports_siri_realtime";
/**
* The Twitter URL for the region.
* <P>
* Type: TEXT
* </P>
*/
public static final String TWITTER_URL = "twitter_url";
/**
* Whether or not the server is experimental (i.e., not production).
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String EXPERIMENTAL = "experimental";
/**
* The StopInfo URL for the region (see #103)
* <P>
* Type: TEXT
* </P>
*/
public static final String STOP_INFO_URL = "stop_info_url";
/**
* The OpenTripPlanner URL for the region
* <P>
* Type: TEXT
* </P>
*/
public static final String OTP_BASE_URL = "otp_base_url";
/**
* The email of the person responsible for the OTP server.
* <P>
* Type: TEXT
* </P>
*/
public static final String OTP_CONTACT_EMAIL = "otp_contact_email";
}
protected interface RegionBoundsColumns {
/**
* The region ID
* <P>
* Type: INTEGER
* </P>
*/
public static final String REGION_ID = "region_id";
/**
* The latitude center of the agencies coverage area
* <P>
* Type: REAL
* </P>
*/
public static final String LATITUDE = "lat";
/**
* The longitude center of the agencies coverage area
* <P>
* Type: REAL
* </P>
*/
public static final String LONGITUDE = "lon";
/**
* The height of the agencies bounding box
* <P>
* Type: REAL
* </P>
*/
public static final String LAT_SPAN = "lat_span";
/**
* The width of the agencies bounding box
* <P>
* Type: REAL
* </P>
*/
public static final String LON_SPAN = "lon_span";
}
protected interface RegionOpen311ServersColumns {
/**
* The region ID
* <P>
* Type: INTEGER
* </P>
*/
public static final String REGION_ID = "region_id";
/**
* The jurisdiction id of the open311 server
* <P>
* Type: TEXT
* </P>
*/
public static final String JURISDICTION = "jurisdiction";
/**
* The api key of the open311 server
* <P>
* Type: TEXT
* </P>
*/
public static final String API_KEY = "api_key";
/**
* The url of the open311 server
* <P>
* Type: TEXT
* </P>
*/
public static final String BASE_URL = "open311_base_url";
}
protected interface RouteHeadsignKeyColumns extends StopRouteKeyColumns {
/**
* The referenced headsign. This may or may not represent a value in the
* Trips table.
* <P>
* Type: TEXT
* </P>
*/
public static final String HEADSIGN = "headsign";
/**
* Whether or not this stop should be excluded as a favorite. This is to allow a user to
* star a route/headsign for all stops, and then remove the star from selected stops.
* <P>
* Type: BOOLEAN
* </P>
*/
public static final String EXCLUDE = "exclude";
}
public static class Stops implements BaseColumns, StopsColumns, UserColumns {
// Cannot be instantiated
private Stops() {
}
/** The URI path portion for this table */
public static final String PATH = "stops";
/** The content:// style URI for this table */
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".stop";
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".stop";
public static Uri insertOrUpdate(String id,
ContentValues values,
boolean markAsUsed) {
ContentResolver cr = Application.get().getContentResolver();
final Uri uri = Uri.withAppendedPath(CONTENT_URI, id);
Cursor c = cr.query(uri, new String[]{USE_COUNT}, null, null,
null);
Uri result;
if (c != null && c.getCount() > 0) {
// Update
if (markAsUsed) {
c.moveToFirst();
int count = c.getInt(0);
values.put(USE_COUNT, count + 1);
values.put(ACCESS_TIME, System.currentTimeMillis());
}
cr.update(uri, values, null, null);
result = uri;
} else {
// Insert
if (markAsUsed) {
values.put(USE_COUNT, 1);
values.put(ACCESS_TIME, System.currentTimeMillis());
} else {
values.put(USE_COUNT, 0);
}
values.put(_ID, id);
result = cr.insert(CONTENT_URI, values);
}
if (c != null) {
c.close();
}
return result;
}
public static boolean markAsFavorite(Context context,
Uri uri,
boolean favorite) {
ContentResolver cr = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(ObaContract.Stops.FAVORITE, favorite ? 1 : 0);
return cr.update(uri, values, null, null) > 0;
}
public static boolean markAsUnused(Context context, Uri uri) {
ContentResolver cr = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(ObaContract.Stops.USE_COUNT, 0);
values.putNull(ObaContract.Stops.ACCESS_TIME);
return cr.update(uri, values, null, null) > 0;
}
}
public static class Routes implements BaseColumns, RoutesColumns,
UserColumns {
// Cannot be instantiated
private Routes() {
}
/** The URI path portion for this table */
public static final String PATH = "routes";
/** The content:// style URI for this table */
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".route";
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".route";
public static Uri insertOrUpdate(Context context,
String id,
ContentValues values,
boolean markAsUsed) {
ContentResolver cr = context.getContentResolver();
final Uri uri = Uri.withAppendedPath(CONTENT_URI, id);
Cursor c = cr.query(uri, new String[]{USE_COUNT}, null, null,
null);
Uri result;
if (c != null && c.getCount() > 0) {
// Update
if (markAsUsed) {
c.moveToFirst();
int count = c.getInt(0);
values.put(USE_COUNT, count + 1);
values.put(ACCESS_TIME, System.currentTimeMillis());
}
cr.update(uri, values, null, null);
result = uri;
} else {
// Insert
if (markAsUsed) {
values.put(USE_COUNT, 1);
values.put(ACCESS_TIME, System.currentTimeMillis());
} else {
values.put(USE_COUNT, 0);
}
values.put(_ID, id);
result = cr.insert(CONTENT_URI, values);
}
if (c != null) {
c.close();
}
return result;
}
protected static boolean markAsFavorite(Context context,
Uri uri,
boolean favorite) {
ContentResolver cr = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(ObaContract.Routes.FAVORITE, favorite ? 1 : 0);
return cr.update(uri, values, null, null) > 0;
}
public static boolean markAsUnused(Context context, Uri uri) {
ContentResolver cr = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(ObaContract.Routes.USE_COUNT, 0);
values.putNull(ObaContract.Routes.ACCESS_TIME);
return cr.update(uri, values, null, null) > 0;
}
/**
* Returns true if this route is a favorite, false if it does not
*
* Note that this is NOT specific to headsign. If you want to know if a combination of a
* routeId and headsign is a user favorite, see
* RouteHeadsignFavorites.isFavorite(context, routeId, headsign).
*
* @param routeUri Uri for a route
* @return true if this route is a favorite, false if it does not
*/
public static boolean isFavorite(Context context, Uri routeUri) {
ContentResolver cr = context.getContentResolver();
String[] ROUTE_USER_PROJECTION = {ObaContract.Routes.FAVORITE};
Cursor c = cr.query(routeUri, ROUTE_USER_PROJECTION, null, null, null);
if (c != null) {
try {
if (c.moveToNext()) {
return (c.getInt(0) == 1);
}
} finally {
c.close();
}
}
// If we get this far, assume its not
return false;
}
}
public static class StopRouteFilters implements StopRouteFilterColumns {
// Cannot be instantiated
private StopRouteFilters() {
}
/** The URI path portion for this table */
public static final String PATH = "stop_routes_filter";
/** The content:// style URI for this table */
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".stoproutefilter";
private static final String FILTER_WHERE = STOP_ID + "=?";
/**
* Gets the filter for the specified Stop ID.
*
* @param context The context.
* @param stopId The stop ID.
* @return The filter. If there is no filter (or on error), it returns
* an empty list.
*/
public static ArrayList<String> get(Context context, String stopId) {
final String[] selection = {ROUTE_ID};
final String[] selectionArgs = {stopId};
ContentResolver cr = context.getContentResolver();
Cursor c = cr.query(CONTENT_URI, selection, FILTER_WHERE,
selectionArgs, null);
ArrayList<String> result = new ArrayList<String>();
if (c != null) {
try {
while (c.moveToNext()) {
result.add(c.getString(0));
}
} finally {
c.close();
}
}
return result;
}
/**
* Sets the filter for the particular stop ID.
*
* @param context The context.
* @param stopId The stop ID.
* @param filter An array of route IDs to filter.
*/
public static void set(Context context,
String stopId,
ArrayList<String> filter) {
if (context == null) {
return;
}
// First, delete any existing rows for this stop.
// Then, insert all of these rows.
// Should we put this in a transaction? We could,
// but it's not terribly important.
final String[] selectionArgs = {stopId};
ContentResolver cr = context.getContentResolver();
cr.delete(CONTENT_URI, FILTER_WHERE, selectionArgs);
ContentValues args = new ContentValues();
args.put(STOP_ID, stopId);
final int len = filter.size();
for (int i = 0; i < len; ++i) {
args.put(ROUTE_ID, filter.get(i));
cr.insert(CONTENT_URI, args);
}
}
}
public static class Trips implements BaseColumns, StopRouteKeyColumns,
TripsColumns {
// Cannot be instantiated
private Trips() {
}
/** The URI path portion for this table */
public static final String PATH = "trips";
/**
* The content:// style URI for this table URI is of the form
* content://<authority>/trips/<tripId>/<stopId>
*/
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".trip";
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".trip";
public static final int DAY_MON = 0x1;
public static final int DAY_TUE = 0x2;
public static final int DAY_WED = 0x4;
public static final int DAY_THU = 0x8;
public static final int DAY_FRI = 0x10;
public static final int DAY_SAT = 0x20;
public static final int DAY_SUN = 0x40;
public static final int DAY_WEEKDAY = DAY_MON | DAY_TUE | DAY_WED
| DAY_THU | DAY_FRI;
public static final int DAY_ALL = DAY_WEEKDAY | DAY_SUN | DAY_SAT;
public static Uri buildUri(String tripId, String stopId) {
return CONTENT_URI.buildUpon().appendPath(tripId)
.appendPath(stopId).build();
}
/**
* Converts a days bitmask into a boolean[] array
*
* @param days A DB compatible days bitmask.
* @return A boolean array representing the days set in the bitmask,
* Mon=0 to Sun=6
*/
public static boolean[] daysToArray(int days) {
final boolean[] result = {
(days & ObaContract.Trips.DAY_MON) == ObaContract.Trips.DAY_MON,
(days & ObaContract.Trips.DAY_TUE) == ObaContract.Trips.DAY_TUE,
(days & ObaContract.Trips.DAY_WED) == ObaContract.Trips.DAY_WED,
(days & ObaContract.Trips.DAY_THU) == ObaContract.Trips.DAY_THU,
(days & ObaContract.Trips.DAY_FRI) == ObaContract.Trips.DAY_FRI,
(days & ObaContract.Trips.DAY_SAT) == ObaContract.Trips.DAY_SAT,
(days & ObaContract.Trips.DAY_SUN) == ObaContract.Trips.DAY_SUN,};
return result;
}
/**
* Converts a boolean[] array to a DB compatible days bitmask
*
* @param days boolean array as returned by daysToArray
* @return A DB compatible days bitmask
*/
public static int arrayToDays(boolean[] days) {
if (days.length != 7) {
throw new IllegalArgumentException("days.length must be 7");
}
int result = 0;
for (int i = 0; i < days.length; ++i) {
final int bit = days[i] ? 1 : 0;
result |= bit << i;
}
return result;
}
/**
* Converts a 'minutes-to-midnight' value into a Unix time.
*
* @param minutes from midnight in UTC.
* @return A Unix time representing the time in the current day.
*/
// Helper functions to convert the DB DepartureTime value
public static long convertDBToTime(int minutes) {
// This converts the minutes-to-midnight to a time of the current
// day.
Time t = new Time();
t.setToNow();
t.set(0, minutes, 0, t.monthDay, t.month, t.year);
return t.toMillis(false);
}
/**
* Converts a Unix time into a 'minutes-to-midnight' in UTC.
*
* @param departureTime A Unix time.
* @return minutes from midnight in UTC.
*/
public static int convertTimeToDB(long departureTime) {
// This converts a time_t to minutes-to-midnight.
Time t = new Time();
t.set(departureTime);
return t.hour * 60 + t.minute;
}
/**
* Converts a weekday value from a android.text.format.Time to a bit.
*
* @param weekday The weekDay value from android.text.format.Time
* @return A DB compatible bit.
*/
public static int getDayBit(int weekday) {
switch (weekday) {
case Time.MONDAY:
return ObaContract.Trips.DAY_MON;
case Time.TUESDAY:
return ObaContract.Trips.DAY_TUE;
case Time.WEDNESDAY:
return ObaContract.Trips.DAY_WED;
case Time.THURSDAY:
return ObaContract.Trips.DAY_THU;
case Time.FRIDAY:
return ObaContract.Trips.DAY_FRI;
case Time.SATURDAY:
return ObaContract.Trips.DAY_SAT;
case Time.SUNDAY:
return ObaContract.Trips.DAY_SUN;
}
return 0;
}
}
public static class TripAlerts implements BaseColumns, TripAlertsColumns {
// Cannot be instantiated
private TripAlerts() {
}
/** The URI path portion for this table */
public static final String PATH = "trip_alerts";
/**
* The content:// style URI for this table URI is of the form
* content://<authority>/trip_alerts/<id>
*/
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".trip_alert";
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".trip_alert";
public static final int STATE_SCHEDULED = 0;
public static final int STATE_POLLING = 1;
public static final int STATE_NOTIFY = 2;
public static final int STATE_CANCELLED = 3;
public static Uri buildUri(int id) {
return CONTENT_URI.buildUpon().appendPath(String.valueOf(id))
.build();
}
public static Uri insertIfNotExists(Context context,
String tripId,
String stopId,
long startTime) {
return insertIfNotExists(context.getContentResolver(), tripId,
stopId, startTime);
}
public static Uri insertIfNotExists(ContentResolver cr,
String tripId,
String stopId,
long startTime) {
Uri result;
Cursor c = cr.query(CONTENT_URI,
new String[]{_ID},
String.format("%s=? AND %s=? AND %s=?",
TRIP_ID, STOP_ID, START_TIME),
new String[]{tripId, stopId, String.valueOf(startTime)},
null);
if (c != null && c.moveToNext()) {
result = buildUri(c.getInt(0));
} else {
ContentValues values = new ContentValues();
values.put(TRIP_ID, tripId);
values.put(STOP_ID, stopId);
values.put(START_TIME, startTime);
result = cr.insert(CONTENT_URI, values);
}
if (c != null) {
c.close();
}
return result;
}
public static void setState(Context context, Uri uri, int state) {
setState(context.getContentResolver(), uri, state);
}
public static void setState(ContentResolver cr, Uri uri, int state) {
ContentValues values = new ContentValues();
values.put(STATE, state);
cr.update(uri, values, null, null);
}
}
public static class ServiceAlerts implements BaseColumns, ServiceAlertsColumns {
// Cannot be instantiated
private ServiceAlerts() {
}
/** The URI path portion for this table */
public static final String PATH = "service_alerts";
/**
* The content:// style URI for this table URI is of the form
* content://<authority>/service_alerts/<id>
*/
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.item/" + BuildConfig.DATABASE_AUTHORITY + ".service_alert";
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".service_alert";
/**
* @param markAsRead true if this alert should be marked as read with the timestamp of
* System.currentTimeMillis(),
* false if the alert should not be marked as read with the timestamp of
* System.currentTimeMillis()
* @param hidden true if this alert should be marked as hidden by the user, false if
* it should be marked as not
* hidden by the user, or null if the hidden value shouldn't be
* changed
*/
public static Uri insertOrUpdate(String id,
ContentValues values,
boolean markAsRead,
Boolean hidden) {
if (id == null) {
return null;
}
if (values == null) {
values = new ContentValues();
}
ContentResolver cr = Application.get().getContentResolver();
final Uri uri = Uri.withAppendedPath(CONTENT_URI, id);
Cursor c = cr.query(uri, new String[]{}, null, null, null);
Uri result;
if (c != null && c.getCount() > 0) {
// Update
if (markAsRead) {
c.moveToFirst();
values.put(MARKED_READ_TIME, System.currentTimeMillis());
}
if (hidden != null) {
c.moveToFirst();
if (hidden) {
values.put(HIDDEN, 1);
} else {
values.put(HIDDEN, 0);
}
}
if (values.size() != 0) {
cr.update(uri, values, null, null);
}
result = uri;
} else {
// Insert
if (markAsRead) {
values.put(MARKED_READ_TIME, System.currentTimeMillis());
}
if (hidden != null) {
if (hidden) {
values.put(HIDDEN, 1);
} else {
values.put(HIDDEN, 0);
}
}
values.put(_ID, id);
result = cr.insert(CONTENT_URI, values);
}
if (c != null) {
c.close();
}
return result;
}
/**
* Returns true if this service alert (situation) has been previously hidden by the
* user, false it if has not
*
* @param situationId The ID of the situation (service alert)
* @return true if this service alert (situation) has been previously hidden by the user,
* false it if has not
*/
public static boolean isHidden(String situationId) {
final String[] selection = {_ID, HIDDEN};
final String[] selectionArgs = {situationId, Integer.toString(1)};
final String WHERE = _ID + "=? AND " + HIDDEN + "=?";
ContentResolver cr = Application.get().getContentResolver();
Cursor c = cr.query(CONTENT_URI, selection, WHERE, selectionArgs, null);
boolean hidden;
if (c != null && c.getCount() > 0) {
hidden = true;
} else {
hidden = false;
}
if (c != null) {
c.close();
}
return hidden;
}
/**
* Marks all alerts as not hidden, and therefore visible
*
* @return the number of rows updated
*/
public static int showAllAlerts() {
ContentResolver cr = Application.get().getContentResolver();
ContentValues values = new ContentValues();
values.put(HIDDEN, 0);
return cr.update(CONTENT_URI, values, null, null);
}
}
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://<authority>/regions/<id>
*/
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 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,
OBA_BASE_URL,
SIRI_BASE_URL,
LANGUAGE,
CONTACT_EMAIL,
SUPPORTS_OBA_DISCOVERY,
SUPPORTS_OBA_REALTIME,
SUPPORTS_SIRI_REALTIME,
TWITTER_URL,
EXPERIMENTAL,
STOP_INFO_URL,
OTP_BASE_URL,
OTP_CONTACT_EMAIL
};
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
c.getString(3), // SIRI Base URL
RegionBounds.getRegion(cr, id), // Bounds
RegionOpen311Servers.getOpen311Server(cr, id), // Open311 servers
c.getString(4), // Lang
c.getString(5), // Contact Email
c.getInt(6) > 0, // Supports Oba Discovery
c.getInt(7) > 0, // Supports Oba Realtime
c.getInt(8) > 0, // Supports Siri Realtime
c.getString(9), // Twitter URL
c.getInt(10) > 0, // Experimental
c.getString(11), // StopInfoUrl
c.getString(12), // OtpBaseUrl
c.getString(13) // OtpContactEmail
);
} 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://<authority>/region_bounds/<id>
*/
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 Uri buildUri(int id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
public static ObaRegionElement.Bounds[] getRegion(ContentResolver cr, int regionId) {
final String[] PROJECTION = {
LATITUDE,
LONGITUDE,
LAT_SPAN,
LON_SPAN
};
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://<authority>/region_open311_servers/<id>
*/
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 Uri buildUri(int id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
public static ObaRegionElement.Open311Server[] 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.Open311Server[] results = new ObaRegionElement.Open311Server[c.getCount()];
if (c.getCount() == 0) {
return results;
}
int i = 0;
c.moveToFirst();
do {
results[i] = new ObaRegionElement.Open311Server(
c.getString(0),
c.getString(1),
c.getString(2));
i++;
} while (c.moveToNext());
return results;
} finally {
c.close();
}
}
return null;
}
}
/**
* Supports storing user-defined favorites for route/headsign/stop combinations. This is
* currently implemented without requiring knowledge of a full set of stops for a route. This
* allows some flexibility in terms of changes server-side without invalidating user's
* favorites - as long as the routeId/headsign combination remains consistent (and stopId,
* when a particular stop is referenced), then user favorites should survive changes in the
* composition of stops for a route.
*
* When the user favorites a route/headsign combination in the ArrivalsListFragment/Header,
* they are prompted if they would like to make it a favorite for the current stop, or for all
* stops. If they make it a favorite for the current stop, a record with
* routeId/headsign/stopId is created, with "exclude" value of false (0). If they make it a
* favorite for all stops, a record with routeId/headsign/ALL_STOPS is created with exclude
* value of false. When arrival times are displayed for a given stopId, if a record in the
* database with routeId/headsign/ALL_STOPS or routeId?headsign/stopId matches AND exclude is
* set to false, then it is shown as a favorite. Otherwise, it is not shown as a favorite.
* If the user unstars a stop, then routeId/headsign/stopId is inserted with an exclude value of
* true (1).S
*/
public static class RouteHeadsignFavorites implements RouteHeadsignKeyColumns, UserColumns {
// Cannot be instantiated
private RouteHeadsignFavorites() {
}
/** The URI path portion for this table */
public static final String PATH = "route_headsign_favorites";
/** The content:// style URI for this table */
public static final Uri CONTENT_URI = Uri.withAppendedPath(
AUTHORITY_URI, PATH);
public static final String CONTENT_DIR_TYPE
= "vnd.android.dir/" + BuildConfig.DATABASE_AUTHORITY + ".routeheadsignfavorites";
// String used to indicate that a route/headsign combination is a favorite for all stops
private static final String ALL_STOPS = "all";
/**
* Set the specified route and headsign combination as a favorite, optionally for a specific
* stop. Note that this will also handle the marking/unmarking of the designated route as
* the favorite as well. The route is marked as not a favorite when no more
* routeId/headsign combinations remain. If marking the route/headsign as favorite for
* all stops, then stopId should be null.
*
* @param routeId routeId to be marked as favorite, in combination with headsign
* @param headsign headsign to be marked as favorite, in combination with routeId
* @param stopId stopId to be marked as a favorite, or null if all stopIds should be marked
* for this routeId/headsign combo.
* @param favorite true if this route and headsign combination should be marked as a
* favorite, false if it should not
*/
public static void markAsFavorite(Context context, String routeId, String headsign, String
stopId, boolean favorite) {
if (context == null) {
return;
}
if (headsign == null) {
headsign = "";
}
ContentResolver cr = context.getContentResolver();
Uri routeUri = Uri.withAppendedPath(ObaContract.Routes.CONTENT_URI, routeId);
String stopIdInternal;
if (stopId != null) {
stopIdInternal = stopId;
} else {
stopIdInternal = ALL_STOPS;
}
final String WHERE = ROUTE_ID + "=? AND " + HEADSIGN + "=? AND " + STOP_ID + "=?";
final String[] selectionArgs = {routeId, headsign, stopIdInternal};
if (favorite) {
if (stopIdInternal != ALL_STOPS) {
// First, delete any potential exclusion records for this stop by removing all records
cr.delete(CONTENT_URI, WHERE, selectionArgs);
}
// Mark as favorite by inserting a record for this route/headsign combo
ContentValues values = new ContentValues();
values.put(ROUTE_ID, routeId);
values.put(HEADSIGN, headsign);
values.put(STOP_ID, stopIdInternal);
values.put(EXCLUDE, 0);
cr.insert(CONTENT_URI, values);
// Mark the route as a favorite also in the routes table
Routes.markAsFavorite(context, routeUri, true);
} else {
// Deselect it as favorite by deleting all records for this route/headsign/stopId combo
cr.delete(CONTENT_URI, WHERE, selectionArgs);
if (stopIdInternal == ALL_STOPS) {
// Also make sure we've deleted the single record for this specific stop, if it exists
// We don't have the stopId here, so we can just delete all records for this routeId/headsign
final String[] selectionArgs2 = {routeId, headsign};
final String WHERE2 = ROUTE_ID + "=? AND " + HEADSIGN + "=?";
cr.delete(CONTENT_URI, WHERE2, selectionArgs2);
}
// If there are no more route/headsign combinations that are favorites for this route,
// then mark the route as not a favorite
if (!isFavorite(context, routeId)) {
Routes.markAsFavorite(context, routeUri, false);
}
// If a single stop is unstarred, but isFavorite(...) == true due to starring all
// stops, insert exclusion record
if (stopIdInternal != ALL_STOPS && isFavorite(routeId, headsign, stopId)) {
// Insert an exclusion record for this single stop, in case the user is unstarring it
// after starring the entire route
ContentValues values = new ContentValues();
values.put(ROUTE_ID, routeId);
values.put(HEADSIGN, headsign);
values.put(STOP_ID, stopIdInternal);
values.put(EXCLUDE, 1);
cr.insert(CONTENT_URI, values);
}
}
StringBuilder analyicsLabel = new StringBuilder();
if (favorite) {
analyicsLabel.append(context.getString(R.string.analytics_label_star_route));
} else {
analyicsLabel.append(context.getString(R.string.analytics_label_unstar_route));
}
analyicsLabel.append(" ").append(routeId).append("_").append(headsign).append(" for ");
if (stopId != null) {
analyicsLabel.append(stopId);
} else {
analyicsLabel.append("all stops");
}
ObaAnalytics.reportEventWithCategory(
ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
context.getString(R.string.analytics_action_edit_field),
analyicsLabel.toString());
}
/**
* Returns true if this combination of routeId and headsign is a favorite for this stop
* or all stops (and that stop is not excluded as a favorite), false if it is not
*
* @param routeId The routeId to check for favorite
* @param headsign The headsign to check for favorite
* @param stopId The stopId to check for favorite
* @return true if this combination of routeId and headsign is a favorite for this stop
* or all stops (and that stop is not excluded as a favorite), false if it is not
*/
public static boolean isFavorite(String routeId, String headsign,
String stopId) {
if (headsign == null) {
headsign = "";
}
final String[] selection = {ROUTE_ID, HEADSIGN, STOP_ID, EXCLUDE};
final String[] selectionArgs = {routeId, headsign, stopId, Integer.toString(0)};
ContentResolver cr = Application.get().getContentResolver();
final String FILTER_WHERE_ALL_FIELDS = ROUTE_ID + "=? AND " + HEADSIGN + "=? AND "
+ STOP_ID + "=? AND " + EXCLUDE + "=?";
Cursor c = cr.query(CONTENT_URI, selection, FILTER_WHERE_ALL_FIELDS,
selectionArgs, null);
boolean favorite;
if (c != null && c.getCount() > 0) {
favorite = true;
} else {
// Check again to see if the user has favorited this route/headsign combo for all stops
final String[] selectionArgs2 = {routeId, headsign, ALL_STOPS};
String WHERE_PARTIAL = ROUTE_ID + "=? AND " + HEADSIGN + "=? AND "
+ STOP_ID + "=?";
Cursor c2 = cr.query(CONTENT_URI, selection, WHERE_PARTIAL,
selectionArgs2, null);
favorite = c2 != null && c2.getCount() > 0;
if (c2 != null) {
c2.close();
}
if (favorite) {
// Finally, make sure the user hasn't excluded this stop as a favorite
final String[] selectionArgs3 = {routeId, headsign, stopId,
Integer.toString(1)};
Cursor c3 = cr.query(CONTENT_URI, selection, FILTER_WHERE_ALL_FIELDS,
selectionArgs3, null);
// If this query returns at least one record, it means the stop has been excluded as
// a favorite (i.e., the user explicitly de-selected it)
boolean isStopExcluded = c3 != null && c3.getCount() > 0;
favorite = !isStopExcluded;
if (c3 != null) {
c3.close();
}
}
}
if (c != null) {
c.close();
}
return favorite;
}
/**
* Returns true if this routeId is listed as a favorite for at least one headsign with
* EXCLUDED set to false, or false if it is not
*
* @param routeId The routeId to check for favorite
* @return true if this routeId is listed as a favorite for at least one headsign without
* EXCLUDE being set to true, or false if it is not
*/
private static boolean isFavorite(Context context, String routeId) {
final String[] selection = {ROUTE_ID, EXCLUDE};
final String[] selectionArgs = {routeId, Integer.toString(0)};
final String WHERE = ROUTE_ID + "=? AND " + EXCLUDE + "=?";
ContentResolver cr = context.getContentResolver();
Cursor c = cr.query(CONTENT_URI, selection, WHERE,
selectionArgs, null);
boolean favorite = false;
if (c != null && c.getCount() > 0) {
favorite = true;
} else {
favorite = false;
}
if (c != null) {
c.close();
}
return favorite;
}
}
}