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/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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fcad107a6..99c059290 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + + + + + + 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..08b9116b2 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java @@ -0,0 +1,132 @@ +/* + * 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.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; +import android.widget.TextView; + +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 DetailFragment()) + .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) { + startActivity(new Intent(this, SettingsActivity.class)); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * A placeholder fragment containing a simple view. + */ + 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 + 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)) { + mForecastStr = intent.getStringExtra(Intent.EXTRA_TEXT); + ((TextView) rootView.findViewById(R.id.detail_text)) + .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/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..b62c711d7 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.sunshine.app; + +import android.content.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; +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.AdapterView; +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; + +/** + * Encapsulates fetching the forecast and displaying it as a {@link ListView} layout. + */ +public class ForecastFragment extends Fragment { + + private ArrayAdapter mForecastAdapter; + + 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) { + updateWeather(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + // 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. + new ArrayList()); + + 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); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long l) { + String forecast = mForecastAdapter.getItem(position); + Intent intent = new Intent(getActivity(), DetailActivity.class) + .putExtra(Intent.EXTRA_TEXT, forecast); + startActivity(intent); + } + }); + + 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(); + + /* 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, 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); + + 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]; + + // 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; + 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, unitType); + resultStrs[i] = day + " - " + description + " - " + highAndLow; + } + return resultStrs; + + } + @Override + protected String[] doInBackground(String... params) { + + // If there's no zip code, there's nothing to look up. Verify size of params. + if (params.length == 0) { + return null; + } + + // These two need to be declared outside the try/catch + // so that they can be closed in the finally block. + HttpURLConnection urlConnection = null; + BufferedReader reader = null; + + // Will contain the raw JSON response as a string. + String forecastJsonStr = null; + + String format = "json"; + String units = "metric"; + int numDays = 7; + + try { + // Construct the URL for the OpenWeatherMap query + // Possible parameters are avaiable at OWM's forecast API page, at + // http://openweathermap.org/API#forecast + final String FORECAST_BASE_URL = + "http://api.openweathermap.org/data/2.5/forecast/daily?"; + final String QUERY_PARAM = "q"; + 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()); + + // Create the request to OpenWeatherMap, and open the connection + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.connect(); + + // Read the input stream into a String + InputStream inputStream = urlConnection.getInputStream(); + StringBuffer buffer = new StringBuffer(); + if (inputStream == null) { + // Nothing to do. + return null; + } + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String line; + while ((line = reader.readLine()) != null) { + // Since it's JSON, adding a newline isn't necessary (it won't affect parsing) + // But it does make debugging a *lot* easier if you print out the completed + // buffer for debugging. + buffer.append(line + "\n"); + } + + if (buffer.length() == 0) { + // Stream was empty. No point in parsing. + return null; + } + forecastJsonStr = buffer.toString(); + } catch (IOException e) { + Log.e(LOG_TAG, "Error ", e); + // If the code didn't successfully get the weather data, there's no point in attemping + // to parse it. + return null; + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + if (reader != null) { + try { + reader.close(); + } catch (final IOException e) { + Log.e(LOG_TAG, "Error closing stream", e); + } + } + } + + try { + return getWeatherDataFromJson(forecastJsonStr, numDays); + } catch (JSONException e) { + Log.e(LOG_TAG, e.getMessage(), e); + e.printStackTrace(); + } + + // This will only happen if there was an error getting or parsing the forecast. + return null; + } + + @Override + protected void onPostExecute(String[] result) { + if (result != null) { + mForecastAdapter.clear(); + for(String dayForecastStr : result) { + mForecastAdapter.add(dayForecastStr); + } + // New data is back from the server. Hooray! + } + } + } +} 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..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 @@ -1,24 +1,41 @@ +/* + * 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.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; -import android.view.LayoutInflater; +import android.preference.PreferenceManager; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - public class MainActivity extends ActionBarActivity { + private final String LOG_TAG = MainActivity.class.getSimpleName(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() - .add(R.id.container, new PlaceholderFragment()) + .add(R.id.container, new ForecastFragment()) .commit(); } } @@ -39,25 +56,38 @@ public boolean onOptionsItemSelected(MenuItem item) { //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); return true; } + if (id == R.id.action_map) { + openPreferredLocationInMap(); + return true; + } return super.onOptionsItemSelected(item); } - /** - * A placeholder fragment containing a simple view. - */ - public static class PlaceholderFragment extends Fragment { + private void openPreferredLocationInMap() { + SharedPreferences sharedPrefs = + PreferenceManager.getDefaultSharedPreferences(this); + String location = sharedPrefs.getString( + getString(R.string.pref_location_key), + getString(R.string.pref_location_default)); - public PlaceholderFragment() { - } + // 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); - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_main, container, false); - return rootView; + 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/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..98dac5661 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java @@ -0,0 +1,83 @@ +/* + * 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 + addPreferencesFromResource(R.xml.pref_general); + + // For all preferences, attach an OnPreferenceChangeListener so the UI summary can be + // updated when the preference changes. + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key))); + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key))); + } + + /** + * Attaches a listener so the summary is always updated with the preference value. + * Also fires the listener once, to initialize the summary (so it shows up before the value + * is changed.) + */ + private void bindPreferenceSummaryToValue(Preference preference) { + // Set the listener to watch for value changes. + preference.setOnPreferenceChangeListener(this); + + // Trigger the listener immediately with the preference's + // current value. + onPreferenceChange(preference, + PreferenceManager + .getDefaultSharedPreferences(preference.getContext()) + .getString(preference.getKey(), "")); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String stringValue = value.toString(); + + if (preference instanceof ListPreference) { + // For list preferences, look up the correct display value in + // the preference's 'entries' list (since they have separate labels/values). + ListPreference listPreference = (ListPreference) preference; + int prefIndex = listPreference.findIndexOfValue(stringValue); + if (prefIndex >= 0) { + preference.setSummary(listPreference.getEntries()[prefIndex]); + } + } else { + // For other preferences, set the summary to the value's simple string representation. + preference.setSummary(stringValue); + } + return true; + } + +} \ 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); + } +} 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..70bef316d --- /dev/null +++ b/app/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index bb3dd4604..372b60d63 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -1,12 +1,17 @@ - + tools:context=".MainActivity$ForecastFragment"> - + - + 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 diff --git a/app/src/main/res/menu/detail.xml b/app/src/main/res/menu/detail.xml new file mode 100644 index 000000000..865ac0539 --- /dev/null +++ b/app/src/main/res/menu/detail.xml @@ -0,0 +1,9 @@ +

+ + 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/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/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/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 ec03bdf4d..14ae616c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,51 @@ + Sunshine - Hello world! + + Settings + Map Location + Share + + + Refresh + Details + Settings + + + Location + + + location + + + 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 new file mode 100644 index 000000000..212b70ad3 --- /dev/null +++ b/app/src/main/res/xml/pref_general.xml @@ -0,0 +1,20 @@ + + + + + + + +