Initial Commit for Start of Class

This commit is contained in:
Dan Galpin
2015-05-24 13:58:42 -07:00
parent 57cdfc5ef2
commit 520559f11c
139 changed files with 5618 additions and 2 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
.gradle
.DS_Store
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
out/
gen/
# Libraries used by the app
# Can explicitly add if we want, but shouldn't do so blindly. Licenses, bloat, etc.
/libs
# Build stuff (auto-generated by android update project ...)
build.xml
ant.properties
local.properties
project.properties
# Eclipse project files
.classpath
.project
# idea project files
.idea/
.idea/.name
*.iml
*.ipr
*.iws
##Gradle-based build
.gradle
build/

35
CONTRIB.md Normal file
View File

@@ -0,0 +1,35 @@
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(https://developers.google.com/open-source/cla/individual).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
2. The repo owner will respond to your issue promptly.
3. If your proposed change is accepted, and you haven't already done so, sign a
Contributor License Agreement (see details above).
4. Fork the desired repo, develop and test your code changes.
5. Ensure that your code adheres to the existing style in the sample to which
you are contributing. Refer to the
[Android Code Style Guide]
(https://source.android.com/source/code-style.html) for the
recommended coding standards for this organization.
6. Ensure that your code has an appropriate set of unit tests which all pass.
7. Submit a pull request.

35
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,35 @@
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(https://cla.developers.google.com).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(https://cla.developers.google.com).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
2. The repo owner will respond to your issue promptly.
3. If your proposed change is accepted, and you haven't already done so, sign a
Contributor License Agreement (see details above).
4. Fork the desired repo, develop and test your code changes.
5. Ensure that your code adheres to the existing style in the sample to which
you are contributing. Refer to the
[Android Code Style Guide]
(https://source.android.com/source/code-style.html) for the
recommended coding standards for this organization.
6. Ensure that your code has an appropriate set of unit tests which all pass.
7. Submit a pull request.

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Google Inc.
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.

View File

@@ -1,2 +1,44 @@
# Advanced_Android_Development
Repo for the Advanced Android App Development course
Advanced Android Sample App
===================================
Synchronizes weather information from OpenWeatherMap on Android Phones and Tablets. Used in the Udacity Advanced Android course.
Pre-requisites
--------------
Android SDK 21 or Higher
Build Tools version 21.1.2
Android Support AppCompat 22.2.0
Getting Started
---------------
This sample uses the Gradle build system. To build this project, use the
"gradlew build" command or use "Import Project" in Android Studio.
Support
-------
- Google+ Community: https://plus.google.com/communities/105153134372062985968
- Stack Overflow: http://stackoverflow.com/questions/tagged/android
Patches are encouraged, and may be submitted by forking this project and
submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details.
License
-------
Copyright 2015 The Android Open Source Project, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you 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.

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

25
app/build.gradle Normal file
View File

@@ -0,0 +1,25 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "com.example.android.sunshine.app"
minSdkVersion 10
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.2'
}

17
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/lyla/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2015 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();
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright (C) 2015 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.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.test.AndroidTestCase;
import java.util.HashSet;
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<String> tableNameHashSet = new HashSet<String>();
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<String> locationColumnHashSet = new HashSet<String>();
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() {
insertLocation();
}
/*
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.
long locationRowId = insertLocation();
// Make sure we have a valid row ID.
assertFalse("Error: Location Not Inserted Correctly", locationRowId == -1L);
// First step: Get reference to writable database
// If there's an error in those massive SQL table creation Strings,
// errors will be thrown here when you try to get a writable database.
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();
// Second Step (Weather): Create weather values
ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);
// Third Step (Weather): Insert ContentValues into database and get a row ID back
long weatherRowId = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, weatherValues);
assertTrue(weatherRowId != -1);
// Fourth Step: Query the database and receive a Cursor back
// A cursor is your primary interface to the query results.
Cursor weatherCursor = db.query(
WeatherContract.WeatherEntry.TABLE_NAME, // Table to Query
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null, // columns to group by
null, // columns to filter by row groups
null // sort order
);
// Move the cursor to the first valid database row and check to see if we have any rows
assertTrue( "Error: No Records returned from location query", weatherCursor.moveToFirst() );
// Fifth Step: Validate the location Query
TestUtilities.validateCurrentRecord("testInsertReadDb weatherEntry failed to validate",
weatherCursor, weatherValues);
// Move the cursor to demonstrate that there is only one record in the database
assertFalse( "Error: More than one record returned from weather query",
weatherCursor.moveToNext() );
// Sixth Step: Close cursor and database
weatherCursor.close();
dbHelper.close();
}
/*
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() {
// First step: Get reference to writable database
// If there's an error in those massive SQL table creation Strings,
// errors will be thrown here when you try to get a writable database.
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();
// Second Step: Create ContentValues of what you want to insert
// (you can use the createNorthPoleLocationValues if you wish)
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
// Third Step: Insert ContentValues into database and get a row ID back
long locationRowId;
locationRowId = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues);
// Verify we got a row back.
assertTrue(locationRowId != -1);
// Data's inserted. IN THEORY. Now pull some out to stare at it and verify it made
// the round trip.
// Fourth Step: Query the database and receive a Cursor back
// A cursor is your primary interface to the query results.
Cursor cursor = db.query(
WeatherContract.LocationEntry.TABLE_NAME, // Table to Query
null, // all columns
null, // Columns for the "where" clause
null, // Values for the "where" clause
null, // columns to group by
null, // columns to filter by row groups
null // sort order
);
// Move the cursor to a valid database row and check to see if we got any records back
// from the query
assertTrue( "Error: No Records returned from location query", cursor.moveToFirst() );
// Fifth Step: Validate data in resulting Cursor with the original ContentValues
// (you can use the validateCurrentRecord function in TestUtilities to validate the
// query if you like)
TestUtilities.validateCurrentRecord("Error: Location Query Validation Failed",
cursor, testValues);
// Move the cursor to demonstrate that there is only one record in the database
assertFalse( "Error: More than one record returned from location query",
cursor.moveToNext() );
// Sixth Step: Close Cursor and Database
cursor.close();
db.close();
return locationRowId;
}
}

View File

@@ -0,0 +1,515 @@
/*
* Copyright (C) 2015 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.ComponentName;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.test.AndroidTestCase;
import android.util.Log;
import com.example.android.sunshine.app.data.WeatherContract.LocationEntry;
import com.example.android.sunshine.app.data.WeatherContract.WeatherEntry;
/*
Note: This is not a complete set of tests of the Sunshine ContentProvider, but it does test
that at least the basic functionality has been implemented correctly.
Students: Uncomment the tests in this class as you implement the functionality in your
ContentProvider to make sure that you've implemented things reasonably correctly.
*/
public class TestProvider extends AndroidTestCase {
public static final String LOG_TAG = TestProvider.class.getSimpleName();
/*
This helper function deletes all records from both database tables using the ContentProvider.
It also queries the ContentProvider to make sure that the database has been successfully
deleted, so it cannot be used until the Query and Delete functions have been written
in the ContentProvider.
Students: Replace the calls to deleteAllRecordsFromDB with this one after you have written
the delete functionality in the ContentProvider.
*/
public void deleteAllRecordsFromProvider() {
mContext.getContentResolver().delete(
WeatherEntry.CONTENT_URI,
null,
null
);
mContext.getContentResolver().delete(
LocationEntry.CONTENT_URI,
null,
null
);
Cursor cursor = mContext.getContentResolver().query(
WeatherEntry.CONTENT_URI,
null,
null,
null,
null
);
assertEquals("Error: Records not deleted from Weather table during delete", 0, cursor.getCount());
cursor.close();
cursor = mContext.getContentResolver().query(
LocationEntry.CONTENT_URI,
null,
null,
null,
null
);
assertEquals("Error: Records not deleted from Location table during delete", 0, cursor.getCount());
cursor.close();
}
/*
Student: Refactor this function to use the deleteAllRecordsFromProvider functionality once
you have implemented delete functionality there.
*/
public void deleteAllRecords() {
deleteAllRecordsFromProvider();
}
// Since we want each test to start with a clean slate, run deleteAllRecords
// in setUp (called by the test runner before each test).
@Override
protected void setUp() throws Exception {
super.setUp();
deleteAllRecords();
}
/*
This test checks to make sure that the content provider is registered correctly.
Students: Uncomment this test to make sure you've correctly registered the WeatherProvider.
*/
public void testProviderRegistry() {
PackageManager pm = mContext.getPackageManager();
// We define the component name based on the package name from the context and the
// WeatherProvider class.
ComponentName componentName = new ComponentName(mContext.getPackageName(),
WeatherProvider.class.getName());
try {
// Fetch the provider info using the component name from the PackageManager
// This throws an exception if the provider isn't registered.
ProviderInfo providerInfo = pm.getProviderInfo(componentName, 0);
// Make sure that the registered authority matches the authority from the Contract.
assertEquals("Error: WeatherProvider registered with authority: " + providerInfo.authority +
" instead of authority: " + WeatherContract.CONTENT_AUTHORITY,
providerInfo.authority, WeatherContract.CONTENT_AUTHORITY);
} catch (PackageManager.NameNotFoundException e) {
// I guess the provider isn't registered correctly.
assertTrue("Error: WeatherProvider not registered at " + mContext.getPackageName(),
false);
}
}
/*
This test doesn't touch the database. It verifies that the ContentProvider returns
the correct type for each type of URI that it can handle.
Students: Uncomment this test to verify that your implementation of GetType is
functioning correctly.
*/
public void testGetType() {
// content://com.example.android.sunshine.app/weather/
String type = mContext.getContentResolver().getType(WeatherEntry.CONTENT_URI);
// vnd.android.cursor.dir/com.example.android.sunshine.app/weather
assertEquals("Error: the WeatherEntry CONTENT_URI should return WeatherEntry.CONTENT_TYPE",
WeatherEntry.CONTENT_TYPE, type);
String testLocation = "94074";
// content://com.example.android.sunshine.app/weather/94074
type = mContext.getContentResolver().getType(
WeatherEntry.buildWeatherLocation(testLocation));
// vnd.android.cursor.dir/com.example.android.sunshine.app/weather
assertEquals("Error: the WeatherEntry CONTENT_URI with location should return WeatherEntry.CONTENT_TYPE",
WeatherEntry.CONTENT_TYPE, type);
long testDate = 1419120000L; // December 21st, 2014
// content://com.example.android.sunshine.app/weather/94074/20140612
type = mContext.getContentResolver().getType(
WeatherEntry.buildWeatherLocationWithDate(testLocation, testDate));
// vnd.android.cursor.item/com.example.android.sunshine.app/weather/1419120000
assertEquals("Error: the WeatherEntry CONTENT_URI with location and date should return WeatherEntry.CONTENT_ITEM_TYPE",
WeatherEntry.CONTENT_ITEM_TYPE, type);
// content://com.example.android.sunshine.app/location/
type = mContext.getContentResolver().getType(LocationEntry.CONTENT_URI);
// vnd.android.cursor.dir/com.example.android.sunshine.app/location
assertEquals("Error: the LocationEntry CONTENT_URI should return LocationEntry.CONTENT_TYPE",
LocationEntry.CONTENT_TYPE, type);
}
/*
This test uses the database directly to insert and then uses the ContentProvider to
read out the data. Uncomment this test to see if the basic weather query functionality
given in the ContentProvider is working correctly.
*/
public void testBasicWeatherQuery() {
// insert our test records into the database
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
long locationRowId = TestUtilities.insertNorthPoleLocationValues(mContext);
// Fantastic. Now that we have a location, add some weather!
ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);
long weatherRowId = db.insert(WeatherEntry.TABLE_NAME, null, weatherValues);
assertTrue("Unable to Insert WeatherEntry into the Database", weatherRowId != -1);
db.close();
// Test the basic content provider query
Cursor weatherCursor = mContext.getContentResolver().query(
WeatherEntry.CONTENT_URI,
null,
null,
null,
null
);
// Make sure we get the correct cursor out of the database
TestUtilities.validateCursor("testBasicWeatherQuery", weatherCursor, weatherValues);
}
/*
This test uses the database directly to insert and then uses the ContentProvider to
read out the data. Uncomment this test to see if your location queries are
performing correctly.
*/
public void testBasicLocationQueries() {
// insert our test records into the database
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
long locationRowId = TestUtilities.insertNorthPoleLocationValues(mContext);
// Test the basic content provider query
Cursor locationCursor = mContext.getContentResolver().query(
LocationEntry.CONTENT_URI,
null,
null,
null,
null
);
// Make sure we get the correct cursor out of the database
TestUtilities.validateCursor("testBasicLocationQueries, location query", locationCursor, testValues);
// Has the NotificationUri been set correctly? --- we can only test this easily against API
// level 19 or greater because getNotificationUri was added in API level 19.
if ( Build.VERSION.SDK_INT >= 19 ) {
assertEquals("Error: Location Query did not properly set NotificationUri",
locationCursor.getNotificationUri(), LocationEntry.CONTENT_URI);
}
}
/*
This test uses the provider to insert and then update the data. Uncomment this test to
see if your update location is functioning correctly.
*/
public void testUpdateLocation() {
// Create a new map of values, where column names are the keys
ContentValues values = TestUtilities.createNorthPoleLocationValues();
Uri locationUri = mContext.getContentResolver().
insert(LocationEntry.CONTENT_URI, values);
long locationRowId = ContentUris.parseId(locationUri);
// Verify we got a row back.
assertTrue(locationRowId != -1);
Log.d(LOG_TAG, "New row id: " + locationRowId);
ContentValues updatedValues = new ContentValues(values);
updatedValues.put(LocationEntry._ID, locationRowId);
updatedValues.put(LocationEntry.COLUMN_CITY_NAME, "Santa's Village");
// Create a cursor with observer to make sure that the content provider is notifying
// the observers as expected
Cursor locationCursor = mContext.getContentResolver().query(LocationEntry.CONTENT_URI, null, null, null, null);
TestUtilities.TestContentObserver tco = TestUtilities.getTestContentObserver();
locationCursor.registerContentObserver(tco);
int count = mContext.getContentResolver().update(
LocationEntry.CONTENT_URI, updatedValues, LocationEntry._ID + "= ?",
new String[] { Long.toString(locationRowId)});
assertEquals(count, 1);
// Test to make sure our observer is called. If not, we throw an assertion.
//
// Students: If your code is failing here, it means that your content provider
// isn't calling getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();
locationCursor.unregisterContentObserver(tco);
locationCursor.close();
// A cursor is your primary interface to the query results.
Cursor cursor = mContext.getContentResolver().query(
LocationEntry.CONTENT_URI,
null, // projection
LocationEntry._ID + " = " + locationRowId,
null, // Values for the "where" clause
null // sort order
);
TestUtilities.validateCursor("testUpdateLocation. Error validating location entry update.",
cursor, updatedValues);
cursor.close();
}
// Make sure we can still delete after adding/updating stuff
//
// Student: Uncomment this test after you have completed writing the insert functionality
// in your provider. It relies on insertions with testInsertReadProvider, so insert and
// query functionality must also be complete before this test can be used.
public void testInsertReadProvider() {
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
// Register a content observer for our insert. This time, directly with the content resolver
TestUtilities.TestContentObserver tco = TestUtilities.getTestContentObserver();
mContext.getContentResolver().registerContentObserver(LocationEntry.CONTENT_URI, true, tco);
Uri locationUri = mContext.getContentResolver().insert(LocationEntry.CONTENT_URI, testValues);
// Did our content observer get called? Students: If this fails, your insert location
// isn't calling getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();
mContext.getContentResolver().unregisterContentObserver(tco);
long locationRowId = ContentUris.parseId(locationUri);
// Verify we got a row back.
assertTrue(locationRowId != -1);
// Data's inserted. IN THEORY. Now pull some out to stare at it and verify it made
// the round trip.
// A cursor is your primary interface to the query results.
Cursor cursor = mContext.getContentResolver().query(
LocationEntry.CONTENT_URI,
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating LocationEntry.",
cursor, testValues);
// Fantastic. Now that we have a location, add some weather!
ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);
// The TestContentObserver is a one-shot class
tco = TestUtilities.getTestContentObserver();
mContext.getContentResolver().registerContentObserver(WeatherEntry.CONTENT_URI, true, tco);
Uri weatherInsertUri = mContext.getContentResolver()
.insert(WeatherEntry.CONTENT_URI, weatherValues);
assertTrue(weatherInsertUri != null);
// Did our content observer get called? Students: If this fails, your insert weather
// in your ContentProvider isn't calling
// getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();
mContext.getContentResolver().unregisterContentObserver(tco);
// A cursor is your primary interface to the query results.
Cursor weatherCursor = mContext.getContentResolver().query(
WeatherEntry.CONTENT_URI, // Table to Query
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // columns to group by
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating WeatherEntry insert.",
weatherCursor, weatherValues);
// Add the location values in with the weather data so that we can make
// sure that the join worked and we actually get all the values back
weatherValues.putAll(testValues);
// Get the joined Weather and Location data
weatherCursor = mContext.getContentResolver().query(
WeatherEntry.buildWeatherLocation(TestUtilities.TEST_LOCATION),
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location Data.",
weatherCursor, weatherValues);
// Get the joined Weather and Location data with a start date
weatherCursor = mContext.getContentResolver().query(
WeatherEntry.buildWeatherLocationWithStartDate(
TestUtilities.TEST_LOCATION, TestUtilities.TEST_DATE),
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location Data with start date.",
weatherCursor, weatherValues);
// Get the joined Weather data for a specific date
weatherCursor = mContext.getContentResolver().query(
WeatherEntry.buildWeatherLocationWithDate(TestUtilities.TEST_LOCATION, TestUtilities.TEST_DATE),
null,
null,
null,
null
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location data for a specific date.",
weatherCursor, weatherValues);
}
// Make sure we can still delete after adding/updating stuff
//
// Student: Uncomment this test after you have completed writing the delete functionality
// in your provider. It relies on insertions with testInsertReadProvider, so insert and
// query functionality must also be complete before this test can be used.
public void testDeleteRecords() {
testInsertReadProvider();
// Register a content observer for our location delete.
TestUtilities.TestContentObserver locationObserver = TestUtilities.getTestContentObserver();
mContext.getContentResolver().registerContentObserver(LocationEntry.CONTENT_URI, true, locationObserver);
// Register a content observer for our weather delete.
TestUtilities.TestContentObserver weatherObserver = TestUtilities.getTestContentObserver();
mContext.getContentResolver().registerContentObserver(WeatherEntry.CONTENT_URI, true, weatherObserver);
deleteAllRecordsFromProvider();
// Students: If either of these fail, you most-likely are not calling the
// getContext().getContentResolver().notifyChange(uri, null); in the ContentProvider
// delete. (only if the insertReadProvider is succeeding)
locationObserver.waitForNotificationOrFail();
weatherObserver.waitForNotificationOrFail();
mContext.getContentResolver().unregisterContentObserver(locationObserver);
mContext.getContentResolver().unregisterContentObserver(weatherObserver);
}
static private final int BULK_INSERT_RECORDS_TO_INSERT = 10;
static ContentValues[] createBulkInsertWeatherValues(long locationRowId) {
long currentTestDate = TestUtilities.TEST_DATE;
long millisecondsInADay = 1000*60*60*24;
ContentValues[] returnContentValues = new ContentValues[BULK_INSERT_RECORDS_TO_INSERT];
for ( int i = 0; i < BULK_INSERT_RECORDS_TO_INSERT; i++, currentTestDate+= millisecondsInADay ) {
ContentValues weatherValues = new ContentValues();
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationRowId);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATE, currentTestDate);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, 1.1);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, 1.2 + 0.01 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, 1.3 - 0.01 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, 75 + i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, 65 - i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, "Asteroids");
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, 5.5 + 0.2 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, 321);
returnContentValues[i] = weatherValues;
}
return returnContentValues;
}
// Student: Uncomment this test after you have completed writing the BulkInsert functionality
// in your provider. Note that this test will work with the built-in (default) provider
// implementation, which just inserts records one-at-a-time, so really do implement the
// BulkInsert ContentProvider function.
public void testBulkInsert() {
// first, let's create a location value
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
Uri locationUri = mContext.getContentResolver().insert(LocationEntry.CONTENT_URI, testValues);
long locationRowId = ContentUris.parseId(locationUri);
// Verify we got a row back.
assertTrue(locationRowId != -1);
// Data's inserted. IN THEORY. Now pull some out to stare at it and verify it made
// the round trip.
// A cursor is your primary interface to the query results.
Cursor cursor = mContext.getContentResolver().query(
LocationEntry.CONTENT_URI,
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testBulkInsert. Error validating LocationEntry.",
cursor, testValues);
// Now we can bulkInsert some weather. In fact, we only implement BulkInsert for weather
// entries. With ContentProviders, you really only have to implement the features you
// use, after all.
ContentValues[] bulkInsertContentValues = createBulkInsertWeatherValues(locationRowId);
// Register a content observer for our bulk insert.
TestUtilities.TestContentObserver weatherObserver = TestUtilities.getTestContentObserver();
mContext.getContentResolver().registerContentObserver(WeatherEntry.CONTENT_URI, true, weatherObserver);
int insertCount = mContext.getContentResolver().bulkInsert(WeatherEntry.CONTENT_URI, bulkInsertContentValues);
// Students: If this fails, it means that you most-likely are not calling the
// getContext().getContentResolver().notifyChange(uri, null); in your BulkInsert
// ContentProvider method.
weatherObserver.waitForNotificationOrFail();
mContext.getContentResolver().unregisterContentObserver(weatherObserver);
assertEquals(insertCount, BULK_INSERT_RECORDS_TO_INSERT);
// A cursor is your primary interface to the query results.
cursor = mContext.getContentResolver().query(
WeatherEntry.CONTENT_URI,
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
WeatherEntry.COLUMN_DATE + " ASC" // sort order == by DATE ASCENDING
);
// we should have as many records in the database as we've inserted
assertEquals(cursor.getCount(), BULK_INSERT_RECORDS_TO_INSERT);
// and let's make sure they match the ones we created
cursor.moveToFirst();
for ( int i = 0; i < BULK_INSERT_RECORDS_TO_INSERT; i++, cursor.moveToNext() ) {
TestUtilities.validateCurrentRecord("testBulkInsert. Error validating WeatherEntry " + i,
cursor, bulkInsertContentValues[i]);
}
cursor.close();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 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.UriMatcher;
import android.net.Uri;
import android.test.AndroidTestCase;
/*
Uncomment this class when you are ready to test your UriMatcher. Note that this class utilizes
constants that are declared with package protection inside of the UriMatcher, which is why
the test must be in the same data package as the Android app code. Doing the test this way is
a nice compromise between data hiding and testability.
*/
public class TestUriMatcher extends AndroidTestCase {
private static final String LOCATION_QUERY = "London, UK";
private static final long TEST_DATE = 1419033600L; // December 20th, 2014
private static final long TEST_LOCATION_ID = 10L;
// content://com.example.android.sunshine.app/weather"
private static final Uri TEST_WEATHER_DIR = WeatherContract.WeatherEntry.CONTENT_URI;
private static final Uri TEST_WEATHER_WITH_LOCATION_DIR = WeatherContract.WeatherEntry.buildWeatherLocation(LOCATION_QUERY);
private static final Uri TEST_WEATHER_WITH_LOCATION_AND_DATE_DIR = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(LOCATION_QUERY, TEST_DATE);
// content://com.example.android.sunshine.app/location"
private static final Uri TEST_LOCATION_DIR = WeatherContract.LocationEntry.CONTENT_URI;
/*
Students: This function tests that your UriMatcher returns the correct integer value
for each of the Uri types that our ContentProvider can handle. Uncomment this when you are
ready to test your UriMatcher.
*/
public void testUriMatcher() {
UriMatcher testMatcher = WeatherProvider.buildUriMatcher();
assertEquals("Error: The WEATHER URI was matched incorrectly.",
testMatcher.match(TEST_WEATHER_DIR), WeatherProvider.WEATHER);
assertEquals("Error: The WEATHER WITH LOCATION URI was matched incorrectly.",
testMatcher.match(TEST_WEATHER_WITH_LOCATION_DIR), WeatherProvider.WEATHER_WITH_LOCATION);
assertEquals("Error: The WEATHER WITH LOCATION AND DATE URI was matched incorrectly.",
testMatcher.match(TEST_WEATHER_WITH_LOCATION_AND_DATE_DIR), WeatherProvider.WEATHER_WITH_LOCATION_AND_DATE);
assertEquals("Error: The LOCATION URI was matched incorrectly.",
testMatcher.match(TEST_LOCATION_DIR), WeatherProvider.LOCATION);
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2015 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.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
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<Map.Entry<String, Object>> valueSet = expectedValues.valueSet();
for (Map.Entry<String, Object> 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();
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2015 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.net.Uri;
import android.test.AndroidTestCase;
/*
Students: This is NOT a complete test for the WeatherContract --- just for the functions
that we expect you to write.
*/
public class TestWeatherContract extends AndroidTestCase {
// intentionally includes a slash to make sure Uri is getting quoted correctly
private static final String TEST_WEATHER_LOCATION = "/North Pole";
private static final long TEST_WEATHER_DATE = 1419033600L; // December 20th, 2014
/*
Students: Uncomment this out to test your weather location function.
*/
public void testBuildWeatherLocation() {
Uri locationUri = WeatherContract.WeatherEntry.buildWeatherLocation(TEST_WEATHER_LOCATION);
assertNotNull("Error: Null Uri returned. You must fill-in buildWeatherLocation in " +
"WeatherContract.",
locationUri);
assertEquals("Error: Weather location not properly appended to the end of the Uri",
TEST_WEATHER_LOCATION, locationUri.getLastPathSegment());
assertEquals("Error: Weather location Uri doesn't match our expected result",
locationUri.toString(),
"content://com.example.android.sunshine.app/weather/%2FNorth%20Pole");
}
}

View File

@@ -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<Boolean> condition)
throws Exception {
while (timeout > 0) {
if (condition.call()) {
return;
}
Thread.sleep(TIME_SLICE);
timeout -= TIME_SLICE;
}
Assert.fail(message.toString());
}
}

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sunshine.app" >
<!-- This permission is necessary in order for Sunshine to perform network access. -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Permissions required by the sync adapter -->
<uses-permission
android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission
android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:theme="@style/ForecastTheme"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DetailActivity"
android:label="@string/title_activity_detail"
android:parentActivityName=".MainActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.android.sunshine.app.MainActivity" />
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity"
android:theme="@style/SettingsTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.android.sunshine.app.MainActivity" />
</activity>
<provider
android:authorities="@string/content_authority"
android:name=".data.WeatherProvider"
android:exported="false"
android:syncable="true" />
<!-- SyncAdapter's dummy authentication service -->
<service android:name=".sync.SunshineAuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<!-- The SyncAdapter service -->
<service
android:name=".sync.SunshineSyncService"
android:exported="true"
>
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,67 @@
/*
* 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.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
public class DetailActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());
DetailFragment fragment = new DetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.add(R.id.weather_detail_container, fragment)
.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();
if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2015 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.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.ShareActionProvider;
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.ImageView;
import android.widget.TextView;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.data.WeatherContract.WeatherEntry;
/**
* A placeholder fragment containing a simple view.
*/
public class DetailFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String LOG_TAG = DetailFragment.class.getSimpleName();
static final String DETAIL_URI = "URI";
private static final String FORECAST_SHARE_HASHTAG = " #SunshineApp";
private ShareActionProvider mShareActionProvider;
private String mForecast;
private Uri mUri;
private static final int DETAIL_LOADER = 0;
private static final String[] DETAIL_COLUMNS = {
WeatherEntry.TABLE_NAME + "." + WeatherEntry._ID,
WeatherEntry.COLUMN_DATE,
WeatherEntry.COLUMN_SHORT_DESC,
WeatherEntry.COLUMN_MAX_TEMP,
WeatherEntry.COLUMN_MIN_TEMP,
WeatherEntry.COLUMN_HUMIDITY,
WeatherEntry.COLUMN_PRESSURE,
WeatherEntry.COLUMN_WIND_SPEED,
WeatherEntry.COLUMN_DEGREES,
WeatherEntry.COLUMN_WEATHER_ID,
// This works because the WeatherProvider returns location data joined with
// weather data, even though they're stored in two different tables.
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING
};
// These indices are tied to DETAIL_COLUMNS. If DETAIL_COLUMNS changes, these
// must change.
public static final int COL_WEATHER_ID = 0;
public static final int COL_WEATHER_DATE = 1;
public static final int COL_WEATHER_DESC = 2;
public static final int COL_WEATHER_MAX_TEMP = 3;
public static final int COL_WEATHER_MIN_TEMP = 4;
public static final int COL_WEATHER_HUMIDITY = 5;
public static final int COL_WEATHER_PRESSURE = 6;
public static final int COL_WEATHER_WIND_SPEED = 7;
public static final int COL_WEATHER_DEGREES = 8;
public static final int COL_WEATHER_CONDITION_ID = 9;
private ImageView mIconView;
private TextView mFriendlyDateView;
private TextView mDateView;
private TextView mDescriptionView;
private TextView mHighTempView;
private TextView mLowTempView;
private TextView mHumidityView;
private TextView mWindView;
private TextView mPressureView;
public DetailFragment() {
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle arguments = getArguments();
if (arguments != null) {
mUri = arguments.getParcelable(DetailFragment.DETAIL_URI);
}
View rootView = inflater.inflate(R.layout.fragment_detail, container, false);
mIconView = (ImageView) rootView.findViewById(R.id.detail_icon);
mDateView = (TextView) rootView.findViewById(R.id.detail_date_textview);
mFriendlyDateView = (TextView) rootView.findViewById(R.id.detail_day_textview);
mDescriptionView = (TextView) rootView.findViewById(R.id.detail_forecast_textview);
mHighTempView = (TextView) rootView.findViewById(R.id.detail_high_textview);
mLowTempView = (TextView) rootView.findViewById(R.id.detail_low_textview);
mHumidityView = (TextView) rootView.findViewById(R.id.detail_humidity_textview);
mWindView = (TextView) rootView.findViewById(R.id.detail_wind_textview);
mPressureView = (TextView) rootView.findViewById(R.id.detail_pressure_textview);
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.
mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem);
// If onLoadFinished happens before this, we can go ahead and set the share intent now.
if (mForecast != null) {
mShareActionProvider.setShareIntent(createShareForecastIntent());
}
}
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, mForecast + FORECAST_SHARE_HASHTAG);
return shareIntent;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
getLoaderManager().initLoader(DETAIL_LOADER, null, this);
super.onActivityCreated(savedInstanceState);
}
void onLocationChanged( String newLocation ) {
// replace the uri, since the location has changed
Uri uri = mUri;
if (null != uri) {
long date = WeatherContract.WeatherEntry.getDateFromUri(uri);
Uri updatedUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(newLocation, date);
mUri = updatedUri;
getLoaderManager().restartLoader(DETAIL_LOADER, null, this);
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if ( null != mUri ) {
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(
getActivity(),
mUri,
DETAIL_COLUMNS,
null,
null,
null
);
}
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (data != null && data.moveToFirst()) {
// Read weather condition ID from cursor
int weatherId = data.getInt(COL_WEATHER_CONDITION_ID);
// Use weather art image
mIconView.setImageResource(Utility.getArtResourceForWeatherCondition(weatherId));
// Read date from cursor and update views for day of week and date
long date = data.getLong(COL_WEATHER_DATE);
String friendlyDateText = Utility.getDayName(getActivity(), date);
String dateText = Utility.getFormattedMonthDay(getActivity(), date);
mFriendlyDateView.setText(friendlyDateText);
mDateView.setText(dateText);
// Read description from cursor and update view
String description = data.getString(COL_WEATHER_DESC);
mDescriptionView.setText(description);
// For accessibility, add a content description to the icon field
mIconView.setContentDescription(description);
// Read high temperature from cursor and update view
boolean isMetric = Utility.isMetric(getActivity());
double high = data.getDouble(COL_WEATHER_MAX_TEMP);
String highString = Utility.formatTemperature(getActivity(), high);
mHighTempView.setText(highString);
// Read low temperature from cursor and update view
double low = data.getDouble(COL_WEATHER_MIN_TEMP);
String lowString = Utility.formatTemperature(getActivity(), low);
mLowTempView.setText(lowString);
// Read humidity from cursor and update view
float humidity = data.getFloat(COL_WEATHER_HUMIDITY);
mHumidityView.setText(getActivity().getString(R.string.format_humidity, humidity));
// Read wind speed and direction from cursor and update view
float windSpeedStr = data.getFloat(COL_WEATHER_WIND_SPEED);
float windDirStr = data.getFloat(COL_WEATHER_DEGREES);
mWindView.setText(Utility.getFormattedWind(getActivity(), windSpeedStr, windDirStr));
// Read pressure from cursor and update view
float pressure = data.getFloat(COL_WEATHER_PRESSURE);
mPressureView.setText(getActivity().getString(R.string.format_pressure, pressure));
// We still need this for the share intent
mForecast = String.format("%s - %s - %s/%s", dateText, description, high, low);
// If onCreateOptionsMenu has already happened, we need to update the share intent now.
if (mShareActionProvider != null) {
mShareActionProvider.setShareIntent(createShareForecastIntent());
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sunshine.app;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
/**
* {@link ForecastAdapter} exposes a list of weather forecasts
* from a {@link Cursor} to a {@link android.widget.ListView}.
*/
public class ForecastAdapter extends CursorAdapter {
private static final int VIEW_TYPE_COUNT = 2;
private static final int VIEW_TYPE_TODAY = 0;
private static final int VIEW_TYPE_FUTURE_DAY = 1;
// Flag to determine if we want to use a separate view for "today".
private boolean mUseTodayLayout = true;
/**
* Cache of the children views for a forecast list item.
*/
public static class ViewHolder {
public final ImageView iconView;
public final TextView dateView;
public final TextView descriptionView;
public final TextView highTempView;
public final TextView lowTempView;
public ViewHolder(View view) {
iconView = (ImageView) view.findViewById(R.id.list_item_icon);
dateView = (TextView) view.findViewById(R.id.list_item_date_textview);
descriptionView = (TextView) view.findViewById(R.id.list_item_forecast_textview);
highTempView = (TextView) view.findViewById(R.id.list_item_high_textview);
lowTempView = (TextView) view.findViewById(R.id.list_item_low_textview);
}
}
public ForecastAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// Choose the layout type
int viewType = getItemViewType(cursor.getPosition());
int layoutId = -1;
switch (viewType) {
case VIEW_TYPE_TODAY: {
layoutId = R.layout.list_item_forecast_today;
break;
}
case VIEW_TYPE_FUTURE_DAY: {
layoutId = R.layout.list_item_forecast;
break;
}
}
View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
view.setTag(viewHolder);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder viewHolder = (ViewHolder) view.getTag();
int viewType = getItemViewType(cursor.getPosition());
switch (viewType) {
case VIEW_TYPE_TODAY: {
// Get weather icon
viewHolder.iconView.setImageResource(Utility.getArtResourceForWeatherCondition(
cursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID)));
break;
}
case VIEW_TYPE_FUTURE_DAY: {
// Get weather icon
viewHolder.iconView.setImageResource(Utility.getIconResourceForWeatherCondition(
cursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID)));
break;
}
}
// Read date from cursor
long dateInMillis = cursor.getLong(ForecastFragment.COL_WEATHER_DATE);
// Find TextView and set formatted date on it
viewHolder.dateView.setText(Utility.getFriendlyDayString(context, dateInMillis));
// Read weather forecast from cursor
String description = cursor.getString(ForecastFragment.COL_WEATHER_DESC);
// Find TextView and set weather forecast on it
viewHolder.descriptionView.setText(description);
// For accessibility, add a content description to the icon field
viewHolder.iconView.setContentDescription(description);
// Read user preference for metric or imperial temperature units
boolean isMetric = Utility.isMetric(context);
// Read high temperature from cursor
double high = cursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP);
viewHolder.highTempView.setText(Utility.formatTemperature(context, high));
// Read low temperature from cursor
double low = cursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP);
viewHolder.lowTempView.setText(Utility.formatTemperature(context, low));
}
public void setUseTodayLayout(boolean useTodayLayout) {
mUseTodayLayout = useTodayLayout;
}
@Override
public int getItemViewType(int position) {
return (position == 0 && mUseTodayLayout) ? VIEW_TYPE_TODAY : VIEW_TYPE_FUTURE_DAY;
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright (C) 2015 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.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.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.ListView;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
/**
* Encapsulates fetching the forecast and displaying it as a {@link ListView} layout.
*/
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
private ForecastAdapter mForecastAdapter;
private ListView mListView;
private int mPosition = ListView.INVALID_POSITION;
private boolean mUseTodayLayout;
private static final String SELECTED_KEY = "selected_position";
private static final int FORECAST_LOADER = 0;
// For the forecast view we're showing only a small subset of the stored data.
// Specify the columns we need.
private static final String[] FORECAST_COLUMNS = {
// In this case the id needs to be fully qualified with a table name, since
// the content provider joins the location & weather tables in the background
// (both have an _id column)
// On the one hand, that's annoying. On the other, you can search the weather table
// using the location set by the user, which is only in the Location table.
// So the convenience is worth it.
WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
WeatherContract.WeatherEntry.COLUMN_DATE,
WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
WeatherContract.WeatherEntry.COLUMN_MIN_TEMP,
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING,
WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
WeatherContract.LocationEntry.COLUMN_COORD_LAT,
WeatherContract.LocationEntry.COLUMN_COORD_LONG
};
// These indices are tied to FORECAST_COLUMNS. If FORECAST_COLUMNS changes, these
// must change.
static final int COL_WEATHER_ID = 0;
static final int COL_WEATHER_DATE = 1;
static final int COL_WEATHER_DESC = 2;
static final int COL_WEATHER_MAX_TEMP = 3;
static final int COL_WEATHER_MIN_TEMP = 4;
static final int COL_LOCATION_SETTING = 5;
static final int COL_WEATHER_CONDITION_ID = 6;
static final int COL_COORD_LAT = 7;
static final int COL_COORD_LONG = 8;
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface Callback {
/**
* DetailFragmentCallback for when an item has been selected.
*/
public void onItemSelected(Uri dateUri);
}
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;
// }
if (id == R.id.action_map) {
openPreferredLocationInMap();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// The ForecastAdapter will take data from a source and
// use it to populate the ListView it's attached to.
mForecastAdapter = new ForecastAdapter(getActivity(), null, 0);
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// Get a reference to the ListView, and attach this adapter to it.
mListView = (ListView) rootView.findViewById(R.id.listview_forecast);
mListView.setAdapter(mForecastAdapter);
// We'll call our MainActivity
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
// CursorAdapter returns a cursor at the correct position for getItem(), or null
// if it cannot seek to that position.
Cursor cursor = (Cursor) adapterView.getItemAtPosition(position);
if (cursor != null) {
String locationSetting = Utility.getPreferredLocation(getActivity());
((Callback) getActivity())
.onItemSelected(WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
locationSetting, cursor.getLong(COL_WEATHER_DATE)
));
}
mPosition = position;
}
});
// If there's instance state, mine it for useful information.
// The end-goal here is that the user never knows that turning their device sideways
// does crazy lifecycle related things. It should feel like some stuff stretched out,
// or magically appeared to take advantage of room, but data or place in the app was never
// actually *lost*.
if (savedInstanceState != null && savedInstanceState.containsKey(SELECTED_KEY)) {
// The listview probably hasn't even been populated yet. Actually perform the
// swapout in onLoadFinished.
mPosition = savedInstanceState.getInt(SELECTED_KEY);
}
mForecastAdapter.setUseTodayLayout(mUseTodayLayout);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
getLoaderManager().initLoader(FORECAST_LOADER, null, this);
super.onActivityCreated(savedInstanceState);
}
// since we read the location when we create the loader, all we need to do is restart things
void onLocationChanged( ) {
getLoaderManager().restartLoader(FORECAST_LOADER, null, this);
}
private void openPreferredLocationInMap() {
// 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
if ( null != mForecastAdapter ) {
Cursor c = mForecastAdapter.getCursor();
if ( null != c ) {
c.moveToPosition(0);
String posLat = c.getString(COL_COORD_LAT);
String posLong = c.getString(COL_COORD_LONG);
Uri geoLocation = Uri.parse("geo:" + posLat + "," + posLong);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(geoLocation);
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivity(intent);
} else {
Log.d(LOG_TAG, "Couldn't call " + geoLocation.toString() + ", no receiving apps installed!");
}
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
// When tablets rotate, the currently selected list item needs to be saved.
// When no item is selected, mPosition will be set to Listview.INVALID_POSITION,
// so check for that before storing.
if (mPosition != ListView.INVALID_POSITION) {
outState.putInt(SELECTED_KEY, mPosition);
}
super.onSaveInstanceState(outState);
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
// This is called when a new Loader needs to be created. This
// fragment only uses one loader, so we don't care about checking the id.
// To only show current and future dates, filter the query to return weather only for
// dates after or including today.
// Sort order: Ascending, by date.
String sortOrder = WeatherContract.WeatherEntry.COLUMN_DATE + " ASC";
String locationSetting = Utility.getPreferredLocation(getActivity());
Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
locationSetting, System.currentTimeMillis());
return new CursorLoader(getActivity(),
weatherForLocationUri,
FORECAST_COLUMNS,
null,
null,
sortOrder);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mForecastAdapter.swapCursor(data);
if (mPosition != ListView.INVALID_POSITION) {
// If we don't need to restart the loader, and there's a desired position to restore
// to, do so now.
mListView.smoothScrollToPosition(mPosition);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mForecastAdapter.swapCursor(null);
}
public void setUseTodayLayout(boolean useTodayLayout) {
mUseTodayLayout = useTodayLayout;
if (mForecastAdapter != null) {
mForecastAdapter.setUseTodayLayout(mUseTodayLayout);
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2015 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.net.Uri;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
public class MainActivity extends ActionBarActivity implements ForecastFragment.Callback {
private final String LOG_TAG = MainActivity.class.getSimpleName();
private static final String DETAILFRAGMENT_TAG = "DFTAG";
private boolean mTwoPane;
private String mLocation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLocation = Utility.getPreferredLocation(this);
setContentView(R.layout.activity_main);
if (findViewById(R.id.weather_detail_container) != null) {
// The detail container view will be present only in the large-screen layouts
// (res/layout-sw600dp). If this view is present, then the activity should be
// in two-pane mode.
mTwoPane = true;
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.weather_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG)
.commit();
}
} else {
mTwoPane = false;
getSupportActionBar().setElevation(0f);
}
ForecastFragment forecastFragment = ((ForecastFragment)getSupportFragmentManager()
.findFragmentById(R.id.fragment_forecast));
forecastFragment.setUseTodayLayout(!mTwoPane);
SunshineSyncAdapter.initializeSyncAdapter(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, 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);
}
@Override
protected void onResume() {
super.onResume();
String location = Utility.getPreferredLocation( this );
// update the location in our second pane using the fragment manager
if (location != null && !location.equals(mLocation)) {
ForecastFragment ff = (ForecastFragment)getSupportFragmentManager().findFragmentById(R.id.fragment_forecast);
if ( null != ff ) {
ff.onLocationChanged();
}
DetailFragment df = (DetailFragment)getSupportFragmentManager().findFragmentByTag(DETAILFRAGMENT_TAG);
if ( null != df ) {
df.onLocationChanged(location);
}
mLocation = location;
}
}
@Override
public void onItemSelected(Uri contentUri) {
if (mTwoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle args = new Bundle();
args.putParcelable(DetailFragment.DETAIL_URI, contentUri);
DetailFragment fragment = new DetailFragment();
fragment.setArguments(args);
getSupportFragmentManager().beginTransaction()
.replace(R.id.weather_detail_container, fragment, DETAILFRAGMENT_TAG)
.commit();
} else {
Intent intent = new Intent(this, DetailActivity.class)
.setData(contentUri);
startActivity(intent);
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2015 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.annotation.TargetApi;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
/**
* A {@link PreferenceActivity} that presents a set of application settings.
* <p>
* See <a href="http://developer.android.com/design/patterns/settings.html">
* Android Design: Settings</a> for design guidelines and the <a
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
* API Guide</a> for more information on developing a Settings UI.
*/
public class SettingsActivity extends PreferenceActivity
implements Preference.OnPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
@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)));
}
// Registers a shared preference change listener that gets notified when preferences change
@Override
protected void onResume() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.registerOnSharedPreferenceChangeListener(this);
super.onResume();
}
// Unregisters a shared preference change listener
@Override
protected void onPause() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
/**
* 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);
// Set the preference summaries
setPreferenceSummary(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
private void setPreferenceSummary(Preference preference, Object value) {
String stringValue = value.toString();
String key = preference.getKey();
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);
}
}
// This gets called before the preference is changed
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
setPreferenceSummary(preference, value);
return true;
}
// This gets called after the preference is changed, which is important because we
// start our synchronization here
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if ( key.equals(getString(R.string.pref_location_key)) ) {
SunshineSyncAdapter.syncImmediately(this);
} else if ( key.equals(getString(R.string.pref_units_key)) ) {
// units have changed. update lists of weather entries accordingly
getContentResolver().notifyChange(WeatherContract.WeatherEntry.CONTENT_URI, null);
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public Intent getParentActivityIntent() {
return super.getParentActivityIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sunshine.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.format.Time;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Utility {
public static String getPreferredLocation(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getString(context.getString(R.string.pref_location_key),
context.getString(R.string.pref_location_default));
}
public static boolean isMetric(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getString(context.getString(R.string.pref_units_key),
context.getString(R.string.pref_units_metric))
.equals(context.getString(R.string.pref_units_metric));
}
public static String formatTemperature(Context context, double temperature) {
// Data stored in Celsius by default. If user prefers to see in Fahrenheit, convert
// the values here.
String suffix = "\u00B0";
if (!isMetric(context)) {
temperature = (temperature * 1.8) + 32;
}
// For presentation, assume the user doesn't care about tenths of a degree.
return String.format(context.getString(R.string.format_temperature), temperature);
}
static String formatDate(long dateInMilliseconds) {
Date date = new Date(dateInMilliseconds);
return DateFormat.getDateInstance().format(date);
}
// Format used for storing dates in the database. ALso used for converting those strings
// back into date objects for comparison/processing.
public static final String DATE_FORMAT = "yyyyMMdd";
/**
* Helper method to convert the database representation of the date into something to display
* to users. As classy and polished a user experience as "20140102" is, we can do better.
*
* @param context Context to use for resource localization
* @param dateInMillis The date in milliseconds
* @return a user-friendly representation of the date.
*/
public static String getFriendlyDayString(Context context, long dateInMillis) {
// The day string for forecast uses the following logic:
// For today: "Today, June 8"
// For tomorrow: "Tomorrow"
// For the next 5 days: "Wednesday" (just the day name)
// For all days after that: "Mon Jun 8"
Time time = new Time();
time.setToNow();
long currentTime = System.currentTimeMillis();
int julianDay = Time.getJulianDay(dateInMillis, time.gmtoff);
int currentJulianDay = Time.getJulianDay(currentTime, time.gmtoff);
// If the date we're building the String for is today's date, the format
// is "Today, June 24"
if (julianDay == currentJulianDay) {
String today = context.getString(R.string.today);
int formatId = R.string.format_full_friendly_date;
return String.format(context.getString(
formatId,
today,
getFormattedMonthDay(context, dateInMillis)));
} else if ( julianDay < currentJulianDay + 7 ) {
// If the input date is less than a week in the future, just return the day name.
return getDayName(context, dateInMillis);
} else {
// Otherwise, use the form "Mon Jun 3"
SimpleDateFormat shortenedDateFormat = new SimpleDateFormat("EEE MMM dd");
return shortenedDateFormat.format(dateInMillis);
}
}
/**
* Given a day, returns just the name to use for that day.
* E.g "today", "tomorrow", "wednesday".
*
* @param context Context to use for resource localization
* @param dateInMillis The date in milliseconds
* @return
*/
public static String getDayName(Context context, long dateInMillis) {
// If the date is today, return the localized version of "Today" instead of the actual
// day name.
Time t = new Time();
t.setToNow();
int julianDay = Time.getJulianDay(dateInMillis, t.gmtoff);
int currentJulianDay = Time.getJulianDay(System.currentTimeMillis(), t.gmtoff);
if (julianDay == currentJulianDay) {
return context.getString(R.string.today);
} else if ( julianDay == currentJulianDay +1 ) {
return context.getString(R.string.tomorrow);
} else {
Time time = new Time();
time.setToNow();
// Otherwise, the format is just the day of the week (e.g "Wednesday".
SimpleDateFormat dayFormat = new SimpleDateFormat("EEEE");
return dayFormat.format(dateInMillis);
}
}
/**
* Converts db date format to the format "Month day", e.g "June 24".
* @param context Context to use for resource localization
* @param dateInMillis The db formatted date string, expected to be of the form specified
* in Utility.DATE_FORMAT
* @return The day in the form of a string formatted "December 6"
*/
public static String getFormattedMonthDay(Context context, long dateInMillis ) {
Time time = new Time();
time.setToNow();
SimpleDateFormat dbDateFormat = new SimpleDateFormat(Utility.DATE_FORMAT);
SimpleDateFormat monthDayFormat = new SimpleDateFormat("MMMM dd");
String monthDayString = monthDayFormat.format(dateInMillis);
return monthDayString;
}
public static String getFormattedWind(Context context, float windSpeed, float degrees) {
int windFormat;
if (Utility.isMetric(context)) {
windFormat = R.string.format_wind_kmh;
} else {
windFormat = R.string.format_wind_mph;
windSpeed = .621371192237334f * windSpeed;
}
// From wind direction in degrees, determine compass direction as a string (e.g NW)
// You know what's fun, writing really long if/else statements with tons of possible
// conditions. Seriously, try it!
String direction = "Unknown";
if (degrees >= 337.5 || degrees < 22.5) {
direction = "N";
} else if (degrees >= 22.5 && degrees < 67.5) {
direction = "NE";
} else if (degrees >= 67.5 && degrees < 112.5) {
direction = "E";
} else if (degrees >= 112.5 && degrees < 157.5) {
direction = "SE";
} else if (degrees >= 157.5 && degrees < 202.5) {
direction = "S";
} else if (degrees >= 202.5 && degrees < 247.5) {
direction = "SW";
} else if (degrees >= 247.5 && degrees < 292.5) {
direction = "W";
} else if (degrees >= 292.5 && degrees < 337.5) {
direction = "NW";
}
return String.format(context.getString(windFormat), windSpeed, direction);
}
/**
* Helper method to provide the icon resource id according to the weather condition id returned
* by the OpenWeatherMap call.
* @param weatherId from OpenWeatherMap API response
* @return resource id for the corresponding icon. -1 if no relation is found.
*/
public static int getIconResourceForWeatherCondition(int weatherId) {
// Based on weather code data found at:
// http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
if (weatherId >= 200 && weatherId <= 232) {
return R.drawable.ic_storm;
} else if (weatherId >= 300 && weatherId <= 321) {
return R.drawable.ic_light_rain;
} else if (weatherId >= 500 && weatherId <= 504) {
return R.drawable.ic_rain;
} else if (weatherId == 511) {
return R.drawable.ic_snow;
} else if (weatherId >= 520 && weatherId <= 531) {
return R.drawable.ic_rain;
} else if (weatherId >= 600 && weatherId <= 622) {
return R.drawable.ic_snow;
} else if (weatherId >= 701 && weatherId <= 761) {
return R.drawable.ic_fog;
} else if (weatherId == 761 || weatherId == 781) {
return R.drawable.ic_storm;
} else if (weatherId == 800) {
return R.drawable.ic_clear;
} else if (weatherId == 801) {
return R.drawable.ic_light_clouds;
} else if (weatherId >= 802 && weatherId <= 804) {
return R.drawable.ic_cloudy;
}
return -1;
}
/**
* Helper method to provide the art resource id according to the weather condition id returned
* by the OpenWeatherMap call.
* @param weatherId from OpenWeatherMap API response
* @return resource id for the corresponding icon. -1 if no relation is found.
*/
public static int getArtResourceForWeatherCondition(int weatherId) {
// Based on weather code data found at:
// http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
if (weatherId >= 200 && weatherId <= 232) {
return R.drawable.art_storm;
} else if (weatherId >= 300 && weatherId <= 321) {
return R.drawable.art_light_rain;
} else if (weatherId >= 500 && weatherId <= 504) {
return R.drawable.art_rain;
} else if (weatherId == 511) {
return R.drawable.art_snow;
} else if (weatherId >= 520 && weatherId <= 531) {
return R.drawable.art_rain;
} else if (weatherId >= 600 && weatherId <= 622) {
return R.drawable.art_snow;
} else if (weatherId >= 701 && weatherId <= 761) {
return R.drawable.art_fog;
} else if (weatherId == 761 || weatherId == 781) {
return R.drawable.art_storm;
} else if (weatherId == 800) {
return R.drawable.art_clear;
} else if (weatherId == 801) {
return R.drawable.art_light_clouds;
} else if (weatherId >= 802 && weatherId <= 804) {
return R.drawable.art_clouds;
}
return -1;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sunshine.app.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.format.Time;
/**
* Defines table and column names for the weather database.
*/
public class WeatherContract {
// The "Content authority" is a name for the entire content provider, similar to the
// relationship between a domain name and its website. A convenient string to use for the
// content authority is the package name for the app, which is guaranteed to be unique on the
// device.
public static final String CONTENT_AUTHORITY = "com.example.android.sunshine.app";
// Use CONTENT_AUTHORITY to create the base of all URI's which apps will use to contact
// the content provider.
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
// Possible paths (appended to base content URI for possible URI's)
// For instance, content://com.example.android.sunshine.app/weather/ is a valid path for
// looking at weather data. content://com.example.android.sunshine.app/givemeroot/ will fail,
// as the ContentProvider hasn't been given any information on what to do with "givemeroot".
// At least, let's hope not. Don't be that dev, reader. Don't be that dev.
public static final String PATH_WEATHER = "weather";
public static final String PATH_LOCATION = "location";
// To make it easy to query for the exact date, we normalize all dates that go into
// the database to the start of the the Julian day at UTC.
public static long normalizeDate(long startDate) {
// normalize the start date to the beginning of the (UTC) day
Time time = new Time();
time.set(startDate);
int julianDay = Time.getJulianDay(startDate, time.gmtoff);
return time.setJulianDay(julianDay);
}
/* Inner class that defines the table contents of the location table */
public static final class LocationEntry implements BaseColumns {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_LOCATION).build();
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_LOCATION;
// Table name
public static final String TABLE_NAME = "location";
// The location setting string is what will be sent to openweathermap
// as the location query.
public static final String COLUMN_LOCATION_SETTING = "location_setting";
// Human readable location string, provided by the API. Because for styling,
// "Mountain View" is more recognizable than 94043.
public static final String COLUMN_CITY_NAME = "city_name";
// In order to uniquely pinpoint the location on the map when we launch the
// map intent, we store the latitude and longitude as returned by openweathermap.
public static final String COLUMN_COORD_LAT = "coord_lat";
public static final String COLUMN_COORD_LONG = "coord_long";
public static Uri buildLocationUri(long id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
}
/* Inner class that defines the table contents of the weather table */
public static final class WeatherEntry implements BaseColumns {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_WEATHER).build();
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;
public static final String TABLE_NAME = "weather";
// Column with the foreign key into the location table.
public static final String COLUMN_LOC_KEY = "location_id";
// Date, stored as long in milliseconds since the epoch
public static final String COLUMN_DATE = "date";
// Weather id as returned by API, to identify the icon to be used
public static final String COLUMN_WEATHER_ID = "weather_id";
// Short description and long description of the weather, as provided by API.
// e.g "clear" vs "sky is clear".
public static final String COLUMN_SHORT_DESC = "short_desc";
// Min and max temperatures for the day (stored as floats)
public static final String COLUMN_MIN_TEMP = "min";
public static final String COLUMN_MAX_TEMP = "max";
// Humidity is stored as a float representing percentage
public static final String COLUMN_HUMIDITY = "humidity";
// Humidity is stored as a float representing percentage
public static final String COLUMN_PRESSURE = "pressure";
// Windspeed is stored as a float representing windspeed mph
public static final String COLUMN_WIND_SPEED = "wind";
// Degrees are meteorological degrees (e.g, 0 is north, 180 is south). Stored as floats.
public static final String COLUMN_DEGREES = "degrees";
public static Uri buildWeatherUri(long id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
/*
Student: This is the buildWeatherLocation function you filled in.
*/
public static Uri buildWeatherLocation(String locationSetting) {
return CONTENT_URI.buildUpon().appendPath(locationSetting).build();
}
public static Uri buildWeatherLocationWithStartDate(
String locationSetting, long startDate) {
long normalizedDate = normalizeDate(startDate);
return CONTENT_URI.buildUpon().appendPath(locationSetting)
.appendQueryParameter(COLUMN_DATE, Long.toString(normalizedDate)).build();
}
public static Uri buildWeatherLocationWithDate(String locationSetting, long date) {
return CONTENT_URI.buildUpon().appendPath(locationSetting)
.appendPath(Long.toString(normalizeDate(date))).build();
}
public static String getLocationSettingFromUri(Uri uri) {
return uri.getPathSegments().get(1);
}
public static long getDateFromUri(Uri uri) {
return Long.parseLong(uri.getPathSegments().get(2));
}
public static long getStartDateFromUri(Uri uri) {
String dateString = uri.getQueryParameter(COLUMN_DATE);
if (null != dateString && dateString.length() > 0)
return Long.parseLong(dateString);
else
return 0;
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sunshine.app.data;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.example.android.sunshine.app.data.WeatherContract.LocationEntry;
import com.example.android.sunshine.app.data.WeatherContract.WeatherEntry;
/**
* Manages a local database for weather data.
*/
public class WeatherDbHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
private static final int DATABASE_VERSION = 2;
static final String DATABASE_NAME = "weather.db";
public WeatherDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
// Create a table to hold locations. A location consists of the string supplied in the
// location setting, the city name, and the latitude and longitude
final String SQL_CREATE_LOCATION_TABLE = "CREATE TABLE " + LocationEntry.TABLE_NAME + " (" +
LocationEntry._ID + " INTEGER PRIMARY KEY," +
LocationEntry.COLUMN_LOCATION_SETTING + " TEXT UNIQUE NOT NULL, " +
LocationEntry.COLUMN_CITY_NAME + " TEXT NOT NULL, " +
LocationEntry.COLUMN_COORD_LAT + " REAL NOT NULL, " +
LocationEntry.COLUMN_COORD_LONG + " REAL NOT NULL " +
" );";
final String SQL_CREATE_WEATHER_TABLE = "CREATE TABLE " + WeatherEntry.TABLE_NAME + " (" +
// Why AutoIncrement here, and not above?
// Unique keys will be auto-generated in either case. But for weather
// forecasting, it's reasonable to assume the user will want information
// for a certain date and all dates *following*, so the forecast data
// should be sorted accordingly.
WeatherEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
// the ID of the location entry associated with this weather data
WeatherEntry.COLUMN_LOC_KEY + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_DATE + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_SHORT_DESC + " TEXT NOT NULL, " +
WeatherEntry.COLUMN_WEATHER_ID + " INTEGER NOT NULL," +
WeatherEntry.COLUMN_MIN_TEMP + " REAL NOT NULL, " +
WeatherEntry.COLUMN_MAX_TEMP + " REAL NOT NULL, " +
WeatherEntry.COLUMN_HUMIDITY + " REAL NOT NULL, " +
WeatherEntry.COLUMN_PRESSURE + " REAL NOT NULL, " +
WeatherEntry.COLUMN_WIND_SPEED + " REAL NOT NULL, " +
WeatherEntry.COLUMN_DEGREES + " REAL NOT NULL, " +
// Set up the location column as a foreign key to location table.
" FOREIGN KEY (" + WeatherEntry.COLUMN_LOC_KEY + ") REFERENCES " +
LocationEntry.TABLE_NAME + " (" + LocationEntry._ID + "), " +
// To assure the application have just one weather entry per day
// per location, it's created a UNIQUE constraint with REPLACE strategy
" UNIQUE (" + WeatherEntry.COLUMN_DATE + ", " +
WeatherEntry.COLUMN_LOC_KEY + ") ON CONFLICT REPLACE);";
sqLiteDatabase.execSQL(SQL_CREATE_LOCATION_TABLE);
sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
// Note that this only fires if you change the version number for your database.
// It does NOT depend on the version number for your application.
// If you want to update the schema without wiping data, commenting out the next 2 lines
// should be your top priority before modifying this method.
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + LocationEntry.TABLE_NAME);
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WeatherEntry.TABLE_NAME);
onCreate(sqLiteDatabase);
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sunshine.app.data;
import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
public class WeatherProvider extends ContentProvider {
// The URI Matcher used by this content provider.
private static final UriMatcher sUriMatcher = buildUriMatcher();
private WeatherDbHelper mOpenHelper;
static final int WEATHER = 100;
static final int WEATHER_WITH_LOCATION = 101;
static final int WEATHER_WITH_LOCATION_AND_DATE = 102;
static final int LOCATION = 300;
private static final SQLiteQueryBuilder sWeatherByLocationSettingQueryBuilder;
static{
sWeatherByLocationSettingQueryBuilder = new SQLiteQueryBuilder();
//This is an inner join which looks like
//weather INNER JOIN location ON weather.location_id = location._id
sWeatherByLocationSettingQueryBuilder.setTables(
WeatherContract.WeatherEntry.TABLE_NAME + " INNER JOIN " +
WeatherContract.LocationEntry.TABLE_NAME +
" ON " + WeatherContract.WeatherEntry.TABLE_NAME +
"." + WeatherContract.WeatherEntry.COLUMN_LOC_KEY +
" = " + WeatherContract.LocationEntry.TABLE_NAME +
"." + WeatherContract.LocationEntry._ID);
}
//location.location_setting = ?
private static final String sLocationSettingSelection =
WeatherContract.LocationEntry.TABLE_NAME+
"." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? ";
//location.location_setting = ? AND date >= ?
private static final String sLocationSettingWithStartDateSelection =
WeatherContract.LocationEntry.TABLE_NAME+
"." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " +
WeatherContract.WeatherEntry.COLUMN_DATE + " >= ? ";
//location.location_setting = ? AND date = ?
private static final String sLocationSettingAndDaySelection =
WeatherContract.LocationEntry.TABLE_NAME +
"." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? AND " +
WeatherContract.WeatherEntry.COLUMN_DATE + " = ? ";
private Cursor getWeatherByLocationSetting(Uri uri, String[] projection, String sortOrder) {
String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri);
long startDate = WeatherContract.WeatherEntry.getStartDateFromUri(uri);
String[] selectionArgs;
String selection;
if (startDate == 0) {
selection = sLocationSettingSelection;
selectionArgs = new String[]{locationSetting};
} else {
selectionArgs = new String[]{locationSetting, Long.toString(startDate)};
selection = sLocationSettingWithStartDateSelection;
}
return sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(),
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
}
private Cursor getWeatherByLocationSettingAndDate(
Uri uri, String[] projection, String sortOrder) {
String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri);
long date = WeatherContract.WeatherEntry.getDateFromUri(uri);
return sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(),
projection,
sLocationSettingAndDaySelection,
new String[]{locationSetting, Long.toString(date)},
null,
null,
sortOrder
);
}
/*
Students: Here is where you need to create the UriMatcher. This UriMatcher will
match each URI to the WEATHER, WEATHER_WITH_LOCATION, WEATHER_WITH_LOCATION_AND_DATE,
and LOCATION integer constants defined above. You can test this by uncommenting the
testUriMatcher test within TestUriMatcher.
*/
static UriMatcher buildUriMatcher() {
// I know what you're thinking. Why create a UriMatcher when you can use regular
// expressions instead? Because you're not crazy, that's why.
// All paths added to the UriMatcher have a corresponding code to return when a match is
// found. The code passed into the constructor represents the code to return for the root
// URI. It's common to use NO_MATCH as the code for this case.
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = WeatherContract.CONTENT_AUTHORITY;
// For each type of URI you want to add, create a corresponding code.
matcher.addURI(authority, WeatherContract.PATH_WEATHER, WEATHER);
matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*", WEATHER_WITH_LOCATION);
matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*/#", WEATHER_WITH_LOCATION_AND_DATE);
matcher.addURI(authority, WeatherContract.PATH_LOCATION, LOCATION);
return matcher;
}
/*
Students: We've coded this for you. We just create a new WeatherDbHelper for later use
here.
*/
@Override
public boolean onCreate() {
mOpenHelper = new WeatherDbHelper(getContext());
return true;
}
/*
Students: Here's where you'll code the getType function that uses the UriMatcher. You can
test this by uncommenting testGetType in TestProvider.
*/
@Override
public String getType(Uri uri) {
// Use the Uri Matcher to determine what kind of URI this is.
final int match = sUriMatcher.match(uri);
switch (match) {
// Student: Uncomment and fill out these two cases
case WEATHER_WITH_LOCATION_AND_DATE:
return WeatherContract.WeatherEntry.CONTENT_ITEM_TYPE;
case WEATHER_WITH_LOCATION:
return WeatherContract.WeatherEntry.CONTENT_TYPE;
case WEATHER:
return WeatherContract.WeatherEntry.CONTENT_TYPE;
case LOCATION:
return WeatherContract.LocationEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Here's the switch statement that, given a URI, will determine what kind of request it is,
// and query the database accordingly.
Cursor retCursor;
switch (sUriMatcher.match(uri)) {
// "weather/*/*"
case WEATHER_WITH_LOCATION_AND_DATE:
{
retCursor = getWeatherByLocationSettingAndDate(uri, projection, sortOrder);
break;
}
// "weather/*"
case WEATHER_WITH_LOCATION: {
retCursor = getWeatherByLocationSetting(uri, projection, sortOrder);
break;
}
// "weather"
case WEATHER: {
retCursor = mOpenHelper.getReadableDatabase().query(
WeatherContract.WeatherEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
break;
}
// "location"
case LOCATION: {
retCursor = mOpenHelper.getReadableDatabase().query(
WeatherContract.LocationEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
retCursor.setNotificationUri(getContext().getContentResolver(), uri);
return retCursor;
}
/*
Student: Add the ability to insert Locations to the implementation of this function.
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
Uri returnUri;
switch (match) {
case WEATHER: {
normalizeDate(values);
long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, values);
if ( _id > 0 )
returnUri = WeatherContract.WeatherEntry.buildWeatherUri(_id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
}
case LOCATION: {
long _id = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, values);
if ( _id > 0 )
returnUri = WeatherContract.LocationEntry.buildLocationUri(_id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return returnUri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
int rowsDeleted;
// this makes delete all rows return the number of rows deleted
if ( null == selection ) selection = "1";
switch (match) {
case WEATHER:
rowsDeleted = db.delete(
WeatherContract.WeatherEntry.TABLE_NAME, selection, selectionArgs);
break;
case LOCATION:
rowsDeleted = db.delete(
WeatherContract.LocationEntry.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// Because a null deletes all rows
if (rowsDeleted != 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsDeleted;
}
private void normalizeDate(ContentValues values) {
// normalize the date value
if (values.containsKey(WeatherContract.WeatherEntry.COLUMN_DATE)) {
long dateValue = values.getAsLong(WeatherContract.WeatherEntry.COLUMN_DATE);
values.put(WeatherContract.WeatherEntry.COLUMN_DATE, WeatherContract.normalizeDate(dateValue));
}
}
@Override
public int update(
Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
int rowsUpdated;
switch (match) {
case WEATHER:
normalizeDate(values);
rowsUpdated = db.update(WeatherContract.WeatherEntry.TABLE_NAME, values, selection,
selectionArgs);
break;
case LOCATION:
rowsUpdated = db.update(WeatherContract.LocationEntry.TABLE_NAME, values, selection,
selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if (rowsUpdated != 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsUpdated;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
case WEATHER:
db.beginTransaction();
int returnCount = 0;
try {
for (ContentValues value : values) {
normalizeDate(value);
long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, value);
if (_id != -1) {
returnCount++;
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(uri, null);
return returnCount;
default:
return super.bulkInsert(uri, values);
}
}
// You do not need to call this method. This is a method specifically to assist the testing
// framework in running smoothly. You can read more at:
// http://developer.android.com/reference/android/content/ContentProvider.html#shutdown()
@Override
@TargetApi(11)
public void shutdown() {
mOpenHelper.close();
super.shutdown();
}
}

View File

@@ -0,0 +1,83 @@
package com.example.android.sunshine.app.sync;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.Bundle;
/**
* Manages "Authentication" to Sunshine's backend service. The SyncAdapter framework
* requires an authenticator object, so syncing to a service that doesn't need authentication
* typically means creating a stub authenticator like this one.
* This code is copied directly, in its entirety, from
* http://developer.android.com/training/sync-adapters/creating-authenticator.html
* Which is a pretty handy reference when creating your own syncadapters. Just sayin'.
*/
public class SunshineAuthenticator extends AbstractAccountAuthenticator {
public SunshineAuthenticator(Context context) {
super(context);
}
// No properties to edit.
@Override
public Bundle editProperties(
AccountAuthenticatorResponse r, String s) {
throw new UnsupportedOperationException();
}
// Because we're not actually adding an account to the device, just return null.
@Override
public Bundle addAccount(
AccountAuthenticatorResponse r,
String s,
String s2,
String[] strings,
Bundle bundle) throws NetworkErrorException {
return null;
}
// Ignore attempts to confirm credentials
@Override
public Bundle confirmCredentials(
AccountAuthenticatorResponse r,
Account account,
Bundle bundle) throws NetworkErrorException {
return null;
}
// Getting an authentication token is not supported
@Override
public Bundle getAuthToken(
AccountAuthenticatorResponse r,
Account account,
String s,
Bundle bundle) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
// Getting a label for the auth token is not supported
@Override
public String getAuthTokenLabel(String s) {
throw new UnsupportedOperationException();
}
// Updating user credentials is not supported
@Override
public Bundle updateCredentials(
AccountAuthenticatorResponse r,
Account account,
String s, Bundle bundle) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
// Checking features for the account is not supported
@Override
public Bundle hasFeatures(
AccountAuthenticatorResponse r,
Account account, String[] strings) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,28 @@
package com.example.android.sunshine.app.sync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
/**
* The service which allows the sync adapter framework to access the authenticator.
*/
public class SunshineAuthenticatorService extends Service {
// Instance field that stores the authenticator object
private SunshineAuthenticator mAuthenticator;
@Override
public void onCreate() {
// Create a new authenticator object
mAuthenticator = new SunshineAuthenticator(this);
}
/*
* When the system binds to this Service to make the RPC call
* return the authenticator's IBinder.
*/
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}

View File

@@ -0,0 +1,536 @@
package com.example.android.sunshine.app.sync;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncRequest;
import android.content.SyncResult;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.format.Time;
import android.util.Log;
import com.example.android.sunshine.app.MainActivity;
import com.example.android.sunshine.app.R;
import com.example.android.sunshine.app.Utility;
import com.example.android.sunshine.app.data.WeatherContract;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Vector;
public class SunshineSyncAdapter extends AbstractThreadedSyncAdapter {
public final String LOG_TAG = SunshineSyncAdapter.class.getSimpleName();
// Interval at which to sync with the weather, in seconds.
// 60 seconds (1 minute) * 180 = 3 hours
public static final int SYNC_INTERVAL = 60 * 180;
public static final int SYNC_FLEXTIME = SYNC_INTERVAL/3;
private static final long DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
private static final int WEATHER_NOTIFICATION_ID = 3004;
private static final String[] NOTIFY_WEATHER_PROJECTION = new String[] {
WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
WeatherContract.WeatherEntry.COLUMN_MIN_TEMP,
WeatherContract.WeatherEntry.COLUMN_SHORT_DESC
};
// these indices must match the projection
private static final int INDEX_WEATHER_ID = 0;
private static final int INDEX_MAX_TEMP = 1;
private static final int INDEX_MIN_TEMP = 2;
private static final int INDEX_SHORT_DESC = 3;
public SunshineSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.d(LOG_TAG, "Starting sync");
String locationQuery = Utility.getPreferredLocation(getContext());
// 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 = 14;
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";
Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon()
.appendQueryParameter(QUERY_PARAM, locationQuery)
.appendQueryParameter(FORMAT_PARAM, format)
.appendQueryParameter(UNITS_PARAM, units)
.appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
.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;
}
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;
}
forecastJsonStr = buffer.toString();
getWeatherDataFromJson(forecastJsonStr, locationQuery);
} catch (IOException e) {
Log.e(LOG_TAG, "Error ", e);
// If the code didn't successfully get the weather data, there's no point in attempting
// to parse it.
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
e.printStackTrace();
} 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;
}
/**
* 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 void getWeatherDataFromJson(String forecastJsonStr,
String locationSetting)
throws JSONException {
// Now we have a String representing the complete forecast in JSON Format.
// Fortunately parsing is easy: constructor takes the JSON string and converts it
// into an Object hierarchy for us.
// These are the names of the JSON objects that need to be extracted.
// Location information
final String OWM_CITY = "city";
final String OWM_CITY_NAME = "name";
final String OWM_COORD = "coord";
// Location coordinate
final String OWM_LATITUDE = "lat";
final String OWM_LONGITUDE = "lon";
// Weather information. Each day's forecast info is an element of the "list" array.
final String OWM_LIST = "list";
final String OWM_PRESSURE = "pressure";
final String OWM_HUMIDITY = "humidity";
final String OWM_WINDSPEED = "speed";
final String OWM_WIND_DIRECTION = "deg";
// All temperatures are children of the "temp" object.
final String OWM_TEMPERATURE = "temp";
final String OWM_MAX = "max";
final String OWM_MIN = "min";
final String OWM_WEATHER = "weather";
final String OWM_DESCRIPTION = "main";
final String OWM_WEATHER_ID = "id";
try {
JSONObject forecastJson = new JSONObject(forecastJsonStr);
JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST);
JSONObject cityJson = forecastJson.getJSONObject(OWM_CITY);
String cityName = cityJson.getString(OWM_CITY_NAME);
JSONObject cityCoord = cityJson.getJSONObject(OWM_COORD);
double cityLatitude = cityCoord.getDouble(OWM_LATITUDE);
double cityLongitude = cityCoord.getDouble(OWM_LONGITUDE);
long locationId = addLocation(locationSetting, cityName, cityLatitude, cityLongitude);
// Insert the new weather information into the database
Vector<ContentValues> cVVector = new Vector<ContentValues>(weatherArray.length());
// 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();
for(int i = 0; i < weatherArray.length(); i++) {
// These are the values that will be collected.
long dateTime;
double pressure;
int humidity;
double windSpeed;
double windDirection;
double high;
double low;
String description;
int weatherId;
// Get the JSON object representing the day
JSONObject dayForecast = weatherArray.getJSONObject(i);
// Cheating to convert this to UTC time, which is what we want anyhow
dateTime = dayTime.setJulianDay(julianStartDay+i);
pressure = dayForecast.getDouble(OWM_PRESSURE);
humidity = dayForecast.getInt(OWM_HUMIDITY);
windSpeed = dayForecast.getDouble(OWM_WINDSPEED);
windDirection = dayForecast.getDouble(OWM_WIND_DIRECTION);
// Description is in a child array called "weather", which is 1 element long.
// That element also contains a weather code.
JSONObject weatherObject =
dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0);
description = weatherObject.getString(OWM_DESCRIPTION);
weatherId = weatherObject.getInt(OWM_WEATHER_ID);
// 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);
high = temperatureObject.getDouble(OWM_MAX);
low = temperatureObject.getDouble(OWM_MIN);
ContentValues weatherValues = new ContentValues();
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationId);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATE, dateTime);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, humidity);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, pressure);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, windSpeed);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, windDirection);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, high);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, low);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, description);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, weatherId);
cVVector.add(weatherValues);
}
int inserted = 0;
// add to database
if ( cVVector.size() > 0 ) {
ContentValues[] cvArray = new ContentValues[cVVector.size()];
cVVector.toArray(cvArray);
getContext().getContentResolver().bulkInsert(WeatherContract.WeatherEntry.CONTENT_URI, cvArray);
// delete old data so we don't build up an endless history
getContext().getContentResolver().delete(WeatherContract.WeatherEntry.CONTENT_URI,
WeatherContract.WeatherEntry.COLUMN_DATE + " <= ?",
new String[] {Long.toString(dayTime.setJulianDay(julianStartDay-1))});
notifyWeather();
}
Log.d(LOG_TAG, "Sync Complete. " + cVVector.size() + " Inserted");
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
e.printStackTrace();
}
}
private void notifyWeather() {
Context context = getContext();
//checking the last update and notify if it' the first of the day
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String displayNotificationsKey = context.getString(R.string.pref_enable_notifications_key);
boolean displayNotifications = prefs.getBoolean(displayNotificationsKey,
Boolean.parseBoolean(context.getString(R.string.pref_enable_notifications_default)));
if ( displayNotifications ) {
String lastNotificationKey = context.getString(R.string.pref_last_notification);
long lastSync = prefs.getLong(lastNotificationKey, 0);
if (System.currentTimeMillis() - lastSync >= DAY_IN_MILLIS) {
// Last sync was more than 1 day ago, let's send a notification with the weather.
String locationQuery = Utility.getPreferredLocation(context);
Uri weatherUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(locationQuery, System.currentTimeMillis());
// we'll query our contentProvider, as always
Cursor cursor = context.getContentResolver().query(weatherUri, NOTIFY_WEATHER_PROJECTION, null, null, null);
if (cursor.moveToFirst()) {
int weatherId = cursor.getInt(INDEX_WEATHER_ID);
double high = cursor.getDouble(INDEX_MAX_TEMP);
double low = cursor.getDouble(INDEX_MIN_TEMP);
String desc = cursor.getString(INDEX_SHORT_DESC);
int iconId = Utility.getIconResourceForWeatherCondition(weatherId);
Resources resources = context.getResources();
Bitmap largeIcon = BitmapFactory.decodeResource(resources,
Utility.getArtResourceForWeatherCondition(weatherId));
String title = context.getString(R.string.app_name);
// Define the text of the forecast.
String contentText = String.format(context.getString(R.string.format_notification),
desc,
Utility.formatTemperature(context, high),
Utility.formatTemperature(context, low));
// NotificationCompatBuilder is a very convenient way to build backward-compatible
// notifications. Just throw in some data.
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(getContext())
.setColor(resources.getColor(R.color.sunshine_light_blue))
.setSmallIcon(iconId)
.setLargeIcon(largeIcon)
.setContentTitle(title)
.setContentText(contentText);
// Make something interesting happen when the user clicks on the notification.
// In this case, opening the app is sufficient.
Intent resultIntent = new Intent(context, MainActivity.class);
// The stack builder object will contain an artificial back stack for the
// started Activity.
// This ensures that navigating backward from the Activity leads out of
// your application to the Home screen.
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(
0,
PendingIntent.FLAG_UPDATE_CURRENT
);
mBuilder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
(NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
// WEATHER_NOTIFICATION_ID allows you to update the notification later on.
mNotificationManager.notify(WEATHER_NOTIFICATION_ID, mBuilder.build());
//refreshing last sync
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(lastNotificationKey, System.currentTimeMillis());
editor.commit();
}
cursor.close();
}
}
}
/**
* Helper method to handle insertion of a new location in the weather database.
*
* @param locationSetting The location string used to request updates from the server.
* @param cityName A human-readable city name, e.g "Mountain View"
* @param lat the latitude of the city
* @param lon the longitude of the city
* @return the row ID of the added location.
*/
long addLocation(String locationSetting, String cityName, double lat, double lon) {
long locationId;
// First, check if the location with this city name exists in the db
Cursor locationCursor = getContext().getContentResolver().query(
WeatherContract.LocationEntry.CONTENT_URI,
new String[]{WeatherContract.LocationEntry._ID},
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?",
new String[]{locationSetting},
null);
if (locationCursor.moveToFirst()) {
int locationIdIndex = locationCursor.getColumnIndex(WeatherContract.LocationEntry._ID);
locationId = locationCursor.getLong(locationIdIndex);
} else {
// Now that the content provider is set up, inserting rows of data is pretty simple.
// First create a ContentValues object to hold the data you want to insert.
ContentValues locationValues = new ContentValues();
// Then add the data, along with the corresponding name of the data type,
// so the content provider knows what kind of value is being inserted.
locationValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, cityName);
locationValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, locationSetting);
locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, lat);
locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, lon);
// Finally, insert location data into the database.
Uri insertedUri = getContext().getContentResolver().insert(
WeatherContract.LocationEntry.CONTENT_URI,
locationValues
);
// The resulting URI contains the ID for the row. Extract the locationId from the Uri.
locationId = ContentUris.parseId(insertedUri);
}
locationCursor.close();
// Wait, that worked? Yes!
return locationId;
}
/**
* Helper method to schedule the sync adapter periodic execution
*/
public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
Account account = getSyncAccount(context);
String authority = context.getString(R.string.content_authority);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// we can enable inexact timers in our periodic sync
SyncRequest request = new SyncRequest.Builder().
syncPeriodic(syncInterval, flexTime).
setSyncAdapter(account, authority).
setExtras(new Bundle()).build();
ContentResolver.requestSync(request);
} else {
ContentResolver.addPeriodicSync(account,
authority, new Bundle(), syncInterval);
}
}
/**
* Helper method to have the sync adapter sync immediately
* @param context The context used to access the account service
*/
public static void syncImmediately(Context context) {
Bundle bundle = new Bundle();
bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(getSyncAccount(context),
context.getString(R.string.content_authority), bundle);
}
/**
* Helper method to get the fake account to be used with SyncAdapter, or make a new one
* if the fake account doesn't exist yet. If we make a new account, we call the
* onAccountCreated method so we can initialize things.
*
* @param context The context used to access the account service
* @return a fake account.
*/
public static Account getSyncAccount(Context context) {
// Get an instance of the Android account manager
AccountManager accountManager =
(AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
// Create the account type and default account
Account newAccount = new Account(
context.getString(R.string.app_name), context.getString(R.string.sync_account_type));
// If the password doesn't exist, the account doesn't exist
if ( null == accountManager.getPassword(newAccount) ) {
/*
* Add the account and account type, no password or user data
* If successful, return the Account object, otherwise report an error.
*/
if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
return null;
}
/*
* If you don't set android:syncable="true" in
* in your <provider> element in the manifest,
* then call ContentResolver.setIsSyncable(account, AUTHORITY, 1)
* here.
*/
onAccountCreated(newAccount, context);
}
return newAccount;
}
private static void onAccountCreated(Account newAccount, Context context) {
/*
* Since we've created an account
*/
SunshineSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);
/*
* Without calling setSyncAutomatically, our periodic sync will not be enabled.
*/
ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);
/*
* Finally, let's do a sync to get things started
*/
syncImmediately(context);
}
public static void initializeSyncAdapter(Context context) {
getSyncAccount(context);
}
}

View File

@@ -0,0 +1,26 @@
package com.example.android.sunshine.app.sync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class SunshineSyncService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static SunshineSyncAdapter sSunshineSyncAdapter = null;
@Override
public void onCreate() {
Log.d("SunshineSyncService", "onCreate - SunshineSyncService");
synchronized (sSyncAdapterLock) {
if (sSunshineSyncAdapter == null) {
sSunshineSyncAdapter = new SunshineSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSunshineSyncAdapter.getSyncAdapterBinder();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<ripple android:color="@color/grey" />
</item>
<item android:state_activated="true"
android:drawable="@color/sunshine_light_blue"/>
<item android:state_selected="true"
android:drawable="@color/sunshine_light_blue"/>
<item android:drawable="@color/sunshine_blue" />
</selector>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- State when a row is being pressed, but hasn't yet been activated (finger down) -->
<item android:state_pressed="true">
<ripple android:color="@color/grey" />
</item>
<!-- When the view is "activated". In SINGLE_CHOICE_MODE, it flags the active row
of a ListView -->
<item android:state_activated="true"
android:drawable="@color/sunshine_light_blue" />
<!-- Default, "just hangin' out" state. -->
<item android:drawable="@android:color/transparent" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show More