From cffe4d29f34f7c0ef8b12d99591b684b7c893779 Mon Sep 17 00:00:00 2001 From: Neil Smith Date: Mon, 10 Nov 2014 08:20:27 +0000 Subject: [PATCH] Mostly split the interface to use the database --- .../uk/me/njae/sunshine/FetchWeatherTask.java | 113 ++--------------- .../uk/me/njae/sunshine/ForecastFragment.java | 115 ++++++++++++++++-- .../java/uk/me/njae/sunshine/Utility.java | 33 +++++ .../njae/sunshine/data/WeatherContract.java | 17 +++ .../main/res/layout/list_item_forecast.xml | 52 ++++++-- app/src/main/res/values/arrays.xml | 2 +- .../res/values/strings_activity_settings.xml | 2 +- app/src/main/res/xml/pref_general.xml | 9 +- 8 files changed, 213 insertions(+), 130 deletions(-) diff --git a/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java b/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java index 44506c6..43747fd 100644 --- a/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java +++ b/app/src/main/java/uk/me/njae/sunshine/FetchWeatherTask.java @@ -3,14 +3,10 @@ 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; @@ -22,7 +18,6 @@ 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; @@ -35,59 +30,14 @@ import uk.me.njae.sunshine.data.WeatherContract.WeatherEntry; */ -public class FetchWeatherTask extends AsyncTask { +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) { + public FetchWeatherTask(Context context) { 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; } /** @@ -101,7 +51,7 @@ public class FetchWeatherTask extends AsyncTask { */ private long addLocation(String locationSetting, String cityName, double lat, double lon) { - Log.v(LOG_TAG, "inserting " + cityName + ", with coord: " + lat + ", " + 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( @@ -112,11 +62,11 @@ public class FetchWeatherTask extends AsyncTask { null); if (cursor.moveToFirst()) { - Log.v(LOG_TAG, "Found it in the database!"); + // 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!"); +// 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); @@ -137,7 +87,7 @@ public class FetchWeatherTask extends AsyncTask { * 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, + private void getWeatherDataFromJson(String forecastJsonStr, int numDays, String locationSetting) throws JSONException { @@ -177,7 +127,7 @@ public class FetchWeatherTask extends AsyncTask { double cityLatitude = coordJSON.getLong(OWM_COORD_LAT); double cityLongitude = coordJSON.getLong(OWM_COORD_LONG); - Log.v(LOG_TAG, cityName + ", with coord: " + cityLatitude + " " + cityLongitude); +// Log.v(LOG_TAG, cityName + ", with coord: " + cityLatitude + " " + cityLongitude); // Insert the location into the database. long locationID = addLocation(locationSetting, cityName, cityLatitude, cityLongitude); @@ -185,7 +135,7 @@ public class FetchWeatherTask extends AsyncTask { // Get and insert the new weather information into the database Vector cVVector = new Vector(weatherArray.length()); - String[] resultStrs = new String[numDays]; +// String[] resultStrs = new String[numDays]; for (int i = 0; i < weatherArray.length(); i++) { // These are the values that will be collected. @@ -242,46 +192,16 @@ public class FetchWeatherTask extends AsyncTask { 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! :( **********"); - } - } + mContext.getContentResolver().bulkInsert(WeatherEntry.CONTENT_URI, cvArray); } - return resultStrs; } @Override - protected String[] doInBackground(String... params) { + protected Void doInBackground(String... params) { // If there's no zip code, there's nothing to look up. Verify size of params. if (params.length == 0) { @@ -367,7 +287,7 @@ public class FetchWeatherTask extends AsyncTask { } try { - return getWeatherDataFromJson(forecastJsonStr, numDays, locationQuery); + getWeatherDataFromJson(forecastJsonStr, numDays, locationQuery); } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); e.printStackTrace(); @@ -376,15 +296,4 @@ public class FetchWeatherTask extends AsyncTask { 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 9dafe14..d78c705 100644 --- a/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java +++ b/app/src/main/java/uk/me/njae/sunshine/ForecastFragment.java @@ -2,10 +2,15 @@ package uk.me.njae.sunshine; import android.content.Intent; import android.content.SharedPreferences; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.SimpleCursorAdapter; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -14,21 +19,53 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.ListView; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.util.ArrayList; +import java.util.Date; + +import uk.me.njae.sunshine.data.WeatherContract; +import uk.me.njae.sunshine.data.WeatherContract.LocationEntry; +import uk.me.njae.sunshine.data.WeatherContract.WeatherEntry; /** * A placeholder fragment containing a simple view. */ -public class ForecastFragment extends Fragment { +public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks { private final String LOG_TAG = ForecastFragment.class.getSimpleName(); - private ArrayAdapter mForecastAdapter; + private SimpleCursorAdapter mForecastAdapter; + + private static final int FORECAST_LOADER = 0; + private String mLocation; + + // For the forecast view we're showing only a small subset of the stored data. + // Specify the columns we need. + private static final String[] FORECAST_COLUMNS = { + // In this case the id needs to be fully qualified with a table name, since + // the content provider joins the location & weather tables in the background + // (both have an _id column) + // On the one hand, that's annoying. On the other, you can search the weather table + // using the location set by the user, which is only in the Location table. + // So the convenience is worth it. + WeatherEntry.TABLE_NAME + "." + WeatherEntry._ID, + WeatherEntry.COLUMN_DATETEXT, + WeatherEntry.COLUMN_SHORT_DESC, + WeatherEntry.COLUMN_MAX_TEMP, + WeatherEntry.COLUMN_MIN_TEMP, + LocationEntry.COLUMN_LOCATION_SETTING + }; + + // These indices are tied to FORECAST_COLUMNS. If FORECAST_COLUMNS changes, these + // must change. + public static final int COL_WEATHER_ID = 0; + public static final int COL_WEATHER_DATE = 1; + public static final int COL_WEATHER_DESC = 2; + public static final int COL_WEATHER_MAX_TEMP = 3; + public static final int COL_WEATHER_MIN_TEMP = 4; + public static final int COL_LOCATION_SETTING = 5; public ForecastFragment() { } @@ -75,20 +112,39 @@ public class ForecastFragment extends Fragment { return super.onOptionsItemSelected(item); } + @Override + public void onActivityCreated(Bundle savedInstanceState) { + getLoaderManager().initLoader(FORECAST_LOADER, null, this); + super.onActivityCreated(savedInstanceState); + } + private void updateWeather() { String location = Utility.getPreferredLocation(getActivity()); - new FetchWeatherTask(getActivity(), mForecastAdapter).execute(location); + new FetchWeatherTask(getActivity()).execute(location); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - mForecastAdapter = new ArrayAdapter( + // The SimpleCursorAdapter will take data from the database through the + // Loader and use it to populate the ListView it's attached to. + mForecastAdapter = new SimpleCursorAdapter( getActivity(), R.layout.list_item_forecast, - R.id.list_item_forecast_textview, - new ArrayList() // weekForecast + null, + // the column names to use to fill the textviews + new String[]{WeatherContract.WeatherEntry.COLUMN_DATETEXT, + WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, + WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, + WeatherContract.WeatherEntry.COLUMN_MIN_TEMP + }, + // the textviews to fill with the data pulled from the columns above + new int[]{R.id.list_item_date_textview, + R.id.list_item_forecast_textview, + R.id.list_item_high_textview, + R.id.list_item_low_textview + }, + 0 ); View rootView = inflater.inflate(R.layout.fragment_main, container, false); @@ -99,9 +155,9 @@ public class ForecastFragment extends Fragment { @Override public void onItemClick(AdapterView adapterView, View view, int position, long l) { - String forecast = mForecastAdapter.getItem(position); + // String forecast = mForecastAdapter.getItem(position); Intent intent = new Intent(getActivity(), DetailActivity.class) - .putExtra(Intent.EXTRA_TEXT, forecast); + .putExtra(Intent.EXTRA_TEXT, "placeholder"); startActivity(intent); } }); @@ -114,5 +170,42 @@ public class ForecastFragment extends Fragment { updateWeather(); } + @Override + public Loader onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // fragment only uses one loader, so we don't care about checking the id. + + // To only show current and future dates, get the String representation for today, + // and filter the query to return weather only for dates after or including today. + // Only return data after today. + String startDate = WeatherContract.getDbDateString(new Date()); + + // Sort order: Ascending, by date. + String sortOrder = WeatherEntry.COLUMN_DATETEXT + " ASC"; + + mLocation = Utility.getPreferredLocation(getActivity()); + Uri weatherForLocationUri = WeatherEntry.buildWeatherLocationWithStartDate( + mLocation, startDate); + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader( + getActivity(), + weatherForLocationUri, + FORECAST_COLUMNS, + null, + null, + sortOrder + ); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mForecastAdapter.swapCursor(data); + } + @Override + public void onLoaderReset(Loader loader) { + mForecastAdapter.swapCursor(null); + } } diff --git a/app/src/main/java/uk/me/njae/sunshine/Utility.java b/app/src/main/java/uk/me/njae/sunshine/Utility.java index eb2b37a..64ddc0a 100644 --- a/app/src/main/java/uk/me/njae/sunshine/Utility.java +++ b/app/src/main/java/uk/me/njae/sunshine/Utility.java @@ -18,11 +18,44 @@ package uk.me.njae.sunshine; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.Log; + +import java.text.DateFormat; +import java.util.Date; + +import uk.me.njae.sunshine.data.WeatherContract; public class Utility { + private static final String LOG_TAG = Utility.class.getSimpleName(); + 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); + Log.v(LOG_TAG, "Units set to " + prefs.getString(context.getString(R.string.pref_units_key), + context.getString(R.string.pref_units_metric))); + 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(double temperature, boolean isMetric) { + double temp; + if ( !isMetric ) { + temp = 9*temperature/5+32; + } else { + temp = temperature; + } + return String.format("%.0f", temp); + } + + static String formatDate(String dateString) { + Date date = WeatherContract.getDateFromDb(dateString); + return DateFormat.getDateInstance().format(date); + } } + 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 6555465..6afd5f3 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 @@ -4,6 +4,7 @@ import android.content.ContentUris; import android.net.Uri; import android.provider.BaseColumns; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -38,6 +39,22 @@ public class WeatherContract { return sdf.format(date); } + /** + * Converts a dateText to a long Unix time representation + * @param dateText the input date string + * @return the Date object + */ + public static Date getDateFromDb(String dateText) { + SimpleDateFormat dbDateFormat = new SimpleDateFormat(DATE_FORMAT); + try { + return dbDateFormat.parse(dateText); + } catch ( ParseException e ) { + e.printStackTrace(); + return null; + } + } + + // 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, diff --git a/app/src/main/res/layout/list_item_forecast.xml b/app/src/main/res/layout/list_item_forecast.xml index aa01480..5d5b268 100644 --- a/app/src/main/res/layout/list_item_forecast.xml +++ b/app/src/main/res/layout/list_item_forecast.xml @@ -1,13 +1,47 @@ - - + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeight" + android:padding="16dp" + > + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 817a855..51dfce4 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2,7 +2,7 @@ - + @string/pref_units_label_metric @string/pref_units_label_imperial diff --git a/app/src/main/res/values/strings_activity_settings.xml b/app/src/main/res/values/strings_activity_settings.xml index 2ef8747..46db135 100644 --- a/app/src/main/res/values/strings_activity_settings.xml +++ b/app/src/main/res/values/strings_activity_settings.xml @@ -13,7 +13,7 @@ General - Temperature units + Temperature units units Metric Imperial diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index d8e9c67..06a7390 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -17,13 +17,10 @@ dismiss it. --> + android:entries="@array/pref_units_options" /> -- 2.34.1