Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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