+ * See
+ * Android Design: Settings for design guidelines and the Settings
+ * API Guide for more information on developing a Settings UI.
+ */
+public class SettingsActivity extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Add 'general' preferences, defined in the XML file
+ addPreferencesFromResource(R.xml.pref_general);
+
+ // For all preferences, attach an OnPreferenceChangeListener so the UI summary can be
+ // updated when the preference changes.
+ bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key)));
+ bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key)));
+ }
+
+ /**
+ * Attaches a listener so the summary is always updated with the preference value.
+ * Also fires the listener once, to initialize the summary (so it shows up before the value
+ * is changed.)
+ */
+ private void bindPreferenceSummaryToValue(Preference preference) {
+ // Set the listener to watch for value changes.
+ preference.setOnPreferenceChangeListener(this);
+
+ // Trigger the listener immediately with the preference's
+ // current value.
+ onPreferenceChange(preference,
+ PreferenceManager
+ .getDefaultSharedPreferences(preference.getContext())
+ .getString(preference.getKey(), ""));
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String stringValue = value.toString();
+
+ if (preference instanceof ListPreference) {
+ // For list preferences, look up the correct display value in
+ // the preference's 'entries' list (since they have separate labels/values).
+ ListPreference listPreference = (ListPreference) preference;
+ int prefIndex = listPreference.findIndexOfValue(stringValue);
+ if (prefIndex >= 0) {
+ preference.setSummary(listPreference.getEntries()[prefIndex]);
+ }
+ } else {
+ // For other preferences, set the summary to the value's simple string representation.
+ preference.setSummary(stringValue);
+ }
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public Intent getParentActivityIntent() {
+ return super.getParentActivityIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/Utility.java b/app/src/main/java/com/example/android/sunshine/app/Utility.java
new file mode 100644
index 000000000..95f5b50e1
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/Utility.java
@@ -0,0 +1,247 @@
+/*
+ * 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 com.example.android.sunshine.app;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.format.Time;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+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));
+ }
+
+ public static boolean isMetric(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ return prefs.getString(context.getString(R.string.pref_units_key),
+ context.getString(R.string.pref_units_metric))
+ .equals(context.getString(R.string.pref_units_metric));
+ }
+
+ static String formatTemperature(Context context, double temperature, boolean isMetric) {
+ double temp;
+ if ( !isMetric ) {
+ temp = 9*temperature/5+32;
+ } else {
+ temp = temperature;
+ }
+ return context.getString(R.string.format_temperature, temp);
+ }
+
+ static String formatDate(long dateInMilliseconds) {
+ Date date = new Date(dateInMilliseconds);
+ return DateFormat.getDateInstance().format(date);
+ }
+
+ // 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";
+
+ /**
+ * Helper method to convert the database representation of the date into something to display
+ * to users. As classy and polished a user experience as "20140102" is, we can do better.
+ *
+ * @param context Context to use for resource localization
+ * @param dateInMillis The date in milliseconds
+ * @return a user-friendly representation of the date.
+ */
+ public static String getFriendlyDayString(Context context, long dateInMillis) {
+ // The day string for forecast uses the following logic:
+ // For today: "Today, June 8"
+ // For tomorrow: "Tomorrow"
+ // For the next 5 days: "Wednesday" (just the day name)
+ // For all days after that: "Mon Jun 8"
+
+ Time time = new Time();
+ time.setToNow();
+ long currentTime = System.currentTimeMillis();
+ int julianDay = Time.getJulianDay(dateInMillis, time.gmtoff);
+ int currentJulianDay = Time.getJulianDay(currentTime, time.gmtoff);
+
+ // If the date we're building the String for is today's date, the format
+ // is "Today, June 24"
+ if (julianDay == currentJulianDay) {
+ String today = context.getString(R.string.today);
+ int formatId = R.string.format_full_friendly_date;
+ return String.format(context.getString(
+ formatId,
+ today,
+ getFormattedMonthDay(context, dateInMillis)));
+ } else if ( julianDay < currentJulianDay + 7 ) {
+ // If the input date is less than a week in the future, just return the day name.
+ return getDayName(context, dateInMillis);
+ } else {
+ // Otherwise, use the form "Mon Jun 3"
+ SimpleDateFormat shortenedDateFormat = new SimpleDateFormat("EEE MMM dd");
+ return shortenedDateFormat.format(dateInMillis);
+ }
+ }
+
+ /**
+ * Given a day, returns just the name to use for that day.
+ * E.g "today", "tomorrow", "wednesday".
+ *
+ * @param context Context to use for resource localization
+ * @param dateInMillis The date in milliseconds
+ * @return
+ */
+ public static String getDayName(Context context, long dateInMillis) {
+ // If the date is today, return the localized version of "Today" instead of the actual
+ // day name.
+
+ Time t = new Time();
+ t.setToNow();
+ int julianDay = Time.getJulianDay(dateInMillis, t.gmtoff);
+ int currentJulianDay = Time.getJulianDay(System.currentTimeMillis(), t.gmtoff);
+ if (julianDay == currentJulianDay) {
+ return context.getString(R.string.today);
+ } else if ( julianDay == currentJulianDay +1 ) {
+ return context.getString(R.string.tomorrow);
+ } else {
+ Time time = new Time();
+ time.setToNow();
+ // Otherwise, the format is just the day of the week (e.g "Wednesday".
+ SimpleDateFormat dayFormat = new SimpleDateFormat("EEEE");
+ return dayFormat.format(dateInMillis);
+ }
+ }
+
+ /**
+ * Converts db date format to the format "Month day", e.g "June 24".
+ * @param context Context to use for resource localization
+ * @param dateInMillis The db formatted date string, expected to be of the form specified
+ * in Utility.DATE_FORMAT
+ * @return The day in the form of a string formatted "December 6"
+ */
+ public static String getFormattedMonthDay(Context context, long dateInMillis ) {
+ Time time = new Time();
+ time.setToNow();
+ SimpleDateFormat dbDateFormat = new SimpleDateFormat(Utility.DATE_FORMAT);
+ SimpleDateFormat monthDayFormat = new SimpleDateFormat("MMMM dd");
+ String monthDayString = monthDayFormat.format(dateInMillis);
+ return monthDayString;
+ }
+
+ public static String getFormattedWind(Context context, float windSpeed, float degrees) {
+ int windFormat;
+ if (Utility.isMetric(context)) {
+ windFormat = R.string.format_wind_kmh;
+ } else {
+ windFormat = R.string.format_wind_mph;
+ windSpeed = .621371192237334f * windSpeed;
+ }
+
+ // From wind direction in degrees, determine compass direction as a string (e.g NW)
+ // You know what's fun, writing really long if/else statements with tons of possible
+ // conditions. Seriously, try it!
+ String direction = "Unknown";
+ if (degrees >= 337.5 || degrees < 22.5) {
+ direction = "N";
+ } else if (degrees >= 22.5 && degrees < 67.5) {
+ direction = "NE";
+ } else if (degrees >= 67.5 && degrees < 112.5) {
+ direction = "E";
+ } else if (degrees >= 112.5 && degrees < 157.5) {
+ direction = "SE";
+ } else if (degrees >= 157.5 && degrees < 202.5) {
+ direction = "S";
+ } else if (degrees >= 202.5 && degrees < 247.5) {
+ direction = "SW";
+ } else if (degrees >= 247.5 && degrees < 292.5) {
+ direction = "W";
+ } else if (degrees >= 292.5 && degrees < 337.5) {
+ direction = "NW";
+ }
+ return String.format(context.getString(windFormat), windSpeed, direction);
+ }
+
+ /**
+ * Helper method to provide the icon resource id according to the weather condition id returned
+ * by the OpenWeatherMap call.
+ * @param weatherId from OpenWeatherMap API response
+ * @return resource id for the corresponding icon. -1 if no relation is found.
+ */
+ public static int getIconResourceForWeatherCondition(int weatherId) {
+ // Based on weather code data found at:
+ // http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
+ if (weatherId >= 200 && weatherId <= 232) {
+ return R.drawable.ic_storm;
+ } else if (weatherId >= 300 && weatherId <= 321) {
+ return R.drawable.ic_light_rain;
+ } else if (weatherId >= 500 && weatherId <= 504) {
+ return R.drawable.ic_rain;
+ } else if (weatherId == 511) {
+ return R.drawable.ic_snow;
+ } else if (weatherId >= 520 && weatherId <= 531) {
+ return R.drawable.ic_rain;
+ } else if (weatherId >= 600 && weatherId <= 622) {
+ return R.drawable.ic_snow;
+ } else if (weatherId >= 701 && weatherId <= 761) {
+ return R.drawable.ic_fog;
+ } else if (weatherId == 761 || weatherId == 781) {
+ return R.drawable.ic_storm;
+ } else if (weatherId == 800) {
+ return R.drawable.ic_clear;
+ } else if (weatherId == 801) {
+ return R.drawable.ic_light_clouds;
+ } else if (weatherId >= 802 && weatherId <= 804) {
+ return R.drawable.ic_cloudy;
+ }
+ return -1;
+ }
+
+ /**
+ * Helper method to provide the art resource id according to the weather condition id returned
+ * by the OpenWeatherMap call.
+ * @param weatherId from OpenWeatherMap API response
+ * @return resource id for the corresponding icon. -1 if no relation is found.
+ */
+ public static int getArtResourceForWeatherCondition(int weatherId) {
+ // Based on weather code data found at:
+ // http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
+ if (weatherId >= 200 && weatherId <= 232) {
+ return R.drawable.art_storm;
+ } else if (weatherId >= 300 && weatherId <= 321) {
+ return R.drawable.art_light_rain;
+ } else if (weatherId >= 500 && weatherId <= 504) {
+ return R.drawable.art_rain;
+ } else if (weatherId == 511) {
+ return R.drawable.art_snow;
+ } else if (weatherId >= 520 && weatherId <= 531) {
+ return R.drawable.art_rain;
+ } else if (weatherId >= 600 && weatherId <= 622) {
+ return R.drawable.art_snow;
+ } else if (weatherId >= 701 && weatherId <= 761) {
+ return R.drawable.art_fog;
+ } else if (weatherId == 761 || weatherId == 781) {
+ return R.drawable.art_storm;
+ } else if (weatherId == 800) {
+ return R.drawable.art_clear;
+ } else if (weatherId == 801) {
+ return R.drawable.art_light_clouds;
+ } else if (weatherId >= 802 && weatherId <= 804) {
+ return R.drawable.art_clouds;
+ }
+ return -1;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/data/WeatherContract.java b/app/src/main/java/com/example/android/sunshine/app/data/WeatherContract.java
new file mode 100644
index 000000000..63387c4a8
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/data/WeatherContract.java
@@ -0,0 +1,168 @@
+/*
+ * 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 com.example.android.sunshine.app.data;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.text.format.Time;
+
+/**
+ * Defines table and column names for the weather database.
+ */
+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 = "com.example.android.sunshine.app";
+
+ // 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);
+
+ // 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";
+
+ // To make it easy to query for the exact date, we normalize all dates that go into
+ // the database to the start of the the Julian day at UTC.
+ public static long normalizeDate(long startDate) {
+ // normalize the start date to the beginning of the (UTC) day
+ Time time = new Time();
+ time.set(startDate);
+ int julianDay = Time.getJulianDay(startDate, time.gmtoff);
+ return time.setJulianDay(julianDay);
+ }
+
+ /* 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 =
+ ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION;
+ public static final String CONTENT_ITEM_TYPE =
+ ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION;
+
+ // Table name
+ public static final String TABLE_NAME = "location";
+
+ // The location setting string is what will be sent to openweathermap
+ // as the location query.
+ public static final String COLUMN_LOCATION_SETTING = "location_setting";
+
+ // Human readable location string, provided by the API. Because for styling,
+ // "Mountain View" is more recognizable than 94043.
+ public static final String COLUMN_CITY_NAME = "city_name";
+
+ // In order to uniquely pinpoint the location on the map when we launch the
+ // 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 =
+ ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;
+ public static final String CONTENT_ITEM_TYPE =
+ ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;
+
+ public static final String TABLE_NAME = "weather";
+
+ // Column with the foreign key into the location table.
+ public static final String COLUMN_LOC_KEY = "location_id";
+ // Date, stored as long in milliseconds since the epoch
+ public static final String COLUMN_DATE = "date";
+ // Weather id as returned by API, to identify the icon to be used
+ public static final String COLUMN_WEATHER_ID = "weather_id";
+
+ // Short description and long description of the weather, as provided by API.
+ // e.g "clear" vs "sky is clear".
+ public static final String COLUMN_SHORT_DESC = "short_desc";
+
+ // Min and max temperatures for the day (stored as floats)
+ public static final String COLUMN_MIN_TEMP = "min";
+ public static final String COLUMN_MAX_TEMP = "max";
+
+ // Humidity is stored as a float representing percentage
+ public static final String COLUMN_HUMIDITY = "humidity";
+
+ // Humidity is stored as a float representing percentage
+ public static final String COLUMN_PRESSURE = "pressure";
+
+ // Windspeed is stored as a float representing windspeed mph
+ public static final String COLUMN_WIND_SPEED = "wind";
+
+ // 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);
+ }
+
+ /*
+ Student: This is the buildWeatherLocation function you filled in.
+ */
+ public static Uri buildWeatherLocation(String locationSetting) {
+ return CONTENT_URI.buildUpon().appendPath(locationSetting).build();
+ }
+
+ public static Uri buildWeatherLocationWithStartDate(
+ String locationSetting, long startDate) {
+ long normalizedDate = normalizeDate(startDate);
+ return CONTENT_URI.buildUpon().appendPath(locationSetting)
+ .appendQueryParameter(COLUMN_DATE, Long.toString(normalizedDate)).build();
+ }
+
+ public static Uri buildWeatherLocationWithDate(String locationSetting, long date) {
+ return CONTENT_URI.buildUpon().appendPath(locationSetting)
+ .appendPath(Long.toString(normalizeDate(date))).build();
+ }
+
+ public static String getLocationSettingFromUri(Uri uri) {
+ return uri.getPathSegments().get(1);
+ }
+
+ public static long getDateFromUri(Uri uri) {
+ return Long.parseLong(uri.getPathSegments().get(2));
+ }
+
+ public static long getStartDateFromUri(Uri uri) {
+ String dateString = uri.getQueryParameter(COLUMN_DATE);
+ if (null != dateString && dateString.length() > 0)
+ return Long.parseLong(dateString);
+ else
+ return 0;
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/data/WeatherDbHelper.java b/app/src/main/java/com/example/android/sunshine/app/data/WeatherDbHelper.java
new file mode 100644
index 000000000..a933c101f
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/data/WeatherDbHelper.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.example.android.sunshine.app.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.example.android.sunshine.app.data.WeatherContract.LocationEntry;
+import com.example.android.sunshine.app.data.WeatherContract.WeatherEntry;
+
+/**
+ * Manages a local database for weather data.
+ */
+public class WeatherDbHelper extends SQLiteOpenHelper {
+
+ // If you change the database schema, you must increment the database version.
+ private static final int DATABASE_VERSION = 2;
+
+ static final String DATABASE_NAME = "weather.db";
+
+ public WeatherDbHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase sqLiteDatabase) {
+ // Create a table to hold locations. A location consists of the string supplied in the
+ // location setting, the city name, and the latitude and longitude
+ final String SQL_CREATE_LOCATION_TABLE = "CREATE TABLE " + LocationEntry.TABLE_NAME + " (" +
+ LocationEntry._ID + " INTEGER PRIMARY KEY," +
+ LocationEntry.COLUMN_LOCATION_SETTING + " TEXT UNIQUE NOT NULL, " +
+ LocationEntry.COLUMN_CITY_NAME + " TEXT NOT NULL, " +
+ LocationEntry.COLUMN_COORD_LAT + " REAL NOT NULL, " +
+ LocationEntry.COLUMN_COORD_LONG + " REAL NOT NULL " +
+ " );";
+
+ final String SQL_CREATE_WEATHER_TABLE = "CREATE TABLE " + WeatherEntry.TABLE_NAME + " (" +
+ // Why AutoIncrement here, and not above?
+ // Unique keys will be auto-generated in either case. But for weather
+ // forecasting, it's reasonable to assume the user will want information
+ // for a certain date and all dates *following*, so the forecast data
+ // should be sorted accordingly.
+ WeatherEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+
+ // the ID of the location entry associated with this weather data
+ WeatherEntry.COLUMN_LOC_KEY + " INTEGER NOT NULL, " +
+ WeatherEntry.COLUMN_DATE + " INTEGER NOT NULL, " +
+ WeatherEntry.COLUMN_SHORT_DESC + " TEXT NOT NULL, " +
+ WeatherEntry.COLUMN_WEATHER_ID + " INTEGER NOT NULL," +
+
+ WeatherEntry.COLUMN_MIN_TEMP + " REAL NOT NULL, " +
+ WeatherEntry.COLUMN_MAX_TEMP + " REAL NOT NULL, " +
+
+ WeatherEntry.COLUMN_HUMIDITY + " REAL NOT NULL, " +
+ WeatherEntry.COLUMN_PRESSURE + " REAL NOT NULL, " +
+ WeatherEntry.COLUMN_WIND_SPEED + " REAL NOT NULL, " +
+ WeatherEntry.COLUMN_DEGREES + " REAL NOT NULL, " +
+
+ // Set up the location column as a foreign key to location table.
+ " FOREIGN KEY (" + WeatherEntry.COLUMN_LOC_KEY + ") REFERENCES " +
+ LocationEntry.TABLE_NAME + " (" + LocationEntry._ID + "), " +
+
+ // To assure the application have just one weather entry per day
+ // per location, it's created a UNIQUE constraint with REPLACE strategy
+ " UNIQUE (" + WeatherEntry.COLUMN_DATE + ", " +
+ WeatherEntry.COLUMN_LOC_KEY + ") ON CONFLICT REPLACE);";
+
+ sqLiteDatabase.execSQL(SQL_CREATE_LOCATION_TABLE);
+ sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ // This database is only a cache for online data, so its upgrade policy is
+ // to simply to discard the data and start over
+ // Note that this only fires if you change the version number for your database.
+ // It does NOT depend on the version number for your application.
+ // If you want to update the schema without wiping data, commenting out the next 2 lines
+ // should be your top priority before modifying this method.
+ sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + LocationEntry.TABLE_NAME);
+ sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WeatherEntry.TABLE_NAME);
+ onCreate(sqLiteDatabase);
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/data/WeatherProvider.java b/app/src/main/java/com/example/android/sunshine/app/data/WeatherProvider.java
new file mode 100644
index 000000000..f607ec793
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/data/WeatherProvider.java
@@ -0,0 +1,354 @@
+/*
+ * 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 com.example.android.sunshine.app.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentProvider;
+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;
+
+public class WeatherProvider extends ContentProvider {
+
+ // The URI Matcher used by this content provider.
+ private static final UriMatcher sUriMatcher = buildUriMatcher();
+ private WeatherDbHelper mOpenHelper;
+
+ static final int WEATHER = 100;
+ static final int WEATHER_WITH_LOCATION = 101;
+ static final int WEATHER_WITH_LOCATION_AND_DATE = 102;
+ static final int LOCATION = 300;
+
+ private static final SQLiteQueryBuilder sWeatherByLocationSettingQueryBuilder;
+
+ static{
+ sWeatherByLocationSettingQueryBuilder = new SQLiteQueryBuilder();
+
+ //This is an inner join which looks like
+ //weather INNER JOIN location ON weather.location_id = location._id
+ 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);
+ }
+
+ //location.location_setting = ?
+ private static final String sLocationSettingSelection =
+ WeatherContract.LocationEntry.TABLE_NAME+
+ "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? ";
+
+ //location.location_setting = ? AND date >= ?
+ private static final String sLocationSettingWithStartDateSelection =
+ WeatherContract.LocationEntry.TABLE_NAME+
+ "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " +
+ WeatherContract.WeatherEntry.COLUMN_DATE + " >= ? ";
+
+ //location.location_setting = ? AND date = ?
+ private static final String sLocationSettingAndDaySelection =
+ WeatherContract.LocationEntry.TABLE_NAME +
+ "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " +
+ WeatherContract.WeatherEntry.COLUMN_DATE + " = ? ";
+
+ private Cursor getWeatherByLocationSetting(Uri uri, String[] projection, String sortOrder) {
+ String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri);
+ long startDate = WeatherContract.WeatherEntry.getStartDateFromUri(uri);
+
+ String[] selectionArgs;
+ String selection;
+
+ if (startDate == 0) {
+ selection = sLocationSettingSelection;
+ selectionArgs = new String[]{locationSetting};
+ } else {
+ selectionArgs = new String[]{locationSetting, Long.toString(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);
+ long date = WeatherContract.WeatherEntry.getDateFromUri(uri);
+
+ return sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(),
+ projection,
+ sLocationSettingAndDaySelection,
+ new String[]{locationSetting, Long.toString(date)},
+ null,
+ null,
+ sortOrder
+ );
+ }
+
+ /*
+ Students: Here is where you need to create the UriMatcher. This UriMatcher will
+ match each URI to the WEATHER, WEATHER_WITH_LOCATION, WEATHER_WITH_LOCATION_AND_DATE,
+ and LOCATION integer constants defined above. You can test this by uncommenting the
+ testUriMatcher test within TestUriMatcher.
+ */
+ 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);
+ return matcher;
+ }
+
+ /*
+ Students: We've coded this for you. We just create a new WeatherDbHelper for later use
+ here.
+ */
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new WeatherDbHelper(getContext());
+ return true;
+ }
+
+ /*
+ Students: Here's where you'll code the getType function that uses the UriMatcher. You can
+ test this by uncommenting testGetType in TestProvider.
+
+ */
+ @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) {
+ // Student: Uncomment and fill out these two cases
+ 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;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ @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: {
+ 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;
+ }
+
+ /*
+ Student: Add the ability to insert Locations to the implementation of this function.
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ Uri returnUri;
+
+ switch (match) {
+ case WEATHER: {
+ normalizeDate(values);
+ long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, values);
+ 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, values);
+ 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(uri, 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;
+ // this makes delete all rows return the number of rows deleted
+ if ( null == selection ) selection = "1";
+ 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);
+ }
+ // Because a null deletes all rows
+ if (rowsDeleted != 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+ return rowsDeleted;
+ }
+
+ private void normalizeDate(ContentValues values) {
+ // normalize the date value
+ if (values.containsKey(WeatherContract.WeatherEntry.COLUMN_DATE)) {
+ long dateValue = values.getAsLong(WeatherContract.WeatherEntry.COLUMN_DATE);
+ values.put(WeatherContract.WeatherEntry.COLUMN_DATE, WeatherContract.normalizeDate(dateValue));
+ }
+ }
+
+ @Override
+ public int update(
+ Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ int rowsUpdated;
+
+ switch (match) {
+ case WEATHER:
+ normalizeDate(values);
+ rowsUpdated = db.update(WeatherContract.WeatherEntry.TABLE_NAME, values, selection,
+ selectionArgs);
+ break;
+ case LOCATION:
+ rowsUpdated = db.update(WeatherContract.LocationEntry.TABLE_NAME, values, 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) {
+ normalizeDate(value);
+ 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);
+ }
+ }
+
+ // You do not need to call this method. This is a method specifically to assist the testing
+ // framework in running smoothly. You can read more at:
+ // http://developer.android.com/reference/android/content/ContentProvider.html#shutdown()
+ @Override
+ @TargetApi(11)
+ public void shutdown() {
+ mOpenHelper.close();
+ super.shutdown();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/service/SunshineService.java b/app/src/main/java/com/example/android/sunshine/app/service/SunshineService.java
new file mode 100644
index 000000000..263e9a308
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/service/SunshineService.java
@@ -0,0 +1,337 @@
+/*
+ * 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 com.example.android.sunshine.app.service;
+
+import android.app.IntentService;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.format.Time;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+
+import com.example.android.sunshine.app.BuildConfig;
+import com.example.android.sunshine.app.data.WeatherContract;
+
+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.util.Vector;
+
+
+public class SunshineService extends IntentService {
+ private ArrayAdapter