Initial Commit for Start of Class
43
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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.
|
||||||
46
README.md
@@ -1,2 +1,44 @@
|
|||||||
# Advanced_Android_Development
|
Advanced Android Sample App
|
||||||
Repo for the Advanced Android App Development course
|
===================================
|
||||||
|
|
||||||
|
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
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
25
app/build.gradle
Normal 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
@@ -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 *;
|
||||||
|
#}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
93
app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) { }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
249
app/src/main/java/com/example/android/sunshine/app/Utility.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable-hdpi/art_clear.png
Executable file
|
After Width: | Height: | Size: 36 KiB |
BIN
app/src/main/res/drawable-hdpi/art_clouds.png
Executable file
|
After Width: | Height: | Size: 31 KiB |
BIN
app/src/main/res/drawable-hdpi/art_fog.png
Executable file
|
After Width: | Height: | Size: 36 KiB |
BIN
app/src/main/res/drawable-hdpi/art_light_clouds.png
Executable file
|
After Width: | Height: | Size: 36 KiB |
BIN
app/src/main/res/drawable-hdpi/art_light_rain.png
Executable file
|
After Width: | Height: | Size: 34 KiB |
BIN
app/src/main/res/drawable-hdpi/art_rain.png
Executable file
|
After Width: | Height: | Size: 40 KiB |
BIN
app/src/main/res/drawable-hdpi/art_snow.png
Executable file
|
After Width: | Height: | Size: 32 KiB |
BIN
app/src/main/res/drawable-hdpi/art_storm.png
Executable file
|
After Width: | Height: | Size: 27 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_clear.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_cloudy.png
Executable file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_fog.png
Executable file
|
After Width: | Height: | Size: 990 B |
BIN
app/src/main/res/drawable-hdpi/ic_light_clouds.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_light_rain.png
Executable file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_logo.png
Executable file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_rain.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_snow.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_storm.png
Executable file
|
After Width: | Height: | Size: 854 B |
BIN
app/src/main/res/drawable-mdpi/art_clear.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/src/main/res/drawable-mdpi/art_clouds.png
Executable file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/drawable-mdpi/art_fog.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable-mdpi/art_light_clouds.png
Executable file
|
After Width: | Height: | Size: 19 KiB |
BIN
app/src/main/res/drawable-mdpi/art_light_rain.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable-mdpi/art_rain.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/drawable-mdpi/art_snow.png
Executable file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/drawable-mdpi/art_storm.png
Executable file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_clear.png
Executable file
|
After Width: | Height: | Size: 721 B |
BIN
app/src/main/res/drawable-mdpi/ic_cloudy.png
Executable file
|
After Width: | Height: | Size: 695 B |
BIN
app/src/main/res/drawable-mdpi/ic_fog.png
Executable file
|
After Width: | Height: | Size: 722 B |
BIN
app/src/main/res/drawable-mdpi/ic_light_clouds.png
Executable file
|
After Width: | Height: | Size: 825 B |
BIN
app/src/main/res/drawable-mdpi/ic_light_rain.png
Executable file
|
After Width: | Height: | Size: 755 B |
BIN
app/src/main/res/drawable-mdpi/ic_logo.png
Executable file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_rain.png
Executable file
|
After Width: | Height: | Size: 845 B |
BIN
app/src/main/res/drawable-mdpi/ic_snow.png
Executable file
|
After Width: | Height: | Size: 905 B |
BIN
app/src/main/res/drawable-mdpi/ic_storm.png
Executable file
|
After Width: | Height: | Size: 637 B |
29
app/src/main/res/drawable-v21/today_touch_selector.xml
Normal 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>
|
||||||
31
app/src/main/res/drawable-v21/touch_selector.xml
Normal 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>
|
||||||
BIN
app/src/main/res/drawable-xhdpi/art_clear.png
Executable file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_clouds.png
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_fog.png
Executable file
|
After Width: | Height: | Size: 58 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_light_clouds.png
Executable file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_light_rain.png
Executable file
|
After Width: | Height: | Size: 52 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_rain.png
Executable file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_snow.png
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/res/drawable-xhdpi/art_storm.png
Executable file
|
After Width: | Height: | Size: 43 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_clear.png
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_cloudy.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_fog.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_light_clouds.png
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_light_rain.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_logo.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_rain.png
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_snow.png
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_storm.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_clear.png
Executable file
|
After Width: | Height: | Size: 133 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_clouds.png
Executable file
|
After Width: | Height: | Size: 113 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_fog.png
Executable file
|
After Width: | Height: | Size: 135 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_light_clouds.png
Executable file
|
After Width: | Height: | Size: 127 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_light_rain.png
Executable file
|
After Width: | Height: | Size: 116 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_rain.png
Executable file
|
After Width: | Height: | Size: 136 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_snow.png
Executable file
|
After Width: | Height: | Size: 122 KiB |
BIN
app/src/main/res/drawable-xxhdpi/art_storm.png
Executable file
|
After Width: | Height: | Size: 100 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_clear.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_cloudy.png
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_fog.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_light_clouds.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_light_rain.png
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_logo.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_rain.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_snow.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_storm.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |