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 java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
-import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;
*/
-public class FetchWeatherTask extends AsyncTask<String, Void, String[]> {
+public class FetchWeatherTask extends AsyncTask<String, Void, Void> {
private final String LOG_TAG = FetchWeatherTask.class.getSimpleName();
- private ArrayAdapter<String> mForecastAdapter;
private final Context mContext;
- public FetchWeatherTask(Context context, ArrayAdapter<String> 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;
}
/**
*/
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(
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);
* 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 {
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);
// Get and insert the new weather information into the database
Vector<ContentValues> cVVector = new Vector<ContentValues>(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.
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) {
}
try {
- return getWeatherDataFromJson(forecastJsonStr, numDays, locationQuery);
+ getWeatherDataFromJson(forecastJsonStr, numDays, locationQuery);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
e.printStackTrace();
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
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;
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<Cursor> {
private final String LOG_TAG = ForecastFragment.class.getSimpleName();
- private ArrayAdapter<String> 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() {
}
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<String>(
+ // 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<String>() // 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);
@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);
}
});
updateWeather();
}
+ @Override
+ public Loader<Cursor> 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<Cursor> loader, Cursor data) {
+ mForecastAdapter.swapCursor(data);
+ }
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ mForecastAdapter.swapCursor(null);
+ }
}
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);
+ }
}
+
import android.net.Uri;
import android.provider.BaseColumns;
+import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
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,
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical" android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/list_item_forecast_textview"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:layout_gravity="center_vertical" />
+ 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"
+ >
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/list_item_date_textview" />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:text=" - " />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/list_item_forecast_textview" />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:text=" - " />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/list_item_high_textview" />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:text="/" />
+
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/list_item_low_textview" />
+
</LinearLayout>
\ No newline at end of file
<resources>
-<string-array name="pref_units_labels">
+<string-array name="pref_units_options">
<item>@string/pref_units_label_metric</item>
<item>@string/pref_units_label_imperial</item>
</string-array>
<!-- Example General settings -->
<string name="pref_header_general">General</string>
- <string name="pref_units_title">Temperature units</string>
+ <string name="pref_units_label">Temperature units</string>
<string name="pref_units_key">units</string>
<string name="pref_units_label_metric">Metric</string>
<string name="pref_units_label_imperial">Imperial</string>
dismiss it. -->
<!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
<ListPreference
+ android:title="@string/pref_units_label"
android:key="@string/pref_units_key"
- android:title="@string/pref_units_title"
- android:defaultValue="@string/pref_units_default"
- android:entries="@array/pref_units_labels"
+ android:defaultValue="@string/pref_units_metric"
android:entryValues="@array/pref_units_values"
- android:summary="%s"
- android:negativeButtonText="@null"
- android:positiveButtonText="@null" />
+ android:entries="@array/pref_units_options" />
</PreferenceScreen>