From e9f367e864077c6f3bf942f98b08d96e9fcd9c6f Mon Sep 17 00:00:00 2001 From: Lyla Date: Mon, 15 Dec 2014 19:58:10 -0800 Subject: [PATCH 01/34] 1.02 Add list item forecast layout --- app/src/main/res/layout/list_item_forecast.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/res/layout/list_item_forecast.xml diff --git a/app/src/main/res/layout/list_item_forecast.xml b/app/src/main/res/layout/list_item_forecast.xml new file mode 100644 index 000000000..965bd5714 --- /dev/null +++ b/app/src/main/res/layout/list_item_forecast.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file From fab554bb01c1edc403663ba1863d191c6a22cefe Mon Sep 17 00:00:00 2001 From: Lyla Date: Mon, 15 Dec 2014 19:58:35 -0800 Subject: [PATCH 02/34] 1.03 Update fragment layout --- app/src/main/res/layout/fragment_main.xml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index bb3dd4604..3fb5f3c1d 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -1,12 +1,18 @@ - - - + + + From de032c1248df4dcf2eb23dc692e8ecfdf01f0220 Mon Sep 17 00:00:00 2001 From: Lyla Date: Mon, 15 Dec 2014 17:34:22 -0800 Subject: [PATCH 03/34] 1.04 Add dummy data --- .../android/sunshine/app/MainActivity.java | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 348cf7efb..deb5421a4 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -1,14 +1,34 @@ +/* + * 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.support.v7.app.ActionBarActivity; -import android.support.v4.app.Fragment; import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + public class MainActivity extends ActionBarActivity { @@ -55,7 +75,19 @@ public PlaceholderFragment() { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { + + // Create some dummy data for the ListView. Here's a sample weekly forecast + String[] data = { + "Mon 6/23 - Sunny - 31/17", + "Tue 6/24 - Foggy - 21/8", + "Wed 6/25 - Cloudy - 22/17", + "Thurs 6/26 - Rainy - 18/11", + "Fri 6/27 - Foggy - 21/10", + "Sat 6/28 - TRAPPED IN WEATHERSTATION - 23/18", + "Sun 6/29 - Sunny - 20/7" + }; + List weekForecast = new ArrayList(Arrays.asList(data)); View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } From 0281f77274581dec4d93dfb0faff26d9c8b1bf8e Mon Sep 17 00:00:00 2001 From: Lyla Date: Mon, 15 Dec 2014 16:20:42 -0800 Subject: [PATCH 04/34] 1.05 Create ArrayAdapter to eventually use to populate the ListView --- .../android/sunshine/app/MainActivity.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index deb5421a4..beb1e2bd4 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -23,13 +23,11 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; - +import android.widget.ArrayAdapter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - - public class MainActivity extends ActionBarActivity { @Override @@ -70,6 +68,8 @@ public boolean onOptionsItemSelected(MenuItem item) { */ public static class PlaceholderFragment extends Fragment { + ArrayAdapter mForecastAdapter; + public PlaceholderFragment() { } @@ -88,6 +88,18 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, "Sun 6/29 - Sunny - 20/7" }; List weekForecast = new ArrayList(Arrays.asList(data)); + + + // Now that we have some dummy forecast data, create an ArrayAdapter. + // The ArrayAdapter will take data from a source (like our dummy forecast) and + // use it to populate the ListView it's attached to. + mForecastAdapter = + new ArrayAdapter( + getActivity(), // The current context (this activity) + R.layout.list_item_forecast, // The name of the layout ID. + R.id.list_item_forecast_textview, // The ID of the textview to populate. + weekForecast); + View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } From 1d424934d97c8c772978837c0eebb292cbc7f808 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 13:38:45 -0800 Subject: [PATCH 05/34] 1.06 Hook the adapter up to the ListView --- .../java/com/example/android/sunshine/app/MainActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index beb1e2bd4..80b7e8967 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -24,6 +24,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.ListView; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -101,6 +102,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, weekForecast); View rootView = inflater.inflate(R.layout.fragment_main, container, false); + + // Get a reference to the ListView, and attach this adapter to it. + ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); + listView.setAdapter(mForecastAdapter); + return rootView; } } From ea6e195702dcb3e1ac1d9abff0242a2ab0c4a785 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 13:41:12 -0800 Subject: [PATCH 06/34] 2.01 Add the network call code --- .../android/sunshine/app/MainActivity.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 80b7e8967..0e4db7b33 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -18,6 +18,7 @@ import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.ActionBarActivity; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -25,6 +26,12 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; +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.ArrayList; import java.util.Arrays; import java.util.List; @@ -107,6 +114,65 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); listView.setAdapter(mForecastAdapter); + // 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; + + try { + // Construct the URL for the OpenWeatherMap query + // Possible parameters are avaiable at OWM's forecast API page, at + // http://openweathermap.org/API#forecast + URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); + + // 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("PlaceholderFragment", "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("PlaceholderFragment", "Error closing stream", e); + } + } + } + return rootView; } } From 370d9bf15029062dc3c451be61e6b794a15aec43 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 13:43:38 -0800 Subject: [PATCH 07/34] 2.02 rename Placeholder -> Forecast --- .../com/example/android/sunshine/app/MainActivity.java | 10 +++++----- app/src/main/res/layout/fragment_main.xml | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 0e4db7b33..d764c99a9 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -44,7 +44,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() - .add(R.id.container, new PlaceholderFragment()) + .add(R.id.container, new ForecastFragment()) .commit(); } } @@ -74,11 +74,11 @@ public boolean onOptionsItemSelected(MenuItem item) { /** * A placeholder fragment containing a simple view. */ - public static class PlaceholderFragment extends Fragment { + public static class ForecastFragment extends Fragment { ArrayAdapter mForecastAdapter; - public PlaceholderFragment() { + public ForecastFragment() { } @Override @@ -156,7 +156,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, } forecastJsonStr = buffer.toString(); } catch (IOException e) { - Log.e("PlaceholderFragment", "Error ", e); + Log.e("ForecastFragment", "Error ", e); // If the code didn't successfully get the weather data, there's no point in attemping // to parse it. return null; @@ -168,7 +168,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, try { reader.close(); } catch (final IOException e) { - Log.e("PlaceholderFragment", "Error closing stream", e); + Log.e("ForecastFragment", "Error closing stream", e); } } } diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 3fb5f3c1d..372b60d63 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -7,8 +7,7 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" - tools:context=".MainActivity$PlaceholderFragment"> - + tools:context=".MainActivity$ForecastFragment"> Date: Wed, 17 Dec 2014 13:46:41 -0800 Subject: [PATCH 08/34] 2.02 refactor ForecastFragment into to separate file --- .../sunshine/app/ForecastFragment.java | 140 ++++++++++++++++++ .../android/sunshine/app/MainActivity.java | 122 --------------- 2 files changed, 140 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java new file mode 100644 index 000000000..054c4bf0f --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -0,0 +1,140 @@ +/* + * 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.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +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.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Encapsulates fetching the forecast and displaying it as a {@link ListView} layout. + */ +public class ForecastFragment extends Fragment { + + private ArrayAdapter mForecastAdapter; + + public ForecastFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + // Create some dummy data for the ListView. Here's a sample weekly forecast + String[] data = { + "Mon 6/23 - Sunny - 31/17", + "Tue 6/24 - Foggy - 21/8", + "Wed 6/25 - Cloudy - 22/17", + "Thurs 6/26 - Rainy - 18/11", + "Fri 6/27 - Foggy - 21/10", + "Sat 6/28 - TRAPPED IN WEATHERSTATION - 23/18", + "Sun 6/29 - Sunny - 20/7" + }; + List weekForecast = new ArrayList(Arrays.asList(data)); + + // Now that we have some dummy forecast data, create an ArrayAdapter. + // The ArrayAdapter will take data from a source (like our dummy forecast) and + // use it to populate the ListView it's attached to. + mForecastAdapter = + new ArrayAdapter( + getActivity(), // The current context (this activity) + R.layout.list_item_forecast, // The name of the layout ID. + R.id.list_item_forecast_textview, // The ID of the textview to populate. + weekForecast); + + View rootView = inflater.inflate(R.layout.fragment_main, container, false); + + // Get a reference to the ListView, and attach this adapter to it. + ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); + listView.setAdapter(mForecastAdapter); + + // 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; + + try { + // Construct the URL for the OpenWeatherMap query + // Possible parameters are avaiable at OWM's forecast API page, at + // http://openweathermap.org/API#forecast + URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); + + // 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("ForecastFragment", "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("ForecastFragment", "Error closing stream", e); + } + } + } + + return rootView; + } +} diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index d764c99a9..e8729e77d 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -16,25 +16,9 @@ package com.example.android.sunshine.app; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v7.app.ActionBarActivity; -import android.util.Log; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -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.ArrayList; -import java.util.Arrays; -import java.util.List; public class MainActivity extends ActionBarActivity { @@ -70,110 +54,4 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - - /** - * A placeholder fragment containing a simple view. - */ - public static class ForecastFragment extends Fragment { - - ArrayAdapter mForecastAdapter; - - public ForecastFragment() { - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - - // Create some dummy data for the ListView. Here's a sample weekly forecast - String[] data = { - "Mon 6/23 - Sunny - 31/17", - "Tue 6/24 - Foggy - 21/8", - "Wed 6/25 - Cloudy - 22/17", - "Thurs 6/26 - Rainy - 18/11", - "Fri 6/27 - Foggy - 21/10", - "Sat 6/28 - TRAPPED IN WEATHERSTATION - 23/18", - "Sun 6/29 - Sunny - 20/7" - }; - List weekForecast = new ArrayList(Arrays.asList(data)); - - - // Now that we have some dummy forecast data, create an ArrayAdapter. - // The ArrayAdapter will take data from a source (like our dummy forecast) and - // use it to populate the ListView it's attached to. - mForecastAdapter = - new ArrayAdapter( - getActivity(), // The current context (this activity) - R.layout.list_item_forecast, // The name of the layout ID. - R.id.list_item_forecast_textview, // The ID of the textview to populate. - weekForecast); - - View rootView = inflater.inflate(R.layout.fragment_main, container, false); - - // Get a reference to the ListView, and attach this adapter to it. - ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); - listView.setAdapter(mForecastAdapter); - - // 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; - - try { - // Construct the URL for the OpenWeatherMap query - // Possible parameters are avaiable at OWM's forecast API page, at - // http://openweathermap.org/API#forecast - URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); - - // 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("ForecastFragment", "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("ForecastFragment", "Error closing stream", e); - } - } - } - - return rootView; - } - } } From 1e4314fb371b6d92400f34d54a77ad5f158fe36d Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Mon, 2 Jun 2014 16:59:55 -0700 Subject: [PATCH 09/34] 2.02 Refactor logging string into constant --- .../sunshine/app/ForecastFragment.java | 120 ++++++++++-------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 054c4bf0f..50cc234fc 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -15,6 +15,7 @@ */ package com.example.android.sunshine.app; +import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; @@ -76,65 +77,74 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast); listView.setAdapter(mForecastAdapter); - // 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; - - try { - // Construct the URL for the OpenWeatherMap query - // Possible parameters are avaiable at OWM's forecast API page, at - // http://openweathermap.org/API#forecast - URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); - - // 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"); - } + return rootView; + } + + public class FetchWeatherTask extends AsyncTask { + + private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); + + @Override + protected Void doInBackground(Void... params) { + // 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; + + try { + // Construct the URL for the OpenWeatherMap query + // Possible parameters are avaiable at OWM's forecast API page, at + // http://openweathermap.org/API#forecast + URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); + + // 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. + 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; - } - forecastJsonStr = buffer.toString(); - } catch (IOException e) { - Log.e("ForecastFragment", "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("ForecastFragment", "Error closing stream", e); + } 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; } - - return rootView; } } From fa5e923fac2b38edb8f2b8db1c93a57725da9da5 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Wed, 28 May 2014 11:08:11 -0700 Subject: [PATCH 10/34] 2.03 Add xml for refresh menu item --- app/src/main/res/menu/forecastfragment.xml | 8 ++++++++ app/src/main/res/values/strings.xml | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/menu/forecastfragment.xml diff --git a/app/src/main/res/menu/forecastfragment.xml b/app/src/main/res/menu/forecastfragment.xml new file mode 100644 index 000000000..50bd1256d --- /dev/null +++ b/app/src/main/res/menu/forecastfragment.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec03bdf4d..94bda40e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,21 @@ + Sunshine - Hello world! + + Settings + + Refresh + Hello world! + From f7f49a8085563095510dab8ea1f81cac8b00fa9e Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Fri, 30 May 2014 13:42:32 -0700 Subject: [PATCH 11/34] 2.04 Inflate menu --- .../sunshine/app/ForecastFragment.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 50cc234fc..2e611d2ec 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -20,6 +20,9 @@ import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; @@ -45,6 +48,30 @@ public class ForecastFragment extends Fragment { public ForecastFragment() { } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Add this line in order for this fragment to handle menu events. + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.forecastfragment, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + if (id == R.id.action_refresh) { + return true; + } + return super.onOptionsItemSelected(item); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { From f18784f490e71f6816fa1fe5fb9b5dafff38b93d Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Wed, 28 May 2014 12:50:07 -0700 Subject: [PATCH 12/34] 2.05 Execute FetchWeatherTask --- .../java/com/example/android/sunshine/app/ForecastFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 2e611d2ec..f30d0cc0a 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -67,6 +67,8 @@ public boolean onOptionsItemSelected(MenuItem item) { // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_refresh) { + FetchWeatherTask weatherTask = new FetchWeatherTask(); + weatherTask.execute(); return true; } return super.onOptionsItemSelected(item); From d1579a4199da55c05b8e71ff2a5f1f404064d88c Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Fri, 30 May 2014 13:08:08 -0700 Subject: [PATCH 13/34] 2.06 Add Internet permission --- app/src/main/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fcad107a6..54ce75263 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + Date: Thu, 29 May 2014 00:45:54 -0700 Subject: [PATCH 14/34] 2.07 Build URL with params --- .../sunshine/app/ForecastFragment.java | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index f30d0cc0a..371d2d36d 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -15,6 +15,7 @@ */ package com.example.android.sunshine.app; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -68,7 +69,7 @@ public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh) { FetchWeatherTask weatherTask = new FetchWeatherTask(); - weatherTask.execute(); + weatherTask.execute("94043"); return true; } return super.onOptionsItemSelected(item); @@ -109,12 +110,18 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - public class FetchWeatherTask extends AsyncTask { + public class FetchWeatherTask extends AsyncTask { private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); @Override - protected Void doInBackground(Void... 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) { + 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; @@ -123,11 +130,31 @@ protected Void doInBackground(Void... params) { // Will contain the raw JSON response as a string. String forecastJsonStr = null; + 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 - URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7"); + 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()); + + Log.v(LOG_TAG, "Built URI " + builtUri.toString()); // Create the request to OpenWeatherMap, and open the connection urlConnection = (HttpURLConnection) url.openConnection(); From c281fef55c00909c6b8072e7c71800a5c9a28cf3 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Thu, 29 May 2014 02:13:02 -0700 Subject: [PATCH 15/34] 2.08 Add in JSON parsing code --- .../sunshine/app/ForecastFragment.java | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 371d2d36d..b206c64c5 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -19,6 +19,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -29,12 +30,17 @@ import android.widget.ArrayAdapter; import android.widget.ListView; +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.ArrayList; import java.util.Arrays; import java.util.List; @@ -110,12 +116,110 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - public class FetchWeatherTask extends AsyncTask { + public class FetchWeatherTask extends AsyncTask { private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); + /* 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. + SimpleDateFormat shortenedDateFormat = new SimpleDateFormat("EEE MMM dd"); + return shortenedDateFormat.format(time); + } + + /** + * Prepare the weather high/lows for presentation. + */ + private String formatHighLows(double high, double low) { + // 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; + } + + /** + * 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_DESCRIPTION = "main"; + + JSONObject forecastJson = new JSONObject(forecastJsonStr); + JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST); + + // OWM returns daily forecasts based upon the local time of the city that is being + // asked for, which means that we need to know the GMT offset to translate this data + // properly. + + // Since this data is also sent in-order and the first day is always the + // current day, we're going to take advantage of that to get a nice + // normalized UTC date for all of our weather. + + Time dayTime = new Time(); + dayTime.setToNow(); + + // we start at the day returned by local time. Otherwise this is a mess. + int julianStartDay = Time.getJulianDay(System.currentTimeMillis(), dayTime.gmtoff); + + // now we work exclusively in UTC + dayTime = new Time(); + + 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; + // Cheating to convert this to UTC time, which is what we want anyhow + dateTime = dayTime.setJulianDay(julianStartDay+i); + 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; + + } @Override - protected Void doInBackground(String... params) { + 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) { @@ -183,6 +287,8 @@ protected Void doInBackground(String... params) { return null; } forecastJsonStr = buffer.toString(); + + Log.v(LOG_TAG, "Forecast string: " + forecastJsonStr); } 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 @@ -200,6 +306,15 @@ protected Void doInBackground(String... params) { } } } + + try { + return getWeatherDataFromJson(forecastJsonStr, numDays); + } 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; } } From c8542c316a3333cfc3ae60e51205f2c5e32c0fa6 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Thu, 29 May 2014 02:32:24 -0700 Subject: [PATCH 16/34] 2.09 Update the adapter so that the data is displayed --- .../android/sunshine/app/ForecastFragment.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index b206c64c5..3ae18a0ad 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -317,5 +317,16 @@ protected String[] doInBackground(String... params) { // 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! + } + } } } From 2141b9c820980041c1026695dde50ee01438733f Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Wed, 4 Jun 2014 17:20:21 -0700 Subject: [PATCH 17/34] 3.00 remove verbose logging statements --- .../example/android/sunshine/app/ForecastFragment.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 3ae18a0ad..6fa3fbc59 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -211,10 +211,6 @@ private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays) highAndLow = formatHighLows(high, low); resultStrs[i] = day + " - " + description + " - " + highAndLow; } - - for (String s : resultStrs) { - Log.v(LOG_TAG, "Forecast entry: " + s); - } return resultStrs; } @@ -258,8 +254,6 @@ protected String[] doInBackground(String... params) { 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"); @@ -287,8 +281,6 @@ protected String[] doInBackground(String... params) { return null; } forecastJsonStr = buffer.toString(); - - Log.v(LOG_TAG, "Forecast string: " + forecastJsonStr); } 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 From bd57648b50360b55b86f0edec1162bb381bd1c80 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Fri, 30 May 2014 14:04:25 -0700 Subject: [PATCH 18/34] 3.01 Add toast on item click --- .../example/android/sunshine/app/ForecastFragment.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 6fa3fbc59..25c838fe4 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -27,8 +27,10 @@ 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 android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; @@ -112,6 +114,14 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // Get a reference to the ListView, and attach this adapter to it. 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); + Toast.makeText(getActivity(), forecast, Toast.LENGTH_SHORT).show(); + } + }); return rootView; } From e8e638d75c648f1f2db9b660ed81092749399715 Mon Sep 17 00:00:00 2001 From: Lyla Date: Tue, 16 Dec 2014 16:06:28 -0800 Subject: [PATCH 19/34] 3.02 Create new DetailActivity --- app/src/main/AndroidManifest.xml | 8 ++ .../android/sunshine/app/DetailActivity.java | 80 +++++++++++++++++++ app/src/main/res/layout/activity_detail.xml | 5 ++ app/src/main/res/layout/fragment_detail.xml | 12 +++ app/src/main/res/menu/detail.xml | 7 ++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 113 insertions(+) create mode 100644 app/src/main/java/com/example/android/sunshine/app/DetailActivity.java create mode 100644 app/src/main/res/layout/activity_detail.xml create mode 100644 app/src/main/res/layout/fragment_detail.xml create mode 100644 app/src/main/res/menu/detail.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 54ce75263..64a14ce2b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,14 @@ + + + diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java new file mode 100644 index 000000000..5c46d42b2 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -0,0 +1,80 @@ +/* + * 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.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +public class DetailActivity extends ActionBarActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_detail); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.container, new PlaceholderFragment()) + .commit(); + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.detail, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * A placeholder fragment containing a simple view. + */ + public static class PlaceholderFragment extends Fragment { + + public PlaceholderFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View rootView = inflater.inflate(R.layout.fragment_detail, container, false); + return rootView; + } + } +} diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml new file mode 100644 index 000000000..3ecaa61f8 --- /dev/null +++ b/app/src/main/res/layout/activity_detail.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml new file mode 100644 index 000000000..a5f512267 --- /dev/null +++ b/app/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/menu/detail.xml b/app/src/main/res/menu/detail.xml new file mode 100644 index 000000000..6e04aa16a --- /dev/null +++ b/app/src/main/res/menu/detail.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94bda40e4..d1fb7f761 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,5 +17,6 @@ Refresh Hello world! + DetailActivity From 4a61c5a65e0fb35bd6859e336754194dc43373a5 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 20:04:06 -0800 Subject: [PATCH 20/34] 3.03 Launch DetailActivity by initiating an explicit intent --- .../com/example/android/sunshine/app/DetailActivity.java | 4 ++-- .../com/example/android/sunshine/app/ForecastFragment.java | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java index 5c46d42b2..14b681a99 100644 --- a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -16,9 +16,9 @@ package com.example.android.sunshine.app; -import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v7.app.ActionBarActivity; +import android.support.v4.app.Fragment; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 25c838fe4..b2284abd6 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -15,6 +15,7 @@ */ package com.example.android.sunshine.app; +import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -30,7 +31,6 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; -import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; @@ -119,7 +119,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, @Override public void onItemClick(AdapterView adapterView, View view, int position, long l) { String forecast = mForecastAdapter.getItem(position); - Toast.makeText(getActivity(), forecast, Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(getActivity(), DetailActivity.class) + .putExtra(Intent.EXTRA_TEXT, forecast); + startActivity(intent); } }); From 0d060ffebcb6b6eec2501a352d3b76dee9d10d05 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 20:06:06 -0800 Subject: [PATCH 21/34] 3.04 Get the forecast from the intent and use it to populate the detail text --- .../android/sunshine/app/DetailActivity.java | 19 +++++++++++++++---- app/src/main/res/layout/fragment_detail.xml | 3 ++- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java index 14b681a99..c934a5eaf 100644 --- a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -17,13 +17,15 @@ package com.example.android.sunshine.app; import android.support.v7.app.ActionBarActivity; -import android.support.v4.app.Fragment; +import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; public class DetailActivity extends ActionBarActivity { @@ -33,7 +35,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_detail); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() - .add(R.id.container, new PlaceholderFragment()) + .add(R.id.container, new DetailFragment()) .commit(); } } @@ -64,9 +66,9 @@ public boolean onOptionsItemSelected(MenuItem item) { /** * A placeholder fragment containing a simple view. */ - public static class PlaceholderFragment extends Fragment { + public static class DetailFragment extends Fragment { - public PlaceholderFragment() { + public DetailFragment() { } @Override @@ -74,6 +76,15 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_detail, container, false); + + // The detail Activity called via intent. Inspect the intent for forecast data. + Intent intent = getActivity().getIntent(); + if (intent != null && intent.hasExtra(Intent.EXTRA_TEXT)) { + String forecastStr = intent.getStringExtra(Intent.EXTRA_TEXT); + ((TextView) rootView.findViewById(R.id.detail_text)) + .setText(forecastStr); + } + return rootView; } } diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index a5f512267..45e7df4b2 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -4,9 +4,10 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" - tools:context="com.example.android.sunshine.app.DetailActivity$PlaceholderFragment"> + tools:context="com.example.android.sunshine.app.DetailActivity.DetailFragment"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d1fb7f761..d8d8fd38c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Refresh + Details Hello world! - DetailActivity From 296319fa21f7cd69f8642e9bca014f3ae68c9ea9 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Fri, 30 May 2014 17:46:52 -0700 Subject: [PATCH 22/34] 3.05 Add SettingsActivity and associated entry in the manifest --- app/src/main/AndroidManifest.xml | 8 ++ .../sunshine/app/SettingsActivity.java | 82 +++++++++++++++++++ app/src/main/res/layout/fragment_detail.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64a14ce2b..99c059290 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,14 @@ android:name="android.support.PARENT_ACTIVITY" android:value="com.example.android.sunshine.app.MainActivity" /> + + + diff --git a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java new file mode 100644 index 000000000..432b91d77 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java @@ -0,0 +1,82 @@ +/* + * 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.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; + +/** + * A {@link PreferenceActivity} that presents a set of application settings. + *

+ * 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 + // TODO: Add preferences from XML + + // For all preferences, attach an OnPreferenceChangeListener so the UI summary can be + // updated when the preference changes. + // TODO: Add preference + } + + /** + * 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; + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 45e7df4b2..70bef316d 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -6,7 +6,7 @@ android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.example.android.sunshine.app.DetailActivity.DetailFragment"> - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8d8fd38c..3bf6d57e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,6 @@ Refresh Details - Hello world! + Settings From fd1244173695bf7344e1bcab49b94035adfaed34 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Fri, 30 May 2014 17:49:22 -0700 Subject: [PATCH 23/34] 3.06 Launch the settings activity when the settings menu item is selected --- .../java/com/example/android/sunshine/app/DetailActivity.java | 1 + .../java/com/example/android/sunshine/app/MainActivity.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java index c934a5eaf..3d2c8e019 100644 --- a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -57,6 +57,7 @@ public boolean onOptionsItemSelected(MenuItem item) { //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); return true; } diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index e8729e77d..213d94f06 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -15,6 +15,7 @@ */ package com.example.android.sunshine.app; +import android.content.Intent; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.view.Menu; @@ -49,6 +50,7 @@ public boolean onOptionsItemSelected(MenuItem item) { //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); return true; } From d5eebe9cdcb75777e78d956408c26fb86eb19f6d Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Sat, 31 May 2014 12:16:11 -0700 Subject: [PATCH 24/34] 3.07 Add location setting xml --- app/src/main/res/values/strings.xml | 9 +++++++++ app/src/main/res/xml/pref_general.xml | 13 +++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 app/src/main/res/xml/pref_general.xml diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3bf6d57e1..abc4a1517 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,13 @@ Details Settings + + Location + + + location + + + 94043 + diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml new file mode 100644 index 000000000..12be85a95 --- /dev/null +++ b/app/src/main/res/xml/pref_general.xml @@ -0,0 +1,13 @@ + + + + + + From 5204747e9378f65bf4a474da4b5c08f61a64d37c Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Sat, 31 May 2014 12:32:05 -0700 Subject: [PATCH 25/34] 3.08 Inflate view in SettingsActivity --- .../com/example/android/sunshine/app/SettingsActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java index 432b91d77..f96b1fea7 100644 --- a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java @@ -36,11 +36,11 @@ public class SettingsActivity extends PreferenceActivity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Add 'general' preferences, defined in the XML file - // TODO: Add preferences from XML + addPreferencesFromResource(R.xml.pref_general); // For all preferences, attach an OnPreferenceChangeListener so the UI summary can be // updated when the preference changes. - // TODO: Add preference + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key))); } /** From a01a0726b0b4e50661ab2c1e8d23ef1c359b1c5a Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Sat, 31 May 2014 13:25:40 -0700 Subject: [PATCH 26/34] 3.09 Get location from shared prefs --- .../com/example/android/sunshine/app/ForecastFragment.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index b2284abd6..ac507d162 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -16,9 +16,11 @@ package com.example.android.sunshine.app; 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; import android.text.format.Time; import android.util.Log; @@ -77,7 +79,10 @@ public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh) { FetchWeatherTask weatherTask = new FetchWeatherTask(); - weatherTask.execute("94043"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String location = prefs.getString(getString(R.string.pref_location_key), + getString(R.string.pref_location_default)); + weatherTask.execute(location); return true; } return super.onOptionsItemSelected(item); From 29db009fd86979be2f9aa502ff1b31d6d229aa99 Mon Sep 17 00:00:00 2001 From: Lyla Date: Tue, 16 Dec 2014 13:29:34 -0800 Subject: [PATCH 27/34] 3.10 Refactor fetching weather data so we don't have to stare at the dummy data --- .../sunshine/app/ForecastFragment.java | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index ac507d162..f1c4f4fcd 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -46,8 +46,6 @@ import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; /** * Encapsulates fetching the forecast and displaying it as a {@link ListView} layout. @@ -78,11 +76,7 @@ public boolean onOptionsItemSelected(MenuItem item) { // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_refresh) { - FetchWeatherTask weatherTask = new FetchWeatherTask(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String location = prefs.getString(getString(R.string.pref_location_key), - getString(R.string.pref_location_default)); - weatherTask.execute(location); + updateWeather(); return true; } return super.onOptionsItemSelected(item); @@ -92,27 +86,14 @@ public boolean onOptionsItemSelected(MenuItem item) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // Create some dummy data for the ListView. Here's a sample weekly forecast - String[] data = { - "Mon 6/23 - Sunny - 31/17", - "Tue 6/24 - Foggy - 21/8", - "Wed 6/25 - Cloudy - 22/17", - "Thurs 6/26 - Rainy - 18/11", - "Fri 6/27 - Foggy - 21/10", - "Sat 6/28 - TRAPPED IN WEATHERSTATION - 23/18", - "Sun 6/29 - Sunny - 20/7" - }; - List weekForecast = new ArrayList(Arrays.asList(data)); - - // Now that we have some dummy forecast data, create an ArrayAdapter. - // The ArrayAdapter will take data from a source (like our dummy forecast) and + // The ArrayAdapter will take data from a source and // use it to populate the ListView it's attached to. mForecastAdapter = new ArrayAdapter( getActivity(), // The current context (this activity) R.layout.list_item_forecast, // The name of the layout ID. R.id.list_item_forecast_textview, // The ID of the textview to populate. - weekForecast); + new ArrayList()); View rootView = inflater.inflate(R.layout.fragment_main, container, false); @@ -133,6 +114,20 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon return rootView; } + private void updateWeather() { + FetchWeatherTask weatherTask = new FetchWeatherTask(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String location = prefs.getString(getString(R.string.pref_location_key), + getString(R.string.pref_location_default)); + weatherTask.execute(location); + } + + @Override + public void onStart() { + super.onStart(); + updateWeather(); + } + public class FetchWeatherTask extends AsyncTask { private final String LOG_TAG = FetchWeatherTask.class.getSimpleName(); From cdd3f2ccabb71946ad5563aad6950ce59e041122 Mon Sep 17 00:00:00 2001 From: Sarah Spikes Date: Wed, 4 Jun 2014 17:21:38 -0700 Subject: [PATCH 28/34] 3.11 Add fully functional units setting --- .../sunshine/app/ForecastFragment.java | 24 +++++++++++++++++-- .../sunshine/app/SettingsActivity.java | 1 + app/src/main/res/values/arrays.xml | 13 ++++++++++ app/src/main/res/values/strings.xml | 18 ++++++++++++++ app/src/main/res/xml/pref_general.xml | 7 ++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/values/arrays.xml diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index f1c4f4fcd..7b5ada34d 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -145,7 +145,15 @@ private String getReadableDateString(long time){ /** * Prepare the weather high/lows for presentation. */ - private String formatHighLows(double high, double low) { + private String formatHighLows(double high, double low, String unitType) { + + if (unitType.equals(getString(R.string.pref_units_imperial))) { + high = (high * 1.8) + 32; + low = (low * 1.8) + 32; + } else if (!unitType.equals(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); @@ -193,6 +201,18 @@ private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays) dayTime = new Time(); String[] resultStrs = new String[numDays]; + + // 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(getActivity()); + String unitType = sharedPrefs.getString( + getString(R.string.pref_units_key), + getString(R.string.pref_units_metric)); + for(int i = 0; i < weatherArray.length(); i++) { // For now, using the format "Day, description, hi/low" String day; @@ -220,7 +240,7 @@ private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays) double high = temperatureObject.getDouble(OWM_MAX); double low = temperatureObject.getDouble(OWM_MIN); - highAndLow = formatHighLows(high, low); + highAndLow = formatHighLows(high, low, unitType); resultStrs[i] = day + " - " + description + " - " + highAndLow; } return resultStrs; diff --git a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java index f96b1fea7..98dac5661 100644 --- a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java @@ -41,6 +41,7 @@ public void onCreate(Bundle savedInstanceState) { // 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))); } /** diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..f752cb333 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,13 @@ + + + + + @string/pref_units_label_metric + @string/pref_units_label_imperial + + + + @string/pref_units_metric + @string/pref_units_imperial + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abc4a1517..6c943a20f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,22 @@ 94043 + + Temperature Units + + + Metric + + + Imperial + + + units + + + metric + + + imperial + diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 12be85a95..212b70ad3 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -10,4 +10,11 @@ android:inputType="text" android:singleLine="true" /> + + From 745a01e91230db498ae77adae3241f2edee83570 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 14:35:34 -0800 Subject: [PATCH 29/34] 3.12 Add map intent from new menu item --- .../android/sunshine/app/MainActivity.java | 34 +++++++++++++++++++ app/src/main/res/menu/detail.xml | 8 +++-- app/src/main/res/menu/main.xml | 9 +++-- app/src/main/res/values/strings.xml | 1 + 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 213d94f06..d2e5e50fe 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -16,13 +16,19 @@ package com.example.android.sunshine.app; import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v7.app.ActionBarActivity; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; public class MainActivity extends ActionBarActivity { + private final String LOG_TAG = MainActivity.class.getSimpleName(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -54,6 +60,34 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } + if (id == R.id.action_map) { + openPreferredLocationInMap(); + return true; + } return super.onOptionsItemSelected(item); } + + private void openPreferredLocationInMap() { + SharedPreferences sharedPrefs = + PreferenceManager.getDefaultSharedPreferences(this); + String location = sharedPrefs.getString( + getString(R.string.pref_location_key), + getString(R.string.pref_location_default)); + + // Using the URI scheme for showing a location found on a map. This super-handy + // intent can is detailed in the "Common Intents" page of Android's developer site: + // http://developer.android.com/guide/components/intents-common.html#Maps + Uri geoLocation = Uri.parse("geo:0,0?").buildUpon() + .appendQueryParameter("q", location) + .build(); + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(geoLocation); + + if (intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); + } else { + Log.d(LOG_TAG, "Couldn't call " + location + ", no receiving apps installed!"); + } + } } diff --git a/app/src/main/res/menu/detail.xml b/app/src/main/res/menu/detail.xml index 6e04aa16a..865ac0539 100644 --- a/app/src/main/res/menu/detail.xml +++ b/app/src/main/res/menu/detail.xml @@ -1,7 +1,9 @@

- + tools:context="com.example.android.sunshine.app.DetailActivity" > + diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index b1cb90811..87d2ed662 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -1,6 +1,11 @@ - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c943a20f..98a5d3944 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ typically from the action bar. The ActionBar is limited real estate, so shorter is better. --> Settings + Map Location Refresh From f63ba9f1873f55120696d9a1f9d203f9618daae6 Mon Sep 17 00:00:00 2001 From: Lyla Date: Wed, 17 Dec 2014 20:08:00 -0800 Subject: [PATCH 30/34] 3.13 Add ShareActionProvider to detail fragment menu --- .../android/sunshine/app/DetailActivity.java | 44 ++++++++++++++++++- app/src/main/res/menu/detailfragment.xml | 9 ++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/menu/detailfragment.xml diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java index 3d2c8e019..08b9116b2 100644 --- a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -20,8 +20,12 @@ import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.ShareActionProvider; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -69,7 +73,13 @@ public boolean onOptionsItemSelected(MenuItem item) { */ public static class DetailFragment extends Fragment { + private static final String LOG_TAG = DetailFragment.class.getSimpleName(); + + private static final String FORECAST_SHARE_HASHTAG = " #SunshineApp"; + private String mForecastStr; + public DetailFragment() { + setHasOptionsMenu(true); } @Override @@ -81,12 +91,42 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // The detail Activity called via intent. Inspect the intent for forecast data. Intent intent = getActivity().getIntent(); if (intent != null && intent.hasExtra(Intent.EXTRA_TEXT)) { - String forecastStr = intent.getStringExtra(Intent.EXTRA_TEXT); + mForecastStr = intent.getStringExtra(Intent.EXTRA_TEXT); ((TextView) rootView.findViewById(R.id.detail_text)) - .setText(forecastStr); + .setText(mForecastStr); } return rootView; } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // Inflate the menu; this adds items to the action bar if it is present. + inflater.inflate(R.menu.detailfragment, menu); + + // Retrieve the share menu item + MenuItem menuItem = menu.findItem(R.id.action_share); + + // Get the provider and hold onto it to set/change the share intent. + ShareActionProvider mShareActionProvider = + (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem); + + // Attach an intent to this ShareActionProvider. You can update this at any time, + // like when the user selects a new piece of data they might like to share. + if (mShareActionProvider != null ) { + mShareActionProvider.setShareIntent(createShareForecastIntent()); + } else { + Log.d(LOG_TAG, "Share Action Provider is null?"); + } + } + + private Intent createShareForecastIntent() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, + mForecastStr + FORECAST_SHARE_HASHTAG); + return shareIntent; + } } } diff --git a/app/src/main/res/menu/detailfragment.xml b/app/src/main/res/menu/detailfragment.xml new file mode 100644 index 000000000..bfef05f66 --- /dev/null +++ b/app/src/main/res/menu/detailfragment.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98a5d3944..14ae616c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ --> Settings Map Location + Share Refresh From 35bbc9639e068489e9fbeab706ab8fac60cc98be Mon Sep 17 00:00:00 2001 From: Lyla Date: Thu, 18 Dec 2014 14:03:02 -0800 Subject: [PATCH 31/34] 4.01 Lifecycle Events quiz adding log statements --- .../android/sunshine/app/MainActivity.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index d2e5e50fe..fc31cf2f4 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -31,6 +31,7 @@ public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { + Log.v(LOG_TAG, "in onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { @@ -90,4 +91,39 @@ private void openPreferredLocationInMap() { Log.d(LOG_TAG, "Couldn't call " + location + ", no receiving apps installed!"); } } + + @Override + protected void onStart() { + Log.v(LOG_TAG, "in onStart"); + super.onStart(); + // The activity is about to become visible. + } + + @Override + protected void onResume() { + Log.v(LOG_TAG, "in onResume"); + super.onResume(); + // The activity has become visible (it is now "resumed"). + } + + @Override + protected void onPause() { + Log.v(LOG_TAG, "in onPause"); + super.onPause(); + // Another activity is taking focus (this activity is about to be "paused"). + } + + @Override + protected void onStop() { + Log.v(LOG_TAG, "in onStop"); + super.onStop(); + // The activity is no longer visible (it is now "stopped") + } + + @Override + protected void onDestroy() { + Log.v(LOG_TAG, "in onDestroy"); + super.onDestroy(); + // The activity is about to be destroyed. + } } From 413fbdf220d53a470a9982fe595b85c33e3c149e Mon Sep 17 00:00:00 2001 From: Lyla Date: Mon, 9 Feb 2015 17:52:30 -0800 Subject: [PATCH 32/34] Revert "4.1 Lifecycle Events quiz -- add log statements to MainActivity lifecycle handlers" This reverts commit 67ef37e9eb94b389831726678e6e42e1d14f30f2. --- .../android/sunshine/app/MainActivity.java | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index fc31cf2f4..d2e5e50fe 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -31,7 +31,6 @@ public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { - Log.v(LOG_TAG, "in onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { @@ -91,39 +90,4 @@ private void openPreferredLocationInMap() { Log.d(LOG_TAG, "Couldn't call " + location + ", no receiving apps installed!"); } } - - @Override - protected void onStart() { - Log.v(LOG_TAG, "in onStart"); - super.onStart(); - // The activity is about to become visible. - } - - @Override - protected void onResume() { - Log.v(LOG_TAG, "in onResume"); - super.onResume(); - // The activity has become visible (it is now "resumed"). - } - - @Override - protected void onPause() { - Log.v(LOG_TAG, "in onPause"); - super.onPause(); - // Another activity is taking focus (this activity is about to be "paused"). - } - - @Override - protected void onStop() { - Log.v(LOG_TAG, "in onStop"); - super.onStop(); - // The activity is no longer visible (it is now "stopped") - } - - @Override - protected void onDestroy() { - Log.v(LOG_TAG, "in onDestroy"); - super.onDestroy(); - // The activity is about to be destroyed. - } } From eacae0cb2112820938b0a8c2272caf07f35a6c34 Mon Sep 17 00:00:00 2001 From: dgalpin Date: Sat, 10 Jan 2015 01:30:55 -0800 Subject: [PATCH 33/34] 4.02 Complete starting code for Lesson 4. Includes lots of important starter files. TestPractice is only temporary and will be deleted during Lesson 4. Several files are incomplete, and TestUtilities will be uncommented during lesson 4. --- .../android/sunshine/app/ApplicationTest.java | 13 -- .../android/sunshine/app/FullTestSuite.java | 32 ++++ .../android/sunshine/app/data/TestDb.java | 171 ++++++++++++++++++ .../sunshine/app/data/TestPractice.java | 33 ++++ .../sunshine/app/data/TestUtilities.java | 149 +++++++++++++++ .../sunshine/app/utils/PollingCheck.java | 73 ++++++++ .../sunshine/app/data/WeatherContract.java | 78 ++++++++ .../sunshine/app/data/WeatherDbHelper.java | 87 +++++++++ 8 files changed, 623 insertions(+), 13 deletions(-) delete mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/ApplicationTest.java create mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/FullTestSuite.java create mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/data/TestDb.java create mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/data/TestPractice.java create mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/data/TestUtilities.java create mode 100644 app/src/androidTest/java/com/example/android/sunshine/app/utils/PollingCheck.java create mode 100644 app/src/main/java/com/example/android/sunshine/app/data/WeatherContract.java create mode 100644 app/src/main/java/com/example/android/sunshine/app/data/WeatherDbHelper.java diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/ApplicationTest.java b/app/src/androidTest/java/com/example/android/sunshine/app/ApplicationTest.java deleted file mode 100644 index eb830ddb8..000000000 --- a/app/src/androidTest/java/com/example/android/sunshine/app/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.android.sunshine.app; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/FullTestSuite.java b/app/src/androidTest/java/com/example/android/sunshine/app/FullTestSuite.java new file mode 100644 index 000000000..0c037a1a2 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/sunshine/app/FullTestSuite.java @@ -0,0 +1,32 @@ +/* + * 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.test.suitebuilder.TestSuiteBuilder; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class FullTestSuite extends TestSuite { + public static Test suite() { + return new TestSuiteBuilder(FullTestSuite.class) + .includeAllPackagesUnderHere().build(); + } + + public FullTestSuite() { + super(); + } +} diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/data/TestDb.java b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestDb.java new file mode 100644 index 000000000..6268fabcc --- /dev/null +++ b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestDb.java @@ -0,0 +1,171 @@ +/* + * 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.test.AndroidTestCase; + +public class TestDb extends AndroidTestCase { + + public static final String LOG_TAG = TestDb.class.getSimpleName(); + + // Since we want each test to start with a clean slate + void deleteTheDatabase() { + mContext.deleteDatabase(WeatherDbHelper.DATABASE_NAME); + } + + /* + This function gets called before each test is executed to delete the database. This makes + sure that we always have a clean test. + */ + public void setUp() { + deleteTheDatabase(); + } + + /* + Students: Uncomment this test once you've written the code to create the Location + table. Note that you will have to have chosen the same column names that I did in + my solution for this test to compile, so if you haven't yet done that, this is + a good time to change your column names to match mine. + + Note that this only tests that the Location table has the correct columns, since we + give you the code for the weather table. This test does not look at the + */ +// public void testCreateDb() throws Throwable { +// // build a HashSet of all of the table names we wish to look for +// // Note that there will be another table in the DB that stores the +// // Android metadata (db version information) +// final HashSet tableNameHashSet = new HashSet(); +// tableNameHashSet.add(WeatherContract.LocationEntry.TABLE_NAME); +// tableNameHashSet.add(WeatherContract.WeatherEntry.TABLE_NAME); +// +// mContext.deleteDatabase(WeatherDbHelper.DATABASE_NAME); +// SQLiteDatabase db = new WeatherDbHelper( +// this.mContext).getWritableDatabase(); +// assertEquals(true, db.isOpen()); +// +// // have we created the tables we want? +// Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null); +// +// assertTrue("Error: This means that the database has not been created correctly", +// c.moveToFirst()); +// +// // verify that the tables have been created +// do { +// tableNameHashSet.remove(c.getString(0)); +// } while( c.moveToNext() ); +// +// // if this fails, it means that your database doesn't contain both the location entry +// // and weather entry tables +// assertTrue("Error: Your database was created without both the location entry and weather entry tables", +// tableNameHashSet.isEmpty()); +// +// // now, do our tables contain the correct columns? +// c = db.rawQuery("PRAGMA table_info(" + WeatherContract.LocationEntry.TABLE_NAME + ")", +// null); +// +// assertTrue("Error: This means that we were unable to query the database for table information.", +// c.moveToFirst()); +// +// // Build a HashSet of all of the column names we want to look for +// final HashSet locationColumnHashSet = new HashSet(); +// locationColumnHashSet.add(WeatherContract.LocationEntry._ID); +// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_CITY_NAME); +// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_COORD_LAT); +// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_COORD_LONG); +// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING); +// +// int columnNameIndex = c.getColumnIndex("name"); +// do { +// String columnName = c.getString(columnNameIndex); +// locationColumnHashSet.remove(columnName); +// } while(c.moveToNext()); +// +// // if this fails, it means that your database doesn't contain all of the required location +// // entry columns +// assertTrue("Error: The database doesn't contain all of the required location entry columns", +// locationColumnHashSet.isEmpty()); +// db.close(); +// } + + /* + Students: Here is where you will build code to test that we can insert and query the + location database. We've done a lot of work for you. You'll want to look in TestUtilities + where you can uncomment out the "createNorthPoleLocationValues" function. You can + also make use of the ValidateCurrentRecord function from within TestUtilities. + */ + public void testLocationTable() { + // First step: Get reference to writable database + + // Create ContentValues of what you want to insert + // (you can use the createNorthPoleLocationValues if you wish) + + // Insert ContentValues into database and get a row ID back + + // Query the database and receive a Cursor back + + // Move the cursor to a valid database row + + // Validate data in resulting Cursor with the original ContentValues + // (you can use the validateCurrentRecord function in TestUtilities to validate the + // query if you like) + + // Finally, close the cursor and database + + } + + /* + Students: Here is where you will build code to test that we can insert and query the + database. We've done a lot of work for you. You'll want to look in TestUtilities + where you can use the "createWeatherValues" function. You can + also make use of the validateCurrentRecord function from within TestUtilities. + */ + public void testWeatherTable() { + // First insert the location, and then use the locationRowId to insert + // the weather. Make sure to cover as many failure cases as you can. + + // Instead of rewriting all of the code we've already written in testLocationTable + // we can move this code to insertLocation and then call insertLocation from both + // tests. Why move it? We need the code to return the ID of the inserted location + // and our testLocationTable can only return void because it's a test. + + // First step: Get reference to writable database + + // Create ContentValues of what you want to insert + // (you can use the createWeatherValues TestUtilities function if you wish) + + // Insert ContentValues into database and get a row ID back + + // Query the database and receive a Cursor back + + // Move the cursor to a valid database row + + // Validate data in resulting Cursor with the original ContentValues + // (you can use the validateCurrentRecord function in TestUtilities to validate the + // query if you like) + + // Finally, close the cursor and database + } + + + /* + Students: This is a helper method for the testWeatherTable quiz. You can move your + code from testLocationTable to here so that you can call this code from both + testWeatherTable and testLocationTable. + */ + public long insertLocation() { + return -1L; + } +} diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/data/TestPractice.java b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestPractice.java new file mode 100644 index 000000000..3395d6725 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestPractice.java @@ -0,0 +1,33 @@ +package com.example.android.sunshine.app.data; + +import android.test.AndroidTestCase; + +public class TestPractice extends AndroidTestCase { + /* + This gets run before every test. + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + public void testThatDemonstratesAssertions() throws Throwable { + int a = 5; + int b = 3; + int c = 5; + int d = 10; + + assertEquals("X should be equal", a, c); + assertTrue("Y should be true", d > a); + assertFalse("Z should be false", a == b); + + if (b > d) { + fail("XX should never happen"); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } +} diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/data/TestUtilities.java b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestUtilities.java new file mode 100644 index 000000000..7cc8772ba --- /dev/null +++ b/app/src/androidTest/java/com/example/android/sunshine/app/data/TestUtilities.java @@ -0,0 +1,149 @@ +package com.example.android.sunshine.app.data; + +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.test.AndroidTestCase; + +import com.example.android.sunshine.app.utils.PollingCheck; + +import java.util.Map; +import java.util.Set; + +/* + Students: These are functions and some test data to make it easier to test your database and + Content Provider. Note that you'll want your WeatherContract class to exactly match the one + in our solution to use these as-given. + */ +public class TestUtilities extends AndroidTestCase { + static final String TEST_LOCATION = "99705"; + static final long TEST_DATE = 1419033600L; // December 20th, 2014 + + static void validateCursor(String error, Cursor valueCursor, ContentValues expectedValues) { + assertTrue("Empty cursor returned. " + error, valueCursor.moveToFirst()); + validateCurrentRecord(error, valueCursor, expectedValues); + valueCursor.close(); + } + + static void validateCurrentRecord(String error, Cursor valueCursor, ContentValues expectedValues) { + Set> valueSet = expectedValues.valueSet(); + for (Map.Entry entry : valueSet) { + String columnName = entry.getKey(); + int idx = valueCursor.getColumnIndex(columnName); + assertFalse("Column '" + columnName + "' not found. " + error, idx == -1); + String expectedValue = entry.getValue().toString(); + assertEquals("Value '" + entry.getValue().toString() + + "' did not match the expected value '" + + expectedValue + "'. " + error, expectedValue, valueCursor.getString(idx)); + } + } + + /* + Students: Use this to create some default weather values for your database tests. + */ + static ContentValues createWeatherValues(long locationRowId) { + ContentValues weatherValues = new ContentValues(); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationRowId); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATE, TEST_DATE); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, 1.1); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, 1.2); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, 1.3); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, 75); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, 65); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, "Asteroids"); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, 5.5); + weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, 321); + + return weatherValues; + } + + /* + Students: You can uncomment this helper function once you have finished creating the + LocationEntry part of the WeatherContract. + */ +// static ContentValues createNorthPoleLocationValues() { +// // Create a new map of values, where column names are the keys +// ContentValues testValues = new ContentValues(); +// testValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, TEST_LOCATION); +// testValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, "North Pole"); +// testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, 64.7488); +// testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, -147.353); +// +// return testValues; +// } + + /* + Students: You can uncomment this function once you have finished creating the + LocationEntry part of the WeatherContract as well as the WeatherDbHelper. + */ +// static long insertNorthPoleLocationValues(Context context) { +// // insert our test records into the database +// WeatherDbHelper dbHelper = new WeatherDbHelper(context); +// SQLiteDatabase db = dbHelper.getWritableDatabase(); +// ContentValues testValues = TestUtilities.createNorthPoleLocationValues(); +// +// long locationRowId; +// locationRowId = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues); +// +// // Verify we got a row back. +// assertTrue("Error: Failure to insert North Pole Location Values", locationRowId != -1); +// +// return locationRowId; +// } + + /* + Students: The functions we provide inside of TestProvider use this utility class to test + the ContentObserver callbacks using the PollingCheck class that we grabbed from the Android + CTS tests. + + Note that this only tests that the onChange function is called; it does not test that the + correct Uri is returned. + */ + static class TestContentObserver extends ContentObserver { + final HandlerThread mHT; + boolean mContentChanged; + + static TestContentObserver getTestContentObserver() { + HandlerThread ht = new HandlerThread("ContentObserverThread"); + ht.start(); + return new TestContentObserver(ht); + } + + private TestContentObserver(HandlerThread ht) { + super(new Handler(ht.getLooper())); + mHT = ht; + } + + // On earlier versions of Android, this onChange method is called + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + mContentChanged = true; + } + + public void waitForNotificationOrFail() { + // Note: The PollingCheck class is taken from the Android CTS (Compatibility Test Suite). + // It's useful to look at the Android CTS source for ideas on how to test your Android + // applications. The reason that PollingCheck works is that, by default, the JUnit + // testing framework is not running on the main Android application thread. + new PollingCheck(5000) { + @Override + protected boolean check() { + return mContentChanged; + } + }.run(); + mHT.quit(); + } + } + + static TestContentObserver getTestContentObserver() { + return TestContentObserver.getTestContentObserver(); + } +} diff --git a/app/src/androidTest/java/com/example/android/sunshine/app/utils/PollingCheck.java b/app/src/androidTest/java/com/example/android/sunshine/app/utils/PollingCheck.java new file mode 100644 index 000000000..733d503d0 --- /dev/null +++ b/app/src/androidTest/java/com/example/android/sunshine/app/utils/PollingCheck.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 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. + * + * Note: This file copied from the Android CTS Tests + */ +package com.example.android.sunshine.app.utils; + +import junit.framework.Assert; + +import java.util.concurrent.Callable; + +public abstract class PollingCheck { + private static final long TIME_SLICE = 50; + private long mTimeout = 3000; + + public PollingCheck() { + } + + public PollingCheck(long timeout) { + mTimeout = timeout; + } + + protected abstract boolean check(); + + public void run() { + if (check()) { + return; + } + + long timeout = mTimeout; + while (timeout > 0) { + try { + Thread.sleep(TIME_SLICE); + } catch (InterruptedException e) { + Assert.fail("unexpected InterruptedException"); + } + + if (check()) { + return; + } + + timeout -= TIME_SLICE; + } + + Assert.fail("unexpected timeout"); + } + + public static void check(CharSequence message, long timeout, Callable condition) + throws Exception { + while (timeout > 0) { + if (condition.call()) { + return; + } + + Thread.sleep(TIME_SLICE); + timeout -= TIME_SLICE; + } + + Assert.fail(message.toString()); + } +} \ 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..11ef79db1 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/data/WeatherContract.java @@ -0,0 +1,78 @@ +/* + * 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.provider.BaseColumns; +import android.text.format.Time; + +/** + * Defines table and column names for the weather database. + */ +public class WeatherContract { + + // 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 + Students: This is where you will add the strings. (Similar to what has been + done for WeatherEntry) + */ + public static final class LocationEntry implements BaseColumns { + public static final String TABLE_NAME = "location"; + + } + + /* Inner class that defines the table contents of the weather table */ + public static final class WeatherEntry implements BaseColumns { + + 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"; + } +} 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..ac33ea26a --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/data/WeatherDbHelper.java @@ -0,0 +1,87 @@ +/* + * 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) { + 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_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); + } +} From 9baae67205e75fdb6128bd853e2db6c2bd283dcb Mon Sep 17 00:00:00 2001 From: Chris Lei Date: Tue, 13 Oct 2015 16:05:10 -0700 Subject: [PATCH 34/34] Add API Key Parameter to OpenWeatherMap API Call --- app/build.gradle | 3 +++ .../com/example/android/sunshine/app/ForecastFragment.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 19167f951..f7ffae8fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + buildTypes.each { + it.buildConfigField 'String', 'OPEN_WEATHER_MAP_API_KEY', MyOpenWeatherMapApiKey + } } dependencies { diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 7b5ada34d..b62c711d7 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -276,12 +276,14 @@ protected String[] doInBackground(String... params) { final String FORMAT_PARAM = "mode"; final String UNITS_PARAM = "units"; final String DAYS_PARAM = "cnt"; + final String APPID_PARAM = "APPID"; 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)) + .appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY) .build(); URL url = new URL(builtUri.toString());