Skip to content
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ dependencies {
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
compile 'com.android.support:recyclerview-v7:22.2.0'
compile 'com.google.android.apps.muzei:muzei-api:2.0'
compile 'com.google.android.gms:play-services-gcm:7.5.0'
compile 'com.google.android.gms:play-services-location:7.5.0'
}
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
android:protectionLevel="signature" />
<uses-permission android:name="com.example.android.sunshine.app.permission.C2D_MESSAGE" />

<!-- Permissions required to use the Place Picker -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -113,6 +116,16 @@
<category android:name="com.example.android.sunshine.app" />
</intent-filter>
</receiver>
<!-- Muzei Extension -->
<service android:name=".muzei.WeatherMuzeiSource"
android:icon="@drawable/ic_muzei"
android:label="@string/app_name"
android:description="@string/muzei_description" >
<intent-filter>
<action android:name="com.google.android.apps.muzei.api.MuzeiArtSource" />
</intent-filter>
<meta-data android:name="color" android:value="@color/primary" />
</service>
<!-- Today Widget -->
<receiver
android:name=".widget.TodayWidgetProvider"
Expand All @@ -125,6 +138,7 @@
android:resource="@xml/widget_info_today" />
</receiver>
<service android:name=".widget.TodayWidgetIntentService" />
<!-- Detail Widget -->
<receiver
android:name=".widget.DetailWidgetProvider"
android:label="@string/title_widget_detail"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.example.android.sunshine.app;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
Expand All @@ -24,9 +25,17 @@
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.common.GooglePlayServicesRepairableException;
import com.google.android.gms.location.places.ui.PlacePicker;

public class LocationEditTextPreference extends EditTextPreference {
static final private int DEFAULT_MINIMUM_LOCATION_LENGTH = 2;
private int mMinLength;
Expand All @@ -42,8 +51,57 @@ public LocationEditTextPreference(Context context, AttributeSet attrs) {
} finally {
a.recycle();
}

// Check to see if Google Play services is available. The Place Picker API is available
// through Google Play services, so if this is false, we'll just carry on as though this
// feature does not exist. If it is true, however, we can add a widget to our preference.
GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
int resultCode = apiAvailability.isGooglePlayServicesAvailable(getContext());
if (resultCode == ConnectionResult.SUCCESS) {
// Add the get current location widget to our location preference
setWidgetLayoutResource(R.layout.pref_current_location);
}
}

@Override
protected View onCreateView(ViewGroup parent) {
View view = super.onCreateView(parent);
View currentLocation = view.findViewById(R.id.current_location);
currentLocation.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Context context = getContext();

// Launch the Place Picker so that the user can specify their location, and then
// return the result to SettingsActivity.
PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder();


// We are in a view right now, not an activity. So we need to get ourselves
// an activity that we can use to start our Place Picker intent. By using
// SettingsActivity in this way, we can ensure the result of the Place Picker
// intent comes to the right place for us to process it.
Activity settingsActivity = (SettingsActivity) context;
try {
settingsActivity.startActivityForResult(
builder.build(context), SettingsActivity.PLACE_PICKER_REQUEST);

} catch (GooglePlayServicesNotAvailableException
| GooglePlayServicesRepairableException e) {
// What did you do?? This is why we check Google Play services in onResume!!!
// The difference in these exception types is the difference between pausing
// for a moment to prompt the user to update/install/enable Play services vs
// complete and utter failure.
// If you prefer to manage Google Play services dynamically, then you can do so
// by responding to these exceptions in the right moment. But I prefer a cleaner
// user experience, which is why you check all of this when the app resumes,
// and then disable/enable features based on that availability.
}
}
});

return view;
}

@Override
protected void showDialog(Bundle state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.text.TextUtils;

import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
import com.google.android.gms.location.places.Place;
import com.google.android.gms.location.places.ui.PlacePicker;
import com.google.android.gms.maps.model.LatLng;

/**
* A {@link PreferenceActivity} that presents a set of application settings.
Expand All @@ -38,6 +42,7 @@
*/
public class SettingsActivity extends PreferenceActivity
implements Preference.OnPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
protected final static int PLACE_PICKER_REQUEST = 9090;

@Override
public void onCreate(Bundle savedInstanceState) {
Expand Down Expand Up @@ -133,7 +138,12 @@ public boolean onPreferenceChange(Preference preference, Object value) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if ( key.equals(getString(R.string.pref_location_key)) ) {
// we've changed the location
// first clear locationStatus
// Wipe out any potential PlacePicker latlng values so that we can use this text entry.
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(getString(R.string.pref_location_latitude));
editor.remove(getString(R.string.pref_location_longitude));
editor.commit();

Utility.resetLocationStatus(this);
SunshineSyncAdapter.syncImmediately(this);
} else if ( key.equals(getString(R.string.pref_units_key)) ) {
Expand All @@ -154,4 +164,48 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin
public Intent getParentActivityIntent() {
return super.getParentActivityIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check to see if the result is from our Place Picker intent
if (requestCode == PLACE_PICKER_REQUEST) {
// Make sure the request was successful
if (resultCode == RESULT_OK) {
Place place = PlacePicker.getPlace(data, this);
String address = place.getAddress().toString();
LatLng latLong = place.getLatLng();

// If the provided place doesn't have an address, we'll form a display-friendly
// string from the latlng values.
if (TextUtils.isEmpty(address)) {
address = String.format("(%.2f, %.2f)",latLong.latitude, latLong.longitude);
}

SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(getString(R.string.pref_location_key), address);

// Also store the latitude and longitude so that we can use these to get a precise
// result from our weather service. We cannot expect the weather service to
// understand addresses that Google formats.
editor.putFloat(getString(R.string.pref_location_latitude),
(float) latLong.latitude);
editor.putFloat(getString(R.string.pref_location_longitude),
(float) latLong.longitude);
editor.commit();

// Tell the SyncAdapter that we've changed the location, so that we can update
// our UI with new values. We need to do this manually because we are responding
// to the PlacePicker widget result here instead of allowing the
// LocationEditTextPreference to handle these changes and invoke our callbacks.
Preference locationPreference = findPreference(getString(R.string.pref_location_key));
setPreferenceSummary(locationPreference, address);
Utility.resetLocationStatus(this);
SunshineSyncAdapter.syncImmediately(this);
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@
import java.util.Locale;

public class Utility {
// We'll default our latlong to 0. Yay, "Earth!"
public static float DEFAULT_LATLONG = 0F;

public static boolean isLocationLatLonAvailable(Context context) {
SharedPreferences prefs
= PreferenceManager.getDefaultSharedPreferences(context);
return prefs.contains(context.getString(R.string.pref_location_latitude))
&& prefs.contains(context.getString(R.string.pref_location_longitude));
}

public static float getLocationLatitude(Context context) {
SharedPreferences prefs
= PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getFloat(context.getString(R.string.pref_location_latitude),
DEFAULT_LATLONG);
}

public static float getLocationLongitude(Context context) {
SharedPreferences prefs
= PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getFloat(context.getString(R.string.pref_location_longitude),
DEFAULT_LATLONG);
}

public static String getPreferredLocation(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getString(context.getString(R.string.pref_location_key),
Expand Down Expand Up @@ -339,7 +363,7 @@ public static String getStringForWeatherCondition(Context context, int weatherId
stringId = R.string.condition_2xx;
} else if (weatherId >= 300 && weatherId <= 321) {
stringId = R.string.condition_3xx;
} else switch(weatherId) {
} else switch (weatherId) {
case 500:
stringId = R.string.condition_500;
break;
Expand Down Expand Up @@ -502,6 +526,42 @@ public static String getStringForWeatherCondition(Context context, int weatherId
return context.getString(stringId);
}

/*
* Helper method to provide the correct image according to the weather condition id returned
* by the OpenWeatherMap call.
*
* @param weatherId from OpenWeatherMap API response
* @return A string URL to an appropriate image or null if no mapping is found
*/
public static String getImageUrlForWeatherCondition(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 "http://upload.wikimedia.org/wikipedia/commons/2/28/Thunderstorm_in_Annemasse,_France.jpg";
} else if (weatherId >= 300 && weatherId <= 321) {
return "http://upload.wikimedia.org/wikipedia/commons/a/a0/Rain_on_leaf_504605006.jpg";
} else if (weatherId >= 500 && weatherId <= 504) {
return "http://upload.wikimedia.org/wikipedia/commons/6/6c/Rain-on-Thassos.jpg";
} else if (weatherId == 511) {
return "http://upload.wikimedia.org/wikipedia/commons/b/b8/Fresh_snow.JPG";
} else if (weatherId >= 520 && weatherId <= 531) {
return "http://upload.wikimedia.org/wikipedia/commons/6/6c/Rain-on-Thassos.jpg";
} else if (weatherId >= 600 && weatherId <= 622) {
return "http://upload.wikimedia.org/wikipedia/commons/b/b8/Fresh_snow.JPG";
} else if (weatherId >= 701 && weatherId <= 761) {
return "http://upload.wikimedia.org/wikipedia/commons/e/e6/Westminster_fog_-_London_-_UK.jpg";
} else if (weatherId == 761 || weatherId == 781) {
return "http://upload.wikimedia.org/wikipedia/commons/d/dc/Raised_dust_ahead_of_a_severe_thunderstorm_1.jpg";
} else if (weatherId == 800) {
return "http://upload.wikimedia.org/wikipedia/commons/7/7e/A_few_trees_and_the_sun_(6009964513).jpg";
} else if (weatherId == 801) {
return "http://upload.wikimedia.org/wikipedia/commons/e/e7/Cloudy_Blue_Sky_(5031259890).jpg";
} else if (weatherId >= 802 && weatherId <= 804) {
return "http://upload.wikimedia.org/wikipedia/commons/5/54/Cloudy_hills_in_Elis,_Greece_2.jpg";
}
return null;
}

/**
* Returns true if the network is available or about to become available.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.muzei;

import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;

import com.example.android.sunshine.app.MainActivity;
import com.example.android.sunshine.app.Utility;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
import com.google.android.apps.muzei.api.Artwork;
import com.google.android.apps.muzei.api.MuzeiArtSource;

/**
* Muzei source that changes your background based on the current weather conditions
*/
public class WeatherMuzeiSource extends MuzeiArtSource {
private static final String[] FORECAST_COLUMNS = new String[]{
WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
WeatherContract.WeatherEntry.COLUMN_SHORT_DESC
};
// these indices must match the projection
private static final int INDEX_WEATHER_ID = 0;
private static final int INDEX_SHORT_DESC = 1;

public WeatherMuzeiSource() {
super("WeatherMuzeiSource");
}

@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
boolean dataUpdated = intent != null &&
SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction());
if (dataUpdated && isEnabled()) {
onUpdate(UPDATE_REASON_OTHER);
}
}

@Override
protected void onUpdate(int reason) {
String location = Utility.getPreferredLocation(this);
Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
location, System.currentTimeMillis());
Cursor cursor = getContentResolver().query(weatherForLocationUri, FORECAST_COLUMNS, null,
null, WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
if (cursor.moveToFirst()) {
int weatherId = cursor.getInt(INDEX_WEATHER_ID);
String desc = cursor.getString(INDEX_SHORT_DESC);

String imageUrl = Utility.getImageUrlForWeatherCondition(weatherId);
// Only publish a new wallpaper if we have a valid image
if (imageUrl != null) {
publishArtwork(new Artwork.Builder()
.imageUri(Uri.parse(imageUrl))
.title(desc)
.byline(location)
.viewIntent(new Intent(this, MainActivity.class))
.build());
}
}
cursor.close();
}
}
Loading