From: Neil Smith Date: Sun, 9 Nov 2014 20:58:42 +0000 (+0000) Subject: Done with bulk imports X-Git-Url: https://git.njae.me.uk/?a=commitdiff_plain;h=10265f8b4d2e1cb0f74f50ac7700aacad6bd2255;p=Sunshine.git Done with bulk imports --- diff --git a/app/src/androidTest/java/uk/me/njae/sunshine/TestDb.java b/app/src/androidTest/java/uk/me/njae/sunshine/TestDb.java index 73353e8..86663d3 100644 --- a/app/src/androidTest/java/uk/me/njae/sunshine/TestDb.java +++ b/app/src/androidTest/java/uk/me/njae/sunshine/TestDb.java @@ -17,6 +17,9 @@ public class TestDb extends AndroidTestCase { public static final String LOG_TAG = TestDb.class.getSimpleName(); + static final String TEST_LOCATION = "99705"; + static final String TEST_DATE = "20141205"; + public void testCreateDb() throws Throwable { mContext.deleteDatabase(WeatherDbHelper.DATABASE_NAME); SQLiteDatabase db = new WeatherDbHelper( @@ -27,12 +30,6 @@ public class TestDb extends AndroidTestCase { public void testInsertReadDb() { - // Test data we're going to insert into the DB to see if it works. - String testLocationSetting = "99705"; - String testCityName = "North Pole"; - double testLatitude = 64.7488; - double testLongitude = -147.353; - // If there's an error in those massive SQL table creation Strings, // errors will be thrown here when you try to get a writable database. WeatherDbHelper dbHelper = new WeatherDbHelper(mContext); @@ -62,7 +59,7 @@ public class TestDb extends AndroidTestCase { null // sort order ); - validateCursor(cursor, locationTestValues); + validateCursor(cursor, locationTestValues); // Fantastic. Now that we have a location, add some weather! diff --git a/app/src/androidTest/java/uk/me/njae/sunshine/TestProvider.java b/app/src/androidTest/java/uk/me/njae/sunshine/TestProvider.java new file mode 100644 index 0000000..508aab3 --- /dev/null +++ b/app/src/androidTest/java/uk/me/njae/sunshine/TestProvider.java @@ -0,0 +1,239 @@ +package uk.me.njae.sunshine; + +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.test.AndroidTestCase; +import android.util.Log; + +import uk.me.njae.sunshine.data.WeatherContract.LocationEntry; +import uk.me.njae.sunshine.data.WeatherContract.WeatherEntry; +import uk.me.njae.sunshine.data.WeatherDbHelper; + +public class TestProvider extends AndroidTestCase { + + public static final String LOG_TAG = TestProvider.class.getSimpleName(); + + // brings our database to an empty state + public void deleteAllRecords() { + mContext.getContentResolver().delete( + WeatherEntry.CONTENT_URI, + null, + null + ); + mContext.getContentResolver().delete( + LocationEntry.CONTENT_URI, + null, + null + ); + + Cursor cursor = mContext.getContentResolver().query( + WeatherEntry.CONTENT_URI, + null, + null, + null, + null + ); + assertEquals(0, cursor.getCount()); + cursor.close(); + + cursor = mContext.getContentResolver().query( + LocationEntry.CONTENT_URI, + null, + null, + null, + null + ); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + // Since we want each test to start with a clean slate, run deleteAllRecords + // in setUp (called by the test runner before each test). + public void setUp() { + deleteAllRecords(); + } + + public void testInsertReadProvider() { + + // If there's an error in those massive SQL table creation Strings, + // errors will be thrown here when you try to get a writable database. + WeatherDbHelper dbHelper = new WeatherDbHelper(mContext); + // SQLiteDatabase db = dbHelper.getWritableDatabase(); + + // Create a new map of values, where column names are the keys + ContentValues locationTestValues = TestDb.createNorthPoleLocationValues(); + + Uri locationInsertUri = mContext.getContentResolver().insert(LocationEntry.CONTENT_URI, locationTestValues); + assertTrue(locationInsertUri != null); + long locationRowId = ContentUris.parseId(locationInsertUri); + + Log.d(LOG_TAG, "New row id: " + locationRowId); + + // Data's inserted. IN THEORY. Now pull some out to stare at it and verify it made + // the round trip. + + // A cursor is your primary interface to the query results. + Cursor cursor = mContext.getContentResolver().query( + LocationEntry.CONTENT_URI, // Table to Query + null, // All columns + null, // Columns for the "where" clause + null, // Values for the "where" clause + null // columns to group by + ); + + TestDb.validateCursor(cursor, locationTestValues); + + // Now see if we can successfully query if we include the row id + cursor = mContext.getContentResolver().query( + LocationEntry.buildLocationUri(locationRowId), + null, // leaving "columns" null just returns all the columns. + null, // cols for "where" clause + null, // values for "where" clause + null // sort order + ); + + TestDb.validateCursor(cursor, locationTestValues); + + + // Fantastic. Now that we have a location, add some weather! + + ContentValues weatherTestValues = TestDb.createWeatherValues(locationRowId); + + Uri weatherInsertUri = mContext.getContentResolver().insert(WeatherEntry.CONTENT_URI, weatherTestValues); + assertTrue(weatherInsertUri != null); + + // A cursor is your primary interface to the query results. + Cursor weatherCursor = mContext.getContentResolver().query( + WeatherEntry.CONTENT_URI, // Table to Query + null, // leaving "columns" null just returns all the columns. + null, // cols for "where" clause + null, // values for "where" clause + null // columns to group by + ); + + TestDb.validateCursor(weatherCursor, weatherTestValues); + + + // Add the location values in with the weather data so that we can make + // sure that the join worked and we actually get all the values back + addAllContentValues(weatherTestValues, locationTestValues); + + // Get the joined Weather and Location data + weatherCursor = mContext.getContentResolver().query( + WeatherEntry.buildWeatherLocation(TestDb.TEST_LOCATION), + null, // leaving "columns" null just returns all the columns. + null, // cols for "where" clause + null, // values for "where" clause + null // sort order + ); + TestDb.validateCursor(weatherCursor, weatherTestValues); + + // Get the joined Weather and Location data with a start date + weatherCursor = mContext.getContentResolver().query( + WeatherEntry.buildWeatherLocationWithStartDate( + TestDb.TEST_LOCATION, TestDb.TEST_DATE), + null, // leaving "columns" null just returns all the columns. + null, // cols for "where" clause + null, // values for "where" clause + null // sort order + ); + TestDb.validateCursor(weatherCursor, weatherTestValues); + + // Get the joined Weather data for a specific date + weatherCursor = mContext.getContentResolver().query( + WeatherEntry.buildWeatherLocationWithDate(TestDb.TEST_LOCATION, TestDb.TEST_DATE), + null, + null, + null, + null + ); + TestDb.validateCursor(weatherCursor, weatherTestValues); + + dbHelper.close(); + } + + public void testGetType() { + // content://uk.me.njae.sunshine/weather/ + String type = mContext.getContentResolver().getType(WeatherEntry.CONTENT_URI); + // vnd.android.cursor.dir/uk.me.njae.sunshine/weather + assertEquals(WeatherEntry.CONTENT_TYPE, type); + + String testLocation = "94074"; + // content://uk.me.njae.sunshine/weather/94074 + type = mContext.getContentResolver().getType( + WeatherEntry.buildWeatherLocation(testLocation)); + // vnd.android.cursor.dir/uk.me.njae.sunshine/weather + assertEquals(WeatherEntry.CONTENT_TYPE, type); + + String testDate = "20140612"; + // content://uk.me.njae.sunshine/weather/94074/20140612 + type = mContext.getContentResolver().getType( + WeatherEntry.buildWeatherLocationWithDate(testLocation, testDate)); + // vnd.android.cursor.item/uk.me.njae.sunshine/weather + assertEquals(WeatherEntry.CONTENT_ITEM_TYPE, type); + + // content://uk.me.njae.sunshine/location/ + type = mContext.getContentResolver().getType(LocationEntry.CONTENT_URI); + // vnd.android.cursor.dir/uk.me.njae.sunshine/location + assertEquals(LocationEntry.CONTENT_TYPE, type); + + // content://uk.me.njae.sunshine/location/1 + type = mContext.getContentResolver().getType(LocationEntry.buildLocationUri(1L)); + // vnd.android.cursor.item/uk.me.njae.sunshine/location + assertEquals(LocationEntry.CONTENT_ITEM_TYPE, type); + } + + public void testUpdateLocation() { + // Create a new map of values, where column names are the keys + ContentValues values = TestDb.createNorthPoleLocationValues(); + + Uri locationUri = mContext.getContentResolver(). + insert(LocationEntry.CONTENT_URI, values); + long locationRowId = ContentUris.parseId(locationUri); + + // Verify we got a row back. + assertTrue(locationRowId != -1); + Log.d(LOG_TAG, "New row id: " + locationRowId); + + ContentValues updatedValues = new ContentValues(values); + updatedValues.put(LocationEntry._ID, locationRowId); + updatedValues.put(LocationEntry.COLUMN_CITY_NAME, "Santa's Village"); + + int count = mContext.getContentResolver().update( + LocationEntry.CONTENT_URI, updatedValues, LocationEntry._ID + "= ?", + new String[] { Long.toString(locationRowId)}); + + assertEquals(count, 1); + + // A cursor is your primary interface to the query results. + Cursor cursor = mContext.getContentResolver().query( + LocationEntry.buildLocationUri(locationRowId), + null, + null, // Columns for the "where" clause + null, // Values for the "where" clause + null // sort order + ); + + TestDb.validateCursor(cursor, updatedValues); + } + + // Make sure we can still delete after adding/updating stuff + public void testDeleteRecordsAtEnd() { + deleteAllRecords(); + } + + + + // The target api annotation is needed for the call to keySet -- we wouldn't want + // to use this in our app, but in a test it's fine to assume a higher target. + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + void addAllContentValues(ContentValues destination, ContentValues source) { + for (String key : source.keySet()) { + destination.put(key, source.getAsString(key)); + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3958f30..b6b4a4e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,9 @@ android:name="android:support.PARENT_ACTIVITY" android:value="MainActivity" /> + diff --git a/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java b/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java new file mode 100644 index 0000000..44506c6 --- /dev/null +++ b/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java @@ -0,0 +1,390 @@ +package uk.me.njae.sunshine; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.ArrayAdapter; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Vector; + +import uk.me.njae.sunshine.data.WeatherContract; +import uk.me.njae.sunshine.data.WeatherContract.LocationEntry; +import uk.me.njae.sunshine.data.WeatherContract.WeatherEntry; + +/** + * Created by neil on 09/11/14. + */ + + +public class FetchWeatherTask extends AsyncTask { + + private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); + + private ArrayAdapter mForecastAdapter; + private final Context mContext; + + public FetchWeatherTask(Context context, ArrayAdapter forecastAdapter) { + mContext = context; + mForecastAdapter = forecastAdapter; + } + + private boolean DEBUG = true; + + /* The date/time conversion code is going to be moved outside the asynctask later, + * so for convenience we're breaking it out into its own method now. + */ + private String getReadableDateString(long time) { + // Because the API returns a unix timestamp (measured in seconds), + // it must be converted to milliseconds in order to be converted to valid date. + Date date = new Date(time * 1000); + SimpleDateFormat format = new SimpleDateFormat("E, MMM d"); + return format.format(date).toString(); + } + + /** + * Prepare the weather high/lows for presentation. + */ + private String formatHighLows(double high, double low) { + // Data is fetched in Celsius by default. + // If user prefers to see in Fahrenheit, convert the values here. + // We do this rather than fetching in Fahrenheit so that the user can + // change this option without us having to re-fetch the data once + // we start storing the values in a database. + SharedPreferences sharedPrefs = + PreferenceManager.getDefaultSharedPreferences(mContext); + String unitType = sharedPrefs.getString( + mContext.getString(R.string.pref_units_key), + mContext.getString(R.string.pref_units_metric)); + + if (unitType.equals(mContext.getString(R.string.pref_units_imperial))) { + high = (high * 1.8) + 32; + low = (low * 1.8) + 32; + } else if (!unitType.equals(mContext.getString(R.string.pref_units_metric))) { + Log.d(LOG_TAG, "Unit type not found: " + unitType); + } + + // For presentation, assume the user doesn't care about tenths of a degree. + long roundedHigh = Math.round(high); + long roundedLow = Math.round(low); + + String highLowStr = roundedHigh + "/" + roundedLow; + return highLowStr; + } + + /** + * Helper method to handle insertion of a new location in the weather database. + * + * @param locationSetting The location string used to request updates from the server. + * @param cityName A human-readable city name, e.g "Mountain View" + * @param lat the latitude of the city + * @param lon the longitude of the city + * @return the row ID of the added location. + */ + private long addLocation(String locationSetting, String cityName, double lat, double lon) { + + Log.v(LOG_TAG, "inserting " + cityName + ", with coord: " + lat + ", " + lon); + + // First, check if the location with this city name exists in the db + Cursor cursor = mContext.getContentResolver().query( + LocationEntry.CONTENT_URI, + new String[]{LocationEntry._ID}, + LocationEntry.COLUMN_LOCATION_SETTING + " = ?", + new String[]{locationSetting}, + null); + + if (cursor.moveToFirst()) { + Log.v(LOG_TAG, "Found it in the database!"); + int locationIdIndex = cursor.getColumnIndex(LocationEntry._ID); + return cursor.getLong(locationIdIndex); + } else { + Log.v(LOG_TAG, "Didn't find it in the database, inserting now!"); + ContentValues locationValues = new ContentValues(); + locationValues.put(LocationEntry.COLUMN_LOCATION_SETTING, locationSetting); + locationValues.put(LocationEntry.COLUMN_CITY_NAME, cityName); + locationValues.put(LocationEntry.COLUMN_COORD_LAT, lat); + locationValues.put(LocationEntry.COLUMN_COORD_LONG, lon); + + Uri locationInsertUri = mContext.getContentResolver() + .insert(LocationEntry.CONTENT_URI, locationValues); + + return ContentUris.parseId(locationInsertUri); + } + } + + /** + * Take the String representing the complete forecast in JSON Format and + * pull out the data we need to construct the Strings needed for the wireframes. + *

+ * Fortunately parsing is easy: constructor takes the JSON string and converts it + * into an Object hierarchy for us. + */ + private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays, + String locationSetting) + throws JSONException { + + // These are the names of the JSON objects that need to be extracted. + + // Location information + final String OWM_CITY = "city"; + final String OWM_CITY_NAME = "name"; + final String OWM_COORD = "coord"; + final String OWM_COORD_LAT = "lat"; + final String OWM_COORD_LONG = "lon"; + + // Weather information. Each day's forecast info is an element of the "list" array. + final String OWM_LIST = "list"; + + final String OWM_DATETIME = "dt"; + final String OWM_PRESSURE = "pressure"; + final String OWM_HUMIDITY = "humidity"; + final String OWM_WINDSPEED = "speed"; + final String OWM_WIND_DIRECTION = "deg"; + + // All temperatures are children of the "temp" object. + final String OWM_TEMPERATURE = "temp"; + final String OWM_MAX = "max"; + final String OWM_MIN = "min"; + + final String OWM_WEATHER = "weather"; + final String OWM_DESCRIPTION = "main"; + final String OWM_WEATHER_ID = "id"; + + JSONObject forecastJson = new JSONObject(forecastJsonStr); + JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST); + + JSONObject cityJson = forecastJson.getJSONObject(OWM_CITY); + String cityName = cityJson.getString(OWM_CITY_NAME); + JSONObject coordJSON = cityJson.getJSONObject(OWM_COORD); + double cityLatitude = coordJSON.getLong(OWM_COORD_LAT); + double cityLongitude = coordJSON.getLong(OWM_COORD_LONG); + + Log.v(LOG_TAG, cityName + ", with coord: " + cityLatitude + " " + cityLongitude); + + // Insert the location into the database. + long locationID = addLocation(locationSetting, cityName, cityLatitude, cityLongitude); + + // Get and insert the new weather information into the database + Vector cVVector = new Vector(weatherArray.length()); + + String[] resultStrs = new String[numDays]; + for (int i = 0; i < weatherArray.length(); i++) { + // These are the values that will be collected. + + long dateTime; + double pressure; + int humidity; + double windSpeed; + double windDirection; + + double high; + double low; + + String description; + int weatherId; + + // Get the JSON object representing the day + JSONObject dayForecast = weatherArray.getJSONObject(i); + + // The date/time is returned as a long. We need to convert that + // into something human-readable, since most people won't read "1400356800" as + // "this saturday". + dateTime = dayForecast.getLong(OWM_DATETIME); + + pressure = dayForecast.getDouble(OWM_PRESSURE); + humidity = dayForecast.getInt(OWM_HUMIDITY); + windSpeed = dayForecast.getDouble(OWM_WINDSPEED); + windDirection = dayForecast.getDouble(OWM_WIND_DIRECTION); + + // Description is in a child array called "weather", which is 1 element long. + // That element also contains a weather code. + JSONObject weatherObject = + dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0); + description = weatherObject.getString(OWM_DESCRIPTION); + weatherId = weatherObject.getInt(OWM_WEATHER_ID); + + // Temperatures are in a child object called "temp". Try not to name variables + // "temp" when working with temperature. It confuses everybody. + JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE); + high = temperatureObject.getDouble(OWM_MAX); + low = temperatureObject.getDouble(OWM_MIN); + + ContentValues weatherValues = new ContentValues(); + + weatherValues.put(WeatherEntry.COLUMN_LOC_KEY, locationID); + weatherValues.put(WeatherEntry.COLUMN_DATETEXT, + WeatherContract.getDbDateString(new Date(dateTime * 1000L))); + weatherValues.put(WeatherEntry.COLUMN_HUMIDITY, humidity); + weatherValues.put(WeatherEntry.COLUMN_PRESSURE, pressure); + weatherValues.put(WeatherEntry.COLUMN_WIND_SPEED, windSpeed); + weatherValues.put(WeatherEntry.COLUMN_DEGREES, windDirection); + weatherValues.put(WeatherEntry.COLUMN_MAX_TEMP, high); + weatherValues.put(WeatherEntry.COLUMN_MIN_TEMP, low); + weatherValues.put(WeatherEntry.COLUMN_SHORT_DESC, description); + weatherValues.put(WeatherEntry.COLUMN_WEATHER_ID, weatherId); + + cVVector.add(weatherValues); + + String highAndLow = formatHighLows(high, low); + String day = getReadableDateString(dateTime); + resultStrs[i] = day + " - " + description + " - " + highAndLow; + } + if (cVVector.size() > 0) { + ContentValues[] cvArray = new ContentValues[cVVector.size()]; + cVVector.toArray(cvArray); + int rowsInserted = mContext.getContentResolver() + .bulkInsert(WeatherEntry.CONTENT_URI, cvArray); + Log.v(LOG_TAG, "inserted " + rowsInserted + " rows of weather data"); + // Use a DEBUG variable to gate whether or not you do this, so you can easily + // turn it on and off, and so that it's easy to see what you can rip out if + // you ever want to remove it. + if (DEBUG) { + Cursor weatherCursor = mContext.getContentResolver().query( + WeatherEntry.CONTENT_URI, + null, + null, + null, + null + ); + + if (weatherCursor.moveToFirst()) { + ContentValues resultValues = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(weatherCursor, resultValues); + Log.v(LOG_TAG, "Query succeeded! **********"); + for (String key : resultValues.keySet()) { + Log.v(LOG_TAG, key + ": " + resultValues.getAsString(key)); + } + } else { + Log.v(LOG_TAG, "Query failed! :( **********"); + } + } + } + return resultStrs; + } + + @Override + protected String[] doInBackground(String... params) { + + // If there's no zip code, there's nothing to look up. Verify size of params. + if (params.length == 0) { + return null; + } + String locationQuery = params[0]; + + // These two need to be declared outside the try/catch + // so that they can be closed in the finally block. + HttpURLConnection urlConnection = null; + BufferedReader reader = null; + + // Will contain the raw JSON response as a string. + String forecastJsonStr = null; + + String format = "json"; + String units = "metric"; + int numDays = 14; + + try { + // Construct the URL for the OpenWeatherMap query + // Possible parameters are avaiable at OWM's forecast API page, at + // http://openweathermap.org/API#forecast + final String FORECAST_BASE_URL = + "http://api.openweathermap.org/data/2.5/forecast/daily?"; + final String QUERY_PARAM = "q"; + final String FORMAT_PARAM = "mode"; + final String UNITS_PARAM = "units"; + final String DAYS_PARAM = "cnt"; + + Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon() + .appendQueryParameter(QUERY_PARAM, params[0]) + .appendQueryParameter(FORMAT_PARAM, format) + .appendQueryParameter(UNITS_PARAM, units) + .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays)) + .build(); + + URL url = new URL(builtUri.toString()); + + // Create the request to OpenWeatherMap, and open the connection + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.connect(); + + // Read the input stream into a String + InputStream inputStream = urlConnection.getInputStream(); + StringBuffer buffer = new StringBuffer(); + if (inputStream == null) { + // Nothing to do. + return null; + } + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String line; + while ((line = reader.readLine()) != null) { + // Since it's JSON, adding a newline isn't necessary (it won't affect parsing) + // But it does make debugging a *lot* easier if you print out the completed + // buffer for debugging. + buffer.append(line + "\n"); + } + + if (buffer.length() == 0) { + // Stream was empty. No point in parsing. + return null; + } + forecastJsonStr = buffer.toString(); + } catch (IOException e) { + Log.e(LOG_TAG, "Error ", e); + // If the code didn't successfully get the weather data, there's no point in attemping + // to parse it. + return null; + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + if (reader != null) { + try { + reader.close(); + } catch (final IOException e) { + Log.e(LOG_TAG, "Error closing stream", e); + } + } + } + + try { + return getWeatherDataFromJson(forecastJsonStr, numDays, locationQuery); + } catch (JSONException e) { + Log.e(LOG_TAG, e.getMessage(), e); + e.printStackTrace(); + } + // This will only happen if there was an error getting or parsing the forecast. + return null; + } + + @Override + protected void onPostExecute(String[] result) { + if (result != null) { + mForecastAdapter.clear(); + for (String dayForecastStr : result) { + mForecastAdapter.add(dayForecastStr); + } + // New data is back from the server. Hooray! + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java b/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java index b6a667a..9dafe14 100644 --- a/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java +++ b/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java @@ -3,7 +3,6 @@ package uk.me.njae.sunshine; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; @@ -17,25 +16,10 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; -import android.widget.Toast; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLEncoder; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; /** * A placeholder fragment containing a simple view. @@ -92,31 +76,14 @@ public class ForecastFragment extends Fragment { } private void updateWeather() { - FetchWeatherTask weatherTask = new FetchWeatherTask(); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String location = preferences.getString(getString(R.string.pref_location_key), - getString(R.string.pref_location_default)); - // weatherTask.execute("2642465"); - weatherTask.execute(location); + String location = Utility.getPreferredLocation(getActivity()); + new FetchWeatherTask(getActivity(), mForecastAdapter).execute(location); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { -// String[] forecastArray = { -// "Today - Sunny - 10/10", -// "Tomorrow - Cloudy - 11/11", -// "Tuesday - Snow - 12/12", -// "Wednesday - Rain - 13/13", -// "Thursday - Hail - 14/14", -// "Friday - Scorchio - 15/15", -// "Saturday - Fog - 16/16" -// }; -// -// List weekForecast = new ArrayList( -// Arrays.asList(forecastArray)); - mForecastAdapter = new ArrayAdapter( getActivity(), R.layout.list_item_forecast, @@ -129,6 +96,7 @@ public class ForecastFragment extends Fragment { ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); listView.setAdapter(mForecastAdapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override public void onItemClick(AdapterView adapterView, View view, int position, long l) { String forecast = mForecastAdapter.getItem(position); @@ -146,205 +114,5 @@ public class ForecastFragment extends Fragment { updateWeather(); } - public class FetchWeatherTask extends AsyncTask { - - private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); - - @Override - protected void onPostExecute(String[] result) { - if (result != null) { - mForecastAdapter.clear(); -// for (String dayForecastStr: result) { -// mForecastAdapter.add(dayForecastStr); -// } - mForecastAdapter.addAll(result); - } - } - - @Override - protected String[] doInBackground(String... params) { - if (params.length == 0) { - return null; - } - - // These two need to be declared outside the try/catch - // so that they can be closed in the finally block. - HttpURLConnection urlConnection = null; - BufferedReader reader = null; - - // Will contain the raw JSON response as a string. - String forecastJsonStr; - - String format = "json"; - String units = "metric"; - int numDays = 7; - - try { - // Construct the URL for the OpenWeatherMap query - // Possible parameters are avaiable at OWM's forecast API page, at - // http://openweathermap.org/API#forecast - final String FORECAST_BASE_URL = "http://api.openweathermap.org/data/2.5/forecast/daily?"; - // final String QUERY_PARAM = "id"; // use this if using a location ID - final String QUERY_PARAM = "q"; - final String FORMAT_PARAM = "mode"; - final String UNITS_PARAM = "units"; - final String DAYS_PARAM = "cnt"; - - Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon() - .appendQueryParameter(QUERY_PARAM, params[0]) - .appendQueryParameter(FORMAT_PARAM, format) - .appendQueryParameter(UNITS_PARAM, units) - .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays)) - .build(); - - URL url = new URL(builtUri.toString()); -/**/ Log.v(LOG_TAG, "Built URI " + builtUri.toString()); - - // Create the request to OpenWeatherMap, and open the connection - urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.connect(); - - // Read the input stream into a String - InputStream inputStream = urlConnection.getInputStream(); - StringBuffer buffer = new StringBuffer(); - if (inputStream == null) { - // Nothing to do. - return null; - } - reader = new BufferedReader(new InputStreamReader(inputStream)); - - String line; - while ((line = reader.readLine()) != null) { - // Since it's JSON, adding a newline isn't necessary (it won't affect parsing) - // But it does make debugging a *lot* easier if you print out the completed - // buffer for debugging. - buffer.append(line + "\n"); - } - - if (buffer.length() == 0) { - // Stream was empty. No point in parsing. - return null; - } - forecastJsonStr = buffer.toString(); -// Log.v(LOG_TAG, "Forecast JSON string: " + forecastJsonStr); - try { - return getWeatherDataFromJson(forecastJsonStr, numDays); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - e.printStackTrace(); - } - - // This will only happen if we fail to read the forecast - return null; - - } catch (IOException e) { - Log.e(LOG_TAG, "Error ", e); - // If the code didn't successfully get the weather data, there's no point in attempting - // to parse it. - forecastJsonStr = null; - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - if (reader != null) { - try { - reader.close(); - } catch (final IOException e) { - Log.e(LOG_TAG, "Error closing stream", e); - } - } - } - return null; - } - - /* The date/time conversion code is going to be moved outside the asynctask later, - * so for convenience we're breaking it out into its own method now. - */ - private String getReadableDateString(long time) { - // Because the API returns a unix timestamp (measured in seconds), - // it must be converted to milliseconds in order to be converted to valid date. - Date date = new Date(time * 1000); - SimpleDateFormat format = new SimpleDateFormat("E, MMM d"); - return format.format(date).toString(); - } - - /** - * Prepare the weather high/lows for presentation. - */ - private String formatHighLows(double high, double low) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String units = preferences.getString(getString(R.string.pref_units_key), - getString(R.string.pref_units_default)); - // For presentation, assume the user doesn't care about tenths of a degree. - long roundedHigh = Math.round(high); - long roundedLow = Math.round(low); - - if (units.equals(getString(R.string.pref_units_imperial))) { - roundedHigh = Math.round(high * 9 / 5 + 32); - roundedLow = Math.round(low * 9 / 5 + 32); - } - - String highLowStr = roundedHigh + "/" + roundedLow; - return highLowStr; - } - - /** - * Take the String representing the complete forecast in JSON Format and - * pull out the data we need to construct the Strings needed for the wireframes. - *

- * Fortunately parsing is easy: constructor takes the JSON string and converts it - * into an Object hierarchy for us. - */ - private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays) - throws JSONException { - // These are the names of the JSON objects that need to be extracted. - final String OWM_LIST = "list"; - final String OWM_WEATHER = "weather"; - final String OWM_TEMPERATURE = "temp"; - final String OWM_MAX = "max"; - final String OWM_MIN = "min"; - final String OWM_DATETIME = "dt"; - final String OWM_DESCRIPTION = "main"; - - JSONObject forecastJson = new JSONObject(forecastJsonStr); - JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST); - - String[] resultStrs = new String[numDays]; - for (int i = 0; i < weatherArray.length(); i++) { - // For now, using the format "Day, description, hi/low" - String day; - String description; - String highAndLow; - - // Get the JSON object representing the day - JSONObject dayForecast = weatherArray.getJSONObject(i); - - // The date/time is returned as a long. We need to convert that - // into something human-readable, since most people won't read "1400356800" as - // "this saturday". - long dateTime = dayForecast.getLong(OWM_DATETIME); - day = getReadableDateString(dateTime); - - // description is in a child array called "weather", which is 1 element long. - JSONObject weatherObject = dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0); - description = weatherObject.getString(OWM_DESCRIPTION); - - // Temperatures are in a child object called "temp". Try not to name variables - // "temp" when working with temperature. It confuses everybody. - JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE); - double high = temperatureObject.getDouble(OWM_MAX); - double low = temperatureObject.getDouble(OWM_MIN); - - highAndLow = formatHighLows(high, low); - resultStrs[i] = day + " - " + description + " - " + highAndLow; - } - -// for (String s: resultStrs) { -// Log.v(LOG_TAG, "Forecast entry: " + s); -// } - return resultStrs; - } - } } diff --git a/app/src/main/java/uk/me/njae/sunshine/Utility.java b/app/src/main/java/uk/me/njae/sunshine/Utility.java new file mode 100644 index 0000000..eb2b37a --- /dev/null +++ b/app/src/main/java/uk/me/njae/sunshine/Utility.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 uk.me.njae.sunshine; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public class Utility { + public static String getPreferredLocation(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getString(context.getString(R.string.pref_location_key), + context.getString(R.string.pref_location_default)); + } +} diff --git a/app/src/main/java/uk/me/njae/sunshine/data/WeatherContract.java b/app/src/main/java/uk/me/njae/sunshine/data/WeatherContract.java index 99dd2d1..6555465 100644 --- a/app/src/main/java/uk/me/njae/sunshine/data/WeatherContract.java +++ b/app/src/main/java/uk/me/njae/sunshine/data/WeatherContract.java @@ -1,14 +1,62 @@ package uk.me.njae.sunshine.data; +import android.content.ContentUris; +import android.net.Uri; import android.provider.BaseColumns; +import java.text.SimpleDateFormat; +import java.util.Date; + /** * Created by neil on 09/11/14. */ public class WeatherContract { + // The "Content authority" is a name for the entire content provider, similar to the + // relationship between a domain name and its website. A convenient string to use for the + // content authority is the package name for the app, which is guaranteed to be unique on the + // device. + public static final String CONTENT_AUTHORITY = "uk.me.njae.sunshine"; + + // Use CONTENT_AUTHORITY to create the base of all URI's which apps will use to contact + // the content provider. + public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY); + + + // Format used for storing dates in the database. ALso used for converting those strings + // back into date objects for comparison/processing. + public static final String DATE_FORMAT = "yyyyMMdd"; + + /** + * Converts Date class to a string representation, used for easy comparison and database lookup. + * @param date The input date + * @return a DB-friendly representation of the date, using the format defined in DATE_FORMAT. + */ + public static String getDbDateString(Date date){ + // Because the API returns a unix timestamp (measured in seconds), + // it must be converted to milliseconds in order to be converted to valid date. + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + return sdf.format(date); + } + + // Possible paths (appended to base content URI for possible URI's) + // For instance, content://com.example.android.sunshine.app/weather/ is a valid path for + // looking at weather data. content://com.example.android.sunshine.app/givemeroot/ will fail, + // as the ContentProvider hasn't been given any information on what to do with "givemeroot". + // At least, let's hope not. Don't be that dev, reader. Don't be that dev. + public static final String PATH_WEATHER = "weather"; + public static final String PATH_LOCATION = "location"; + /* Inner class that defines the table contents of the location table */ public static final class LocationEntry implements BaseColumns { + public static final Uri CONTENT_URI = + BASE_CONTENT_URI.buildUpon().appendPath(PATH_LOCATION).build(); + + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION; + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION; + // Table name public static final String TABLE_NAME = "location"; @@ -24,11 +72,24 @@ public class WeatherContract { // map intent, we store the latitude and longitude as returned by openweathermap. public static final String COLUMN_COORD_LAT = "coord_lat"; public static final String COLUMN_COORD_LONG = "coord_long"; + + public static Uri buildLocationUri(long id) { + return ContentUris.withAppendedId(CONTENT_URI, id); + } } /* Inner class that defines the table contents of the weather table */ public static final class WeatherEntry implements BaseColumns { + public static final Uri CONTENT_URI = + BASE_CONTENT_URI.buildUpon().appendPath(PATH_WEATHER).build(); + + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER; + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER; + + public static final String TABLE_NAME = "weather"; // Column with the foreign key into the location table. @@ -57,5 +118,35 @@ public class WeatherContract { // Degrees are meteorological degrees (e.g, 0 is north, 180 is south). Stored as floats. public static final String COLUMN_DEGREES = "degrees"; + + public static Uri buildWeatherUri(long id) { + return ContentUris.withAppendedId(CONTENT_URI, id); + } + + public static Uri buildWeatherLocation(String locationSetting) { + return CONTENT_URI.buildUpon().appendPath(locationSetting).build(); + } + + public static Uri buildWeatherLocationWithStartDate( + String locationSetting, String startDate) { + return CONTENT_URI.buildUpon().appendPath(locationSetting) + .appendQueryParameter(COLUMN_DATETEXT, startDate).build(); + } + + public static Uri buildWeatherLocationWithDate(String locationSetting, String date) { + return CONTENT_URI.buildUpon().appendPath(locationSetting).appendPath(date).build(); + } + + public static String getLocationSettingFromUri(Uri uri) { + return uri.getPathSegments().get(1); + } + + public static String getDateFromUri(Uri uri) { + return uri.getPathSegments().get(2); + } + + public static String getStartDateFromUri(Uri uri) { + return uri.getQueryParameter(COLUMN_DATETEXT); + } } } diff --git a/app/src/main/java/uk/me/njae/sunshine/data/WeatherProvider.java b/app/src/main/java/uk/me/njae/sunshine/data/WeatherProvider.java new file mode 100644 index 0000000..e064050 --- /dev/null +++ b/app/src/main/java/uk/me/njae/sunshine/data/WeatherProvider.java @@ -0,0 +1,309 @@ +package uk.me.njae.sunshine.data; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; + +/** + * Created by neil on 09/11/14. + */ +public class WeatherProvider extends ContentProvider { + + // The URI Matcher used by this content provider. + private static final UriMatcher sUriMatcher = buildUriMatcher(); + + private static final int WEATHER = 100; + private static final int WEATHER_WITH_LOCATION = 101; + private static final int WEATHER_WITH_LOCATION_AND_DATE = 102; + private static final int LOCATION = 300; + private static final int LOCATION_ID = 301; + + private WeatherDbHelper mOpenHelper; + + private static UriMatcher buildUriMatcher() { + // I know what you're thinking. Why create a UriMatcher when you can use regular + // expressions instead? Because you're not crazy, that's why. + + // All paths added to the UriMatcher have a corresponding code to return when a match is + // found. The code passed into the constructor represents the code to return for the root + // URI. It's common to use NO_MATCH as the code for this case. + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + final String authority = WeatherContract.CONTENT_AUTHORITY; + + // For each type of URI you want to add, create a corresponding code. + matcher.addURI(authority, WeatherContract.PATH_WEATHER, WEATHER); + matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*", WEATHER_WITH_LOCATION); + matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*/*", WEATHER_WITH_LOCATION_AND_DATE); + + matcher.addURI(authority, WeatherContract.PATH_LOCATION, LOCATION); + matcher.addURI(authority, WeatherContract.PATH_LOCATION + "/#", LOCATION_ID); + + return matcher; + } + + + private static final SQLiteQueryBuilder sWeatherByLocationSettingQueryBuilder; + + static { + sWeatherByLocationSettingQueryBuilder = new SQLiteQueryBuilder(); + sWeatherByLocationSettingQueryBuilder.setTables( + WeatherContract.WeatherEntry.TABLE_NAME + " INNER JOIN " + + WeatherContract.LocationEntry.TABLE_NAME + + " ON " + WeatherContract.WeatherEntry.TABLE_NAME + + "." + WeatherContract.WeatherEntry.COLUMN_LOC_KEY + + " = " + WeatherContract.LocationEntry.TABLE_NAME + + "." + WeatherContract.LocationEntry._ID); + } + + private static final String sLocationSettingSelection = + WeatherContract.LocationEntry.TABLE_NAME+ + "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? "; + private static final String sLocationSettingWithStartDateSelection = + WeatherContract.LocationEntry.TABLE_NAME+ + "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " + + WeatherContract.WeatherEntry.COLUMN_DATETEXT + " >= ? "; + private static final String sLocationSettingAndDaySelection = + WeatherContract.LocationEntry.TABLE_NAME + + "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " + + WeatherContract.WeatherEntry.COLUMN_DATETEXT + " = ? "; + + private Cursor getWeatherByLocationSetting(Uri uri, String[] projection, String sortOrder) { + String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri); + String startDate = WeatherContract.WeatherEntry.getStartDateFromUri(uri); + + String[] selectionArgs; + String selection; + + if (startDate == null) { + selection = sLocationSettingSelection; + selectionArgs = new String[]{locationSetting}; + } else { + selectionArgs = new String[]{locationSetting, startDate}; + selection = sLocationSettingWithStartDateSelection; + } + + return sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(), + projection, + selection, + selectionArgs, + null, + null, + sortOrder + ); + } + + private Cursor getWeatherByLocationSettingAndDate( + Uri uri, String[] projection, String sortOrder) { + String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri); + String date = WeatherContract.WeatherEntry.getDateFromUri(uri); + + return sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(), + projection, + sLocationSettingAndDaySelection, + new String[]{locationSetting, date}, + null, + null, + sortOrder + ); + } + + @Override + public boolean onCreate() { + mOpenHelper = new WeatherDbHelper(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + // Here's the switch statement that, given a URI, will determine what kind of request it is, + // and query the database accordingly. + Cursor retCursor; + switch (sUriMatcher.match(uri)) { + // "weather/*/*" + case WEATHER_WITH_LOCATION_AND_DATE: + { + retCursor = getWeatherByLocationSettingAndDate(uri, projection, sortOrder); + break; + } + // "weather/*" + case WEATHER_WITH_LOCATION: { + retCursor = getWeatherByLocationSetting(uri, projection, sortOrder); + break; + } + // "weather" + case WEATHER: { + retCursor = mOpenHelper.getReadableDatabase().query( + WeatherContract.WeatherEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + sortOrder + ); + break; + } + // "location/*" + case LOCATION_ID: { + retCursor = mOpenHelper.getReadableDatabase().query( + WeatherContract.LocationEntry.TABLE_NAME, + projection, + WeatherContract.LocationEntry._ID + " = '" + ContentUris.parseId(uri) + "'", + null, + null, + null, + sortOrder + ); + break; + } + // "location" + case LOCATION: { + retCursor = mOpenHelper.getReadableDatabase().query( + WeatherContract.LocationEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + sortOrder); + break; + } + + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + retCursor.setNotificationUri(getContext().getContentResolver(), uri); + return retCursor; + } + + @Override + public String getType(Uri uri) { + // Use the Uri Matcher to determine what kind of URI this is. + final int match = sUriMatcher.match(uri); + + switch (match) { + case WEATHER_WITH_LOCATION_AND_DATE: + return WeatherContract.WeatherEntry.CONTENT_ITEM_TYPE; + case WEATHER_WITH_LOCATION: + return WeatherContract.WeatherEntry.CONTENT_TYPE; + case WEATHER: + return WeatherContract.WeatherEntry.CONTENT_TYPE; + case LOCATION: + return WeatherContract.LocationEntry.CONTENT_TYPE; + case LOCATION_ID: + return WeatherContract.LocationEntry.CONTENT_ITEM_TYPE; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + Uri returnUri; + + switch (match) { + case WEATHER: { + long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, contentValues); + if ( _id > 0 ) + returnUri = WeatherContract.WeatherEntry.buildWeatherUri(_id); + else + throw new android.database.SQLException("Failed to insert row into " + uri); + break; + } + case LOCATION: { + long _id = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, contentValues); + if ( _id > 0 ) + returnUri = WeatherContract.LocationEntry.buildLocationUri(_id); + else + throw new android.database.SQLException("Failed to insert row into " + uri); + break; + } + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + getContext().getContentResolver().notifyChange(returnUri, null); + return returnUri; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + int rowsDeleted = 0; + + switch (match) { + case WEATHER: { + rowsDeleted = db.delete(WeatherContract.WeatherEntry.TABLE_NAME, selection, selectionArgs); + break; + } + case LOCATION: { + rowsDeleted = db.delete(WeatherContract.LocationEntry.TABLE_NAME, selection, selectionArgs); + break; + } + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + if (selection == null || rowsDeleted != 0) { + getContext().getContentResolver().notifyChange(uri, null); + } + return rowsDeleted; + } + + @Override + public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + int rowsUpdated; + + switch (match) { + case WEATHER: + rowsUpdated = db.update(WeatherContract.WeatherEntry.TABLE_NAME, contentValues, selection, + selectionArgs); + break; + case LOCATION: + rowsUpdated = db.update(WeatherContract.LocationEntry.TABLE_NAME, contentValues, selection, + selectionArgs); + break; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + if (rowsUpdated != 0) { + getContext().getContentResolver().notifyChange(uri, null); + } + return rowsUpdated; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + switch (match) { + case WEATHER: + db.beginTransaction(); + int returnCount = 0; + try { + for (ContentValues value : values) { + long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, value); + if (_id != -1) { + returnCount++; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + getContext().getContentResolver().notifyChange(uri, null); + return returnCount; + default: + return super.bulkInsert(uri, values); + } + } +}