diff --git a/README.md b/README.md
index 25f28e20..388b7f00 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,14 @@ Pre-requisites
Android SDK 21 or Higher
Build Tools version 21.1.2
Android Support AppCompat 22.2.0
+Android Support Annotations 22.2.0
+Android Support GridLayout 22.2.0
+Android Support CardView 22.2.0
+Android Support Design 22.2.0
+Android Support RecyclerView 22.2.0
+Google Play Services GCM 7.0.0
+BumpTech Glide 3.5.2
+
Getting Started
---------------
diff --git a/app/build.gradle b/app/build.gradle
index 19167f95..79af6450 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
+apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 21
@@ -17,9 +18,21 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ buildTypes.each {
+ it.buildConfigField 'String', 'OPEN_WEATHER_MAP_API_KEY', MyOpenWeatherMapApiKey
+ }
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
- compile 'com.android.support:appcompat-v7:21.0.2'
+ compile 'com.github.bumptech.glide:glide:3.5.2'
+ compile 'com.android.support:support-annotations:22.2.0'
+ compile 'com.android.support:gridlayout-v7:22.2.0'
+ compile 'com.android.support:cardview-v7:22.2.0'
+ 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'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 93c77805..a8893d32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,15 +28,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ android:theme="@style/AppTheme"
+ android:supportsRtl="true">
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme.Main">
@@ -46,7 +61,8 @@
+ android:parentActivityName=".MainActivity"
+ android:theme="@style/AppTheme.Details">
@@ -88,6 +104,75 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java
index 2f327151..f223f49e 100644
--- a/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java
+++ b/app/src/main/java/com/example/android/sunshine/app/DetailActivity.java
@@ -16,13 +16,16 @@
package com.example.android.sunshine.app;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
-public class DetailActivity extends ActionBarActivity {
+public class DetailActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -35,6 +38,7 @@ protected void onCreate(Bundle savedInstanceState) {
Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());
+ arguments.putBoolean(DetailFragment.DETAIL_TRANSITION_ANIMATION, true);
DetailFragment fragment = new DetailFragment();
fragment.setArguments(arguments);
@@ -42,26 +46,9 @@ protected void onCreate(Bundle savedInstanceState) {
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;
+ // Being here means we are in animation mode
+ supportPostponeEnterTransition();
}
- return super.onOptionsItemSelected(item);
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/DetailFragment.java b/app/src/main/java/com/example/android/sunshine/app/DetailFragment.java
index 11b09d60..c6162a71 100644
--- a/app/src/main/java/com/example/android/sunshine/app/DetailFragment.java
+++ b/app/src/main/java/com/example/android/sunshine/app/DetailFragment.java
@@ -23,17 +23,21 @@
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.app.AppCompatActivity;
+import android.support.v7.widget.CardView;
import android.support.v7.widget.ShareActionProvider;
+import android.support.v7.widget.Toolbar;
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.view.ViewParent;
import android.widget.ImageView;
import android.widget.TextView;
+import com.bumptech.glide.Glide;
import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.data.WeatherContract.WeatherEntry;
@@ -44,12 +48,13 @@ public class DetailFragment extends Fragment implements LoaderManager.LoaderCall
private static final String LOG_TAG = DetailFragment.class.getSimpleName();
static final String DETAIL_URI = "URI";
+ static final String DETAIL_TRANSITION_ANIMATION = "DTA";
private static final String FORECAST_SHARE_HASHTAG = " #SunshineApp";
- private ShareActionProvider mShareActionProvider;
private String mForecast;
private Uri mUri;
+ private boolean mTransitionAnimation;
private static final int DETAIL_LOADER = 0;
@@ -83,14 +88,16 @@ public class DetailFragment extends Fragment implements LoaderManager.LoaderCall
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 mHumidityLabelView;
private TextView mWindView;
+ private TextView mWindLabelView;
private TextView mPressureView;
+ private TextView mPressureLabelView;
public DetailFragment() {
setHasOptionsMenu(true);
@@ -103,35 +110,36 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle arguments = getArguments();
if (arguments != null) {
mUri = arguments.getParcelable(DetailFragment.DETAIL_URI);
+ mTransitionAnimation = arguments.getBoolean(DetailFragment.DETAIL_TRANSITION_ANIMATION, false);
}
- View rootView = inflater.inflate(R.layout.fragment_detail, container, false);
+ View rootView = inflater.inflate(R.layout.fragment_detail_start, 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);
+ mHumidityLabelView = (TextView) rootView.findViewById(R.id.detail_humidity_label_textview);
mWindView = (TextView) rootView.findViewById(R.id.detail_wind_textview);
+ mWindLabelView = (TextView) rootView.findViewById(R.id.detail_wind_label_textview);
mPressureView = (TextView) rootView.findViewById(R.id.detail_pressure_textview);
+ mPressureLabelView = (TextView) rootView.findViewById(R.id.detail_pressure_label_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);
-
+ private void finishCreatingMenu(Menu menu) {
// Retrieve the share menu item
MenuItem menuItem = menu.findItem(R.id.action_share);
+ menuItem.setIntent(createShareForecastIntent());
+ }
- // 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());
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if ( getActivity() instanceof DetailActivity ){
+ // Inflate the menu; this adds items to the action bar if it is present.
+ inflater.inflate(R.menu.detailfragment, menu);
+ finishCreatingMenu(menu);
}
}
@@ -174,31 +182,50 @@ public Loader onCreateLoader(int id, Bundle args) {
null
);
}
+ ViewParent vp = getView().getParent();
+ if ( vp instanceof CardView ) {
+ ((View)vp).setVisibility(View.INVISIBLE);
+ }
return null;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
if (data != null && data.moveToFirst()) {
+ ViewParent vp = getView().getParent();
+ if ( vp instanceof CardView ) {
+ ((View)vp).setVisibility(View.VISIBLE);
+ }
+
// Read weather condition ID from cursor
int weatherId = data.getInt(COL_WEATHER_CONDITION_ID);
- // Use weather art image
- mIconView.setImageResource(Utility.getArtResourceForWeatherCondition(weatherId));
+ if ( Utility.usingLocalGraphics(getActivity()) ) {
+ mIconView.setImageResource(Utility.getArtResourceForWeatherCondition(weatherId));
+ } else {
+ // Use weather art image
+ Glide.with(this)
+ .load(Utility.getArtUrlForWeatherCondition(getActivity(), weatherId))
+ .error(Utility.getArtResourceForWeatherCondition(weatherId))
+ .crossFade()
+ .into(mIconView);
+ }
// 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);
+ String dateText = Utility.getFullFriendlyDayString(getActivity(),date);
mDateView.setText(dateText);
- // Read description from cursor and update view
- String description = data.getString(COL_WEATHER_DESC);
+ // Get description from weather condition ID
+ String description = Utility.getStringForWeatherCondition(getActivity(), weatherId);
mDescriptionView.setText(description);
+ mDescriptionView.setContentDescription(getString(R.string.a11y_forecast, description));
- // For accessibility, add a content description to the icon field
- mIconView.setContentDescription(description);
+ // For accessibility, add a content description to the icon field. Because the ImageView
+ // is independently focusable, it's better to have a description of the image. Using
+ // null is appropriate when the image is purely decorative or when the image already
+ // has text describing it in the same UI component.
+ mIconView.setContentDescription(getString(R.string.a11y_forecast_icon, description));
// Read high temperature from cursor and update view
boolean isMetric = Utility.isMetric(getActivity());
@@ -206,31 +233,56 @@ public void onLoadFinished(Loader loader, Cursor data) {
double high = data.getDouble(COL_WEATHER_MAX_TEMP);
String highString = Utility.formatTemperature(getActivity(), high);
mHighTempView.setText(highString);
+ mHighTempView.setContentDescription(getString(R.string.a11y_high_temp, 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);
+ mLowTempView.setContentDescription(getString(R.string.a11y_low_temp, lowString));
// Read humidity from cursor and update view
float humidity = data.getFloat(COL_WEATHER_HUMIDITY);
mHumidityView.setText(getActivity().getString(R.string.format_humidity, humidity));
+ mHumidityView.setContentDescription(getString(R.string.a11y_humidity, mHumidityView.getText()));
+ mHumidityLabelView.setContentDescription(mHumidityView.getContentDescription());
// 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));
+ mWindView.setContentDescription(getString(R.string.a11y_wind, mWindView.getText()));
+ mWindLabelView.setContentDescription(mWindView.getContentDescription());
// Read pressure from cursor and update view
float pressure = data.getFloat(COL_WEATHER_PRESSURE);
- mPressureView.setText(getActivity().getString(R.string.format_pressure, pressure));
+ mPressureView.setText(getString(R.string.format_pressure, pressure));
+ mPressureView.setContentDescription(getString(R.string.a11y_pressure, mPressureView.getText()));
+ mPressureLabelView.setContentDescription(mPressureView.getContentDescription());
// 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());
+ }
+ AppCompatActivity activity = (AppCompatActivity)getActivity();
+ Toolbar toolbarView = (Toolbar) getView().findViewById(R.id.toolbar);
+
+ // We need to start the enter transition after the data has loaded
+ if ( mTransitionAnimation ) {
+ activity.supportStartPostponedEnterTransition();
+
+ if ( null != toolbarView ) {
+ activity.setSupportActionBar(toolbarView);
+
+ activity.getSupportActionBar().setDisplayShowTitleEnabled(false);
+ activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ } else {
+ if ( null != toolbarView ) {
+ Menu menu = toolbarView.getMenu();
+ if ( null != menu ) menu.clear();
+ toolbarView.inflateMenu(R.menu.detailfragment);
+ finishCreatingMenu(toolbarView.getMenu());
}
}
}
diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
index ada11de9..8bcb4b93 100644
--- a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
+++ b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java
@@ -17,130 +17,215 @@
import android.content.Context;
import android.database.Cursor;
-import android.support.v4.widget.CursorAdapter;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Checkable;
import android.widget.ImageView;
import android.widget.TextView;
+import com.bumptech.glide.Glide;
+import com.example.android.sunshine.app.data.WeatherContract;
+
/**
* {@link ForecastAdapter} exposes a list of weather forecasts
- * from a {@link Cursor} to a {@link android.widget.ListView}.
+ * from a {@link android.database.Cursor} to a {@link android.support.v7.widget.RecyclerView}.
*/
-public class ForecastAdapter extends CursorAdapter {
+public class ForecastAdapter extends RecyclerView.Adapter {
- 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;
+ private Cursor mCursor;
+ final private Context mContext;
+ final private ForecastAdapterOnClickHandler mClickHandler;
+ final private View mEmptyView;
+ final private ItemChoiceManager mICM;
+
/**
* 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 class ForecastAdapterViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ public final ImageView mIconView;
+ public final TextView mDateView;
+ public final TextView mDescriptionView;
+ public final TextView mHighTempView;
+ public final TextView mLowTempView;
+
+ public ForecastAdapterViewHolder(View view) {
+ super(view);
+ mIconView = (ImageView) view.findViewById(R.id.list_item_icon);
+ mDateView = (TextView) view.findViewById(R.id.list_item_date_textview);
+ mDescriptionView = (TextView) view.findViewById(R.id.list_item_forecast_textview);
+ mHighTempView = (TextView) view.findViewById(R.id.list_item_high_textview);
+ mLowTempView = (TextView) view.findViewById(R.id.list_item_low_textview);
+ view.setOnClickListener(this);
}
+
+ @Override
+ public void onClick(View v) {
+ int adapterPosition = getAdapterPosition();
+ mCursor.moveToPosition(adapterPosition);
+ int dateColumnIndex = mCursor.getColumnIndex(WeatherContract.WeatherEntry.COLUMN_DATE);
+ mClickHandler.onClick(mCursor.getLong(dateColumnIndex), this);
+ mICM.onClick(this);
+ }
+ }
+
+ public static interface ForecastAdapterOnClickHandler {
+ void onClick(Long date, ForecastAdapterViewHolder vh);
}
- public ForecastAdapter(Context context, Cursor c, int flags) {
- super(context, c, flags);
+ public ForecastAdapter(Context context, ForecastAdapterOnClickHandler dh, View emptyView, int choiceMode) {
+ mContext = context;
+ mClickHandler = dh;
+ mEmptyView = emptyView;
+ mICM = new ItemChoiceManager(this);
+ mICM.setChoiceMode(choiceMode);
}
+ /*
+ This takes advantage of the fact that the viewGroup passed to onCreateViewHolder is the
+ RecyclerView that will be used to contain the view, so that it can get the current
+ ItemSelectionManager from the view.
+
+ One could implement this pattern without modifying RecyclerView by taking advantage
+ of the view tag to store the ItemChoiceManager.
+ */
@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;
+ public ForecastAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ if ( viewGroup instanceof RecyclerView ) {
+ 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(viewGroup.getContext()).inflate(layoutId, viewGroup, false);
+ view.setFocusable(true);
+ return new ForecastAdapterViewHolder(view);
+ } else {
+ throw new RuntimeException("Not bound to RecyclerView");
}
-
- 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)));
+ public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder, int position) {
+ mCursor.moveToPosition(position);
+ int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
+ int defaultImage;
+ boolean useLongToday;
+
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_TODAY:
+ defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
+ useLongToday = true;
break;
- }
+ default:
+ defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
+ useLongToday = false;
}
+ if ( Utility.usingLocalGraphics(mContext) ) {
+ forecastAdapterViewHolder.mIconView.setImageResource(defaultImage);
+ } else {
+ Glide.with(mContext)
+ .load(Utility.getArtUrlForWeatherCondition(mContext, weatherId))
+ .error(defaultImage)
+ .crossFade()
+ .into(forecastAdapterViewHolder.mIconView);
+ }
+
+ // this enables better animations. even if we lose state due to a device rotation,
+ // the animator can use this to re-find the original view
+ ViewCompat.setTransitionName(forecastAdapterViewHolder.mIconView, "iconView" + position);
+
// Read date from cursor
- long dateInMillis = cursor.getLong(ForecastFragment.COL_WEATHER_DATE);
+ long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
+
// Find TextView and set formatted date on it
- viewHolder.dateView.setText(Utility.getFriendlyDayString(context, dateInMillis));
+ forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis, useLongToday));
// 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);
+ String description = Utility.getStringForWeatherCondition(mContext, weatherId);
- // For accessibility, add a content description to the icon field
- viewHolder.iconView.setContentDescription(description);
+ // Find TextView and set weather forecast on it
+ forecastAdapterViewHolder.mDescriptionView.setText(description);
+ forecastAdapterViewHolder.mDescriptionView.setContentDescription(mContext.getString(R.string.a11y_forecast, description));
- // Read user preference for metric or imperial temperature units
- boolean isMetric = Utility.isMetric(context);
+ // For accessibility, we don't want a content description for the icon field
+ // because the information is repeated in the description view and the icon
+ // is not individually selectable
// Read high temperature from cursor
- double high = cursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP);
- viewHolder.highTempView.setText(Utility.formatTemperature(context, high));
+ double high = mCursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP);
+ String highString = Utility.formatTemperature(mContext, high);
+ forecastAdapterViewHolder.mHighTempView.setText(highString);
+ forecastAdapterViewHolder.mHighTempView.setContentDescription(mContext.getString(R.string.a11y_high_temp, highString));
// Read low temperature from cursor
- double low = cursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP);
- viewHolder.lowTempView.setText(Utility.formatTemperature(context, low));
+ double low = mCursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP);
+ String lowString = Utility.formatTemperature(mContext, low);
+ forecastAdapterViewHolder.mLowTempView.setText(lowString);
+ forecastAdapterViewHolder.mLowTempView.setContentDescription(mContext.getString(R.string.a11y_low_temp, lowString));
+
+ mICM.onBindViewHolder(forecastAdapterViewHolder, position);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ mICM.onRestoreInstanceState(savedInstanceState);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ mICM.onSaveInstanceState(outState);
}
public void setUseTodayLayout(boolean useTodayLayout) {
mUseTodayLayout = useTodayLayout;
}
+ public int getSelectedItemPosition() {
+ return mICM.getSelectedItemPosition();
+ }
+
@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;
+ public int getItemCount() {
+ if ( null == mCursor ) return 0;
+ return mCursor.getCount();
+ }
+
+ public void swapCursor(Cursor newCursor) {
+ mCursor = newCursor;
+ notifyDataSetChanged();
+ mEmptyView.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ public void selectView(RecyclerView.ViewHolder viewHolder) {
+ if ( viewHolder instanceof ForecastAdapterViewHolder ) {
+ ForecastAdapterViewHolder vfh = (ForecastAdapterViewHolder)viewHolder;
+ vfh.onClick(vfh.itemView);
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
index 828db351..2c4537e3 100644
--- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
+++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java
@@ -15,14 +15,25 @@
*/
package com.example.android.sunshine.app;
+import android.annotation.TargetApi;
+import android.app.Activity;
import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.design.widget.AppBarLayout;
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.ViewCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -30,22 +41,24 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ListView;
+import android.view.ViewTreeObserver;
+import android.widget.AbsListView;
+import android.widget.TextView;
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.
+ * Encapsulates fetching the forecast and displaying it as a {@link android.support.v7.widget.RecyclerView} layout.
*/
-public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks {
+public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks, SharedPreferences.OnSharedPreferenceChangeListener {
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 RecyclerView mRecyclerView;
+ private boolean mUseTodayLayout, mAutoSelectView;
+ private int mChoiceMode;
+ private boolean mHoldForTransition;
+ private long mInitialSelectedDate = -1;
private static final String SELECTED_KEY = "selected_position";
@@ -91,7 +104,7 @@ public interface Callback {
/**
* DetailFragmentCallback for when an item has been selected.
*/
- public void onItemSelected(Uri dateUri);
+ public void onItemSelected(Uri dateUri, ForecastAdapter.ForecastAdapterViewHolder vh);
}
public ForecastFragment() {
@@ -104,6 +117,20 @@ public void onCreate(Bundle savedInstanceState) {
setHasOptionsMenu(true);
}
+ @Override
+ public void onResume() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ sp.registerOnSharedPreferenceChangeListener(this);
+ super.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ sp.unregisterOnSharedPreferenceChangeListener(this);
+ super.onPause();
+ }
+
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.forecastfragment, menu);
@@ -127,47 +154,96 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
+ @Override
+ public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
+ super.onInflate(activity, attrs, savedInstanceState);
+ TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.ForecastFragment,
+ 0, 0);
+ mChoiceMode = a.getInt(R.styleable.ForecastFragment_android_choiceMode, AbsListView.CHOICE_MODE_NONE);
+ mAutoSelectView = a.getBoolean(R.styleable.ForecastFragment_autoSelectView, false);
+ mHoldForTransition = a.getBoolean(R.styleable.ForecastFragment_sharedElementTransitions, false);
+ a.recycle();
+ }
+
@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() {
+ // Get a reference to the RecyclerView, and attach this adapter to it.
+ mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_forecast);
+ // Set the layout manager
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ View emptyView = rootView.findViewById(R.id.recyclerview_forecast_empty);
+
+ // use this setting to improve performance if you know that changes
+ // in content do not change the layout size of the RecyclerView
+ mRecyclerView.setHasFixedSize(true);
+
+ // The ForecastAdapter will take data from a source and
+ // use it to populate the RecyclerView it's attached to.
+ mForecastAdapter = new ForecastAdapter(getActivity(), new ForecastAdapter.ForecastAdapterOnClickHandler() {
@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;
+ public void onClick(Long date, ForecastAdapter.ForecastAdapterViewHolder vh) {
+ String locationSetting = Utility.getPreferredLocation(getActivity());
+ ((Callback) getActivity())
+ .onItemSelected(WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
+ locationSetting, date),
+ vh
+ );
+ }
+ }, emptyView, mChoiceMode);
+
+ // specify an adapter (see also next example)
+ mRecyclerView.setAdapter(mForecastAdapter);
+
+ final View parallaxView = rootView.findViewById(R.id.parallax_bar);
+ if (null != parallaxView) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+ int max = parallaxView.getHeight();
+ if (dy > 0) {
+ parallaxView.setTranslationY(Math.max(-max, parallaxView.getTranslationY() - dy / 2));
+ } else {
+ parallaxView.setTranslationY(Math.min(0, parallaxView.getTranslationY() - dy / 2));
+ }
+ }
+ });
}
- });
+ }
+
+ final AppBarLayout appbarView = (AppBarLayout)rootView.findViewById(R.id.appbar);
+ if (null != appbarView) {
+ ViewCompat.setElevation(appbarView, 0);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (0 == mRecyclerView.computeVerticalScrollOffset()) {
+ appbarView.setElevation(0);
+ } else {
+ appbarView.setElevation(appbarView.getTargetElevation());
+ }
+ }
+ });
+ }
+ }
// 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);
+ if (savedInstanceState != null) {
+ mForecastAdapter.onRestoreInstanceState(savedInstanceState);
}
mForecastAdapter.setUseTodayLayout(mUseTodayLayout);
@@ -177,12 +253,18 @@ public void onItemClick(AdapterView> adapterView, View view, int position, lon
@Override
public void onActivityCreated(Bundle savedInstanceState) {
+ // We hold for transition here just in-case the activity
+ // needs to be re-created. In a standard return transition,
+ // this doesn't actually make a difference.
+ if ( mHoldForTransition ) {
+ getActivity().supportPostponeEnterTransition();
+ }
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( ) {
+ void onLocationChanged() {
getLoaderManager().restartLoader(FORECAST_LOADER, null, this);
}
@@ -190,9 +272,9 @@ 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 ) {
+ if (null != mForecastAdapter) {
Cursor c = mForecastAdapter.getCursor();
- if ( null != c ) {
+ if (null != c) {
c.moveToPosition(0);
String posLat = c.getString(COL_COORD_LAT);
String posLong = c.getString(COL_COORD_LONG);
@@ -214,14 +296,11 @@ private void openPreferredLocationInMap() {
@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);
- }
+ mForecastAdapter.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
+
@Override
public Loader onCreateLoader(int i, Bundle bundle) {
// This is called when a new Loader needs to be created. This
@@ -248,10 +327,58 @@ public Loader onCreateLoader(int i, Bundle bundle) {
@Override
public void onLoadFinished(Loader 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);
+ updateEmptyView();
+ if ( data.getCount() == 0 ) {
+ getActivity().supportStartPostponedEnterTransition();
+ } else {
+ mRecyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ // Since we know we're going to get items, we keep the listener around until
+ // we see Children.
+ if (mRecyclerView.getChildCount() > 0) {
+ mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
+ int position = mForecastAdapter.getSelectedItemPosition();
+ if (position == RecyclerView.NO_POSITION &&
+ -1 != mInitialSelectedDate) {
+ Cursor data = mForecastAdapter.getCursor();
+ int count = data.getCount();
+ int dateColumn = data.getColumnIndex(WeatherContract.WeatherEntry.COLUMN_DATE);
+ for ( int i = 0; i < count; i++ ) {
+ data.moveToPosition(i);
+ if ( data.getLong(dateColumn) == mInitialSelectedDate ) {
+ position = i;
+ break;
+ }
+ }
+ }
+ if (position == RecyclerView.NO_POSITION) position = 0;
+ // If we don't need to restart the loader, and there's a desired position to restore
+ // to, do so now.
+ mRecyclerView.smoothScrollToPosition(position);
+ RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(position);
+ if (null != vh && mAutoSelectView) {
+ mForecastAdapter.selectView(vh);
+ }
+ if ( mHoldForTransition ) {
+ getActivity().supportStartPostponedEnterTransition();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ }
+
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (null != mRecyclerView) {
+ mRecyclerView.clearOnScrollListeners();
}
}
@@ -266,4 +393,46 @@ public void setUseTodayLayout(boolean useTodayLayout) {
mForecastAdapter.setUseTodayLayout(mUseTodayLayout);
}
}
+
+ public void setInitialSelectedDate(long initialSelectedDate) {
+ mInitialSelectedDate = initialSelectedDate;
+ }
+
+ /*
+ Updates the empty list view with contextually relevant information that the user can
+ use to determine why they aren't seeing weather.
+ */
+ private void updateEmptyView() {
+ if ( mForecastAdapter.getItemCount() == 0 ) {
+ TextView tv = (TextView) getView().findViewById(R.id.recyclerview_forecast_empty);
+ if ( null != tv ) {
+ // if cursor is empty, why? do we have an invalid location
+ int message = R.string.empty_forecast_list;
+ @SunshineSyncAdapter.LocationStatus int location = Utility.getLocationStatus(getActivity());
+ switch (location) {
+ case SunshineSyncAdapter.LOCATION_STATUS_SERVER_DOWN:
+ message = R.string.empty_forecast_list_server_down;
+ break;
+ case SunshineSyncAdapter.LOCATION_STATUS_SERVER_INVALID:
+ message = R.string.empty_forecast_list_server_error;
+ break;
+ case SunshineSyncAdapter.LOCATION_STATUS_INVALID:
+ message = R.string.empty_forecast_list_invalid_location;
+ break;
+ default:
+ if (!Utility.isNetworkAvailable(getActivity())) {
+ message = R.string.empty_forecast_list_no_network;
+ }
+ }
+ tv.setText(message);
+ }
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(getString(R.string.pref_location_status_key))) {
+ updateEmptyView();
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java b/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java
new file mode 100644
index 00000000..f046fb0f
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java
@@ -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;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.util.LongSparseArray;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.widget.AbsListView;
+import android.widget.Checkable;
+
+/**
+ * The ItemChoiceManager class keeps track of which positions have been selected. Note that it
+ * doesn't take advantage of new adapter features to track changes in the underlying data.
+ */
+public class ItemChoiceManager {
+ private final String LOG_TAG = MainActivity.class.getSimpleName();
+ private final String SELECTED_ITEMS_KEY = "SIK";
+ private int mChoiceMode;
+
+ private RecyclerView.Adapter mAdapter;
+ private RecyclerView.AdapterDataObserver mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ if (mAdapter != null && mAdapter.hasStableIds())
+ confirmCheckedPositionsById(mAdapter.getItemCount());
+ }
+ };
+
+ private ItemChoiceManager() {
+ }
+
+ ;
+
+ public ItemChoiceManager(RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * How many positions in either direction we will search to try to
+ * find a checked item with a stable ID that moved position across
+ * a data set change. If the item isn't found it will be unselected.
+ */
+ private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
+
+ /**
+ * Running state of which positions are currently checked
+ */
+ SparseBooleanArray mCheckStates = new SparseBooleanArray();
+
+ /**
+ * Running state of which IDs are currently checked.
+ * If there is a value for a given key, the checked state for that ID is true
+ * and the value holds the last known position in the adapter for that id.
+ */
+ LongSparseArray mCheckedIdStates = new LongSparseArray();
+
+ public void onClick(RecyclerView.ViewHolder vh) {
+ if (mChoiceMode == AbsListView.CHOICE_MODE_NONE)
+ return;
+
+ int checkedItemCount = mCheckStates.size();
+ int position = vh.getAdapterPosition();
+
+ if (position == RecyclerView.NO_POSITION) {
+ Log.d(LOG_TAG, "Unable to Set Item State");
+ return;
+ }
+
+ switch (mChoiceMode) {
+ case AbsListView.CHOICE_MODE_NONE:
+ break;
+ case AbsListView.CHOICE_MODE_SINGLE: {
+ boolean checked = mCheckStates.get(position, false);
+ if (!checked) {
+ for (int i = 0; i < checkedItemCount; i++) {
+ mAdapter.notifyItemChanged(mCheckStates.keyAt(i));
+ }
+ mCheckStates.clear();
+ mCheckStates.put(position, true);
+ mCheckedIdStates.clear();
+ mCheckedIdStates.put(mAdapter.getItemId(position), position);
+ }
+ // We directly call onBindViewHolder here because notifying that an item has
+ // changed on an item that has the focus causes it to lose focus, which makes
+ // keyboard navigation a bit annoying
+ mAdapter.onBindViewHolder(vh, position);
+ break;
+ }
+ case AbsListView.CHOICE_MODE_MULTIPLE: {
+ boolean checked = mCheckStates.get(position, false);
+ mCheckStates.put(position, !checked);
+ // We directly call onBindViewHolder here because notifying that an item has
+ // changed on an item that has the focus causes it to lose focus, which makes
+ // keyboard navigation a bit annoying
+ mAdapter.onBindViewHolder(vh, position);
+ break;
+ }
+ case AbsListView.CHOICE_MODE_MULTIPLE_MODAL: {
+ throw new RuntimeException("Multiple Modal not implemented in ItemChoiceManager.");
+ }
+ }
+ }
+
+ /**
+ * Defines the choice behavior for the RecyclerView. By default, RecyclerViewChoiceMode does
+ * not have any choice behavior (AbsListView.CHOICE_MODE_NONE). By setting the choiceMode to
+ * AbsListView.CHOICE_MODE_SINGLE, the RecyclerView allows up to one item to be in a
+ * chosen state.
+ *
+ * @param choiceMode One of AbsListView.CHOICE_MODE_NONE, AbsListView.CHOICE_MODE_SINGLE
+ */
+ public void setChoiceMode(int choiceMode) {
+ if (mChoiceMode != choiceMode) {
+ mChoiceMode = choiceMode;
+ clearSelections();
+ }
+ }
+
+ /**
+ * Returns the checked state of the specified position. The result is only
+ * valid if the choice mode has been set to AbsListView.CHOICE_MODE_SINGLE,
+ * but the code does not check this.
+ *
+ * @param position The item whose checked state to return
+ * @return The item's checked state
+ * @see #setChoiceMode(int)
+ */
+ public boolean isItemChecked(int position) {
+ return mCheckStates.get(position);
+ }
+
+ void clearSelections() {
+ mCheckStates.clear();
+ mCheckedIdStates.clear();
+ }
+
+ void confirmCheckedPositionsById(int oldItemCount) {
+ // Clear out the positional check states, we'll rebuild it below from IDs.
+ mCheckStates.clear();
+
+ for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
+ final long id = mCheckedIdStates.keyAt(checkedIndex);
+ final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
+
+ final long lastPosId = mAdapter.getItemId(lastPos);
+ if (id != lastPosId) {
+ // Look around to see if the ID is nearby. If not, uncheck it.
+ final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
+ final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, oldItemCount);
+ boolean found = false;
+ for (int searchPos = start; searchPos < end; searchPos++) {
+ final long searchId = mAdapter.getItemId(searchPos);
+ if (id == searchId) {
+ found = true;
+ mCheckStates.put(searchPos, true);
+ mCheckedIdStates.setValueAt(checkedIndex, searchPos);
+ break;
+ }
+ }
+
+ if (!found) {
+ mCheckedIdStates.delete(id);
+ checkedIndex--;
+ }
+ } else {
+ mCheckStates.put(lastPos, true);
+ }
+ }
+ }
+
+ public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) {
+ boolean checked = isItemChecked(position);
+ if (vh.itemView instanceof Checkable) {
+ ((Checkable) vh.itemView).setChecked(checked);
+ }
+ ViewCompat.setActivated(vh.itemView, checked);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ byte[] states = savedInstanceState.getByteArray(SELECTED_ITEMS_KEY);
+ if ( null != states ) {
+ Parcel inParcel = Parcel.obtain();
+ inParcel.unmarshall(states, 0, states.length);
+ inParcel.setDataPosition(0);
+ mCheckStates = inParcel.readSparseBooleanArray();
+ final int numStates = inParcel.readInt();
+ mCheckedIdStates.clear();
+ for (int i=0; i(vh.mIconView, getString(R.string.detail_icon_transition_name)));
+ ActivityCompat.startActivity(this, intent, activityOptions.toBundle());
}
}
+
+ /**
+ * Check the device to make sure it has the Google Play Services APK. If
+ * it doesn't, display a dialog that allows users to download the APK from
+ * the Google Play Store or enable it in the device's system settings.
+ */
+ private boolean checkPlayServices() {
+ GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+ int resultCode = apiAvailability.isGooglePlayServicesAvailable(this);
+ if (resultCode != ConnectionResult.SUCCESS) {
+ if (apiAvailability.isUserResolvableError(resultCode)) {
+ apiAvailability.getErrorDialog(this, resultCode,
+ PLAY_SERVICES_RESOLUTION_REQUEST).show();
+ } else {
+ Log.i(LOG_TAG, "This device is not supported.");
+ finish();
+ }
+ return false;
+ }
+ return true;
+ }
}
diff --git a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
index 1869c5ca..ae20b520 100644
--- a/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
+++ b/app/src/main/java/com/example/android/sunshine/app/SettingsActivity.java
@@ -24,9 +24,16 @@
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
-
+import android.support.design.widget.Snackbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
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.
@@ -38,6 +45,8 @@
*/
public class SettingsActivity extends PreferenceActivity
implements Preference.OnPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
+ protected final static int PLACE_PICKER_REQUEST = 9090;
+ private ImageView mAttribution;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -49,6 +58,20 @@ public void onCreate(Bundle savedInstanceState) {
// updated when the preference changes.
bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key)));
bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key)));
+ bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_art_pack_key)));
+
+
+ // If we are using a PlacePicker location, we need to show attributions.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mAttribution = new ImageView(this);
+ mAttribution.setImageResource(R.drawable.powered_by_google_light);
+
+ if (!Utility.isLocationLatLonAvailable(this)) {
+ mAttribution.setVisibility(View.GONE);
+ }
+
+ setListFooter(mAttribution);
+ }
}
// Registers a shared preference change listener that gets notified when preferences change
@@ -95,10 +118,28 @@ private void setPreferenceSummary(Preference preference, Object value) {
if (prefIndex >= 0) {
preference.setSummary(listPreference.getEntries()[prefIndex]);
}
+ } else if (key.equals(getString(R.string.pref_location_key))) {
+ @SunshineSyncAdapter.LocationStatus int status = Utility.getLocationStatus(this);
+ switch (status) {
+ case SunshineSyncAdapter.LOCATION_STATUS_OK:
+ preference.setSummary(stringValue);
+ break;
+ case SunshineSyncAdapter.LOCATION_STATUS_UNKNOWN:
+ preference.setSummary(getString(R.string.pref_location_unknown_description, value.toString()));
+ break;
+ case SunshineSyncAdapter.LOCATION_STATUS_INVALID:
+ preference.setSummary(getString(R.string.pref_location_error_description, value.toString()));
+ break;
+ default:
+ // Note --- if the server is down we still assume the value
+ // is valid
+ preference.setSummary(stringValue);
+ }
} 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
@@ -113,10 +154,30 @@ public boolean onPreferenceChange(Preference preference, Object value) {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if ( key.equals(getString(R.string.pref_location_key)) ) {
+ // we've changed the location
+ // 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();
+
+ // Remove attributions for our any PlacePicker locations.
+ if (mAttribution != null) {
+ mAttribution.setVisibility(View.GONE);
+ }
+
+ Utility.resetLocationStatus(this);
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);
+ } else if ( key.equals(getString(R.string.pref_location_status_key)) ) {
+ // our location status has changed. Update the summary accordingly
+ Preference locationPreference = findPreference(getString(R.string.pref_location_key));
+ bindPreferenceSummaryToValue(locationPreference);
+ } else if ( key.equals(getString(R.string.pref_art_pack_key)) ) {
+ // art pack have changed. update lists of weather entries accordingly
+ getContentResolver().notifyChange(WeatherContract.WeatherEntry.CONTENT_URI, null);
}
}
@@ -125,4 +186,59 @@ 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);
+
+ // Add attributions for our new PlacePicker location.
+ if (mAttribution != null) {
+ mAttribution.setVisibility(View.VISIBLE);
+ } else {
+ // For pre-Honeycomb devices, we cannot add a footer, so we will use a snackbar
+ View rootView = findViewById(android.R.id.content);
+ Snackbar.make(rootView, getString(R.string.attribution_text),
+ Snackbar.LENGTH_LONG).show();
+ }
+
+ Utility.resetLocationStatus(this);
+ SunshineSyncAdapter.syncImmediately(this);
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
}
diff --git a/app/src/main/java/com/example/android/sunshine/app/Utility.java b/app/src/main/java/com/example/android/sunshine/app/Utility.java
index bb7047d0..5f559620 100644
--- a/app/src/main/java/com/example/android/sunshine/app/Utility.java
+++ b/app/src/main/java/com/example/android/sunshine/app/Utility.java
@@ -17,14 +17,43 @@
import android.content.Context;
import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.preference.PreferenceManager;
import android.text.format.Time;
+import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
+
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
+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),
@@ -67,7 +96,7 @@ static String formatDate(long dateInMilliseconds) {
* @param dateInMillis The date in milliseconds
* @return a user-friendly representation of the date.
*/
- public static String getFriendlyDayString(Context context, long dateInMillis) {
+ public static String getFriendlyDayString(Context context, long dateInMillis, boolean displayLongToday) {
// The day string for forecast uses the following logic:
// For today: "Today, June 8"
// For tomorrow: "Tomorrow"
@@ -82,7 +111,7 @@ public static String getFriendlyDayString(Context context, long dateInMillis) {
// If the date we're building the String for is today's date, the format
// is "Today, June 24"
- if (julianDay == currentJulianDay) {
+ if (displayLongToday && julianDay == currentJulianDay) {
String today = context.getString(R.string.today);
int formatId = R.string.format_full_friendly_date;
return String.format(context.getString(
@@ -99,6 +128,24 @@ public static String getFriendlyDayString(Context context, long dateInMillis) {
}
}
+ /**
+ * 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 getFullFriendlyDayString(Context context, long dateInMillis) {
+
+ String day = getDayName(context, dateInMillis);
+ int formatId = R.string.format_full_friendly_date;
+ return String.format(context.getString(
+ formatId,
+ day,
+ getFormattedMonthDay(context, dateInMillis)));
+ }
+
/**
* Given a day, returns just the name to use for that day.
* E.g "today", "tomorrow", "wednesday".
@@ -212,6 +259,60 @@ public static int getIconResourceForWeatherCondition(int weatherId) {
return -1;
}
+ /**
+ * Helper method to return whether or not Sunshine is using local graphics.
+ *
+ * @param context Context to use for retrieving the preference
+ * @return true if Sunshine is using local graphics, false otherwise.
+ */
+ public static boolean usingLocalGraphics(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String sunshineArtPack = context.getString(R.string.pref_art_pack_sunshine);
+ return prefs.getString(context.getString(R.string.pref_art_pack_key),
+ sunshineArtPack).equals(sunshineArtPack);
+ }
+
+ /**
+ * Helper method to provide the art urls according to the weather condition id returned
+ * by the OpenWeatherMap call.
+ *
+ * @param context Context to use for retrieving the URL format
+ * @param weatherId from OpenWeatherMap API response
+ * @return url for the corresponding weather artwork. null if no relation is found.
+ */
+ public static String getArtUrlForWeatherCondition(Context context, int weatherId) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String formatArtUrl = prefs.getString(context.getString(R.string.pref_art_pack_key),
+ context.getString(R.string.pref_art_pack_sunshine));
+
+ // Based on weather code data found at:
+ // http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
+ if (weatherId >= 200 && weatherId <= 232) {
+ return String.format(Locale.US, formatArtUrl, "storm");
+ } else if (weatherId >= 300 && weatherId <= 321) {
+ return String.format(Locale.US, formatArtUrl, "light_rain");
+ } else if (weatherId >= 500 && weatherId <= 504) {
+ return String.format(Locale.US, formatArtUrl, "rain");
+ } else if (weatherId == 511) {
+ return String.format(Locale.US, formatArtUrl, "snow");
+ } else if (weatherId >= 520 && weatherId <= 531) {
+ return String.format(Locale.US, formatArtUrl, "rain");
+ } else if (weatherId >= 600 && weatherId <= 622) {
+ return String.format(Locale.US, formatArtUrl, "snow");
+ } else if (weatherId >= 701 && weatherId <= 761) {
+ return String.format(Locale.US, formatArtUrl, "fog");
+ } else if (weatherId == 761 || weatherId == 781) {
+ return String.format(Locale.US, formatArtUrl, "storm");
+ } else if (weatherId == 800) {
+ return String.format(Locale.US, formatArtUrl, "clear");
+ } else if (weatherId == 801) {
+ return String.format(Locale.US, formatArtUrl, "light_clouds");
+ } else if (weatherId >= 802 && weatherId <= 804) {
+ return String.format(Locale.US, formatArtUrl, "clouds");
+ }
+ return null;
+ }
+
/**
* Helper method to provide the art resource id according to the weather condition id returned
* by the OpenWeatherMap call.
@@ -246,4 +347,256 @@ public static int getArtResourceForWeatherCondition(int weatherId) {
}
return -1;
}
+
+ /**
+ * Helper method to provide the string according to the weather
+ * condition id returned by the OpenWeatherMap call.
+ * @param context Android context
+ * @param weatherId from OpenWeatherMap API response
+ * @return string for the weather condition. null if no relation is found.
+ */
+ public static String getStringForWeatherCondition(Context context, int weatherId) {
+ // Based on weather code data found at:
+ // http://bugs.openweathermap.org/projects/api/wiki/Weather_Condition_Codes
+ int stringId;
+ if (weatherId >= 200 && weatherId <= 232) {
+ stringId = R.string.condition_2xx;
+ } else if (weatherId >= 300 && weatherId <= 321) {
+ stringId = R.string.condition_3xx;
+ } else switch (weatherId) {
+ case 500:
+ stringId = R.string.condition_500;
+ break;
+ case 501:
+ stringId = R.string.condition_501;
+ break;
+ case 502:
+ stringId = R.string.condition_502;
+ break;
+ case 503:
+ stringId = R.string.condition_503;
+ break;
+ case 504:
+ stringId = R.string.condition_504;
+ break;
+ case 511:
+ stringId = R.string.condition_511;
+ break;
+ case 520:
+ stringId = R.string.condition_520;
+ break;
+ case 531:
+ stringId = R.string.condition_531;
+ break;
+ case 600:
+ stringId = R.string.condition_600;
+ break;
+ case 601:
+ stringId = R.string.condition_601;
+ break;
+ case 602:
+ stringId = R.string.condition_602;
+ break;
+ case 611:
+ stringId = R.string.condition_611;
+ break;
+ case 612:
+ stringId = R.string.condition_612;
+ break;
+ case 615:
+ stringId = R.string.condition_615;
+ break;
+ case 616:
+ stringId = R.string.condition_616;
+ break;
+ case 620:
+ stringId = R.string.condition_620;
+ break;
+ case 621:
+ stringId = R.string.condition_621;
+ break;
+ case 622:
+ stringId = R.string.condition_622;
+ break;
+ case 701:
+ stringId = R.string.condition_701;
+ break;
+ case 711:
+ stringId = R.string.condition_711;
+ break;
+ case 721:
+ stringId = R.string.condition_721;
+ break;
+ case 731:
+ stringId = R.string.condition_731;
+ break;
+ case 741:
+ stringId = R.string.condition_741;
+ break;
+ case 751:
+ stringId = R.string.condition_751;
+ break;
+ case 761:
+ stringId = R.string.condition_761;
+ break;
+ case 762:
+ stringId = R.string.condition_762;
+ break;
+ case 771:
+ stringId = R.string.condition_771;
+ break;
+ case 781:
+ stringId = R.string.condition_781;
+ break;
+ case 800:
+ stringId = R.string.condition_800;
+ break;
+ case 801:
+ stringId = R.string.condition_801;
+ break;
+ case 802:
+ stringId = R.string.condition_802;
+ break;
+ case 803:
+ stringId = R.string.condition_803;
+ break;
+ case 804:
+ stringId = R.string.condition_804;
+ break;
+ case 900:
+ stringId = R.string.condition_900;
+ break;
+ case 901:
+ stringId = R.string.condition_901;
+ break;
+ case 902:
+ stringId = R.string.condition_902;
+ break;
+ case 903:
+ stringId = R.string.condition_903;
+ break;
+ case 904:
+ stringId = R.string.condition_904;
+ break;
+ case 905:
+ stringId = R.string.condition_905;
+ break;
+ case 906:
+ stringId = R.string.condition_906;
+ break;
+ case 951:
+ stringId = R.string.condition_951;
+ break;
+ case 952:
+ stringId = R.string.condition_952;
+ break;
+ case 953:
+ stringId = R.string.condition_953;
+ break;
+ case 954:
+ stringId = R.string.condition_954;
+ break;
+ case 955:
+ stringId = R.string.condition_955;
+ break;
+ case 956:
+ stringId = R.string.condition_956;
+ break;
+ case 957:
+ stringId = R.string.condition_957;
+ break;
+ case 958:
+ stringId = R.string.condition_958;
+ break;
+ case 959:
+ stringId = R.string.condition_959;
+ break;
+ case 960:
+ stringId = R.string.condition_960;
+ break;
+ case 961:
+ stringId = R.string.condition_961;
+ break;
+ case 962:
+ stringId = R.string.condition_962;
+ break;
+ default:
+ return context.getString(R.string.condition_unknown, 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.
+ *
+ * @param c Context used to get the ConnectivityManager
+ * @return true if the network is available
+ */
+ static public boolean isNetworkAvailable(Context c) {
+ ConnectivityManager cm =
+ (ConnectivityManager)c.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null &&
+ activeNetwork.isConnectedOrConnecting();
+ }
+
+ /**
+ *
+ * @param c Context used to get the SharedPreferences
+ * @return the location status integer type
+ */
+ @SuppressWarnings("ResourceType")
+ static public @SunshineSyncAdapter.LocationStatus
+ int getLocationStatus(Context c){
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(c);
+ return sp.getInt(c.getString(R.string.pref_location_status_key), SunshineSyncAdapter.LOCATION_STATUS_UNKNOWN);
+ }
+
+ /**
+ * Resets the location status. (Sets it to SunshineSyncAdapter.LOCATION_STATUS_UNKNOWN)
+ * @param c Context used to get the SharedPreferences
+ */
+ static public void resetLocationStatus(Context c){
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(c);
+ SharedPreferences.Editor spe = sp.edit();
+ spe.putInt(c.getString(R.string.pref_location_status_key), SunshineSyncAdapter.LOCATION_STATUS_UNKNOWN);
+ spe.apply();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java
new file mode 100644
index 00000000..14627478
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java
@@ -0,0 +1,110 @@
+/*
+ * 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.gcm;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.example.android.sunshine.app.MainActivity;
+import com.example.android.sunshine.app.R;
+import com.google.android.gms.gcm.GcmListenerService;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class MyGcmListenerService extends GcmListenerService {
+
+ private static final String TAG = "MyGcmListenerService";
+
+ private static final String EXTRA_DATA = "data";
+ private static final String EXTRA_WEATHER = "weather";
+ private static final String EXTRA_LOCATION = "location";
+
+ public static final int NOTIFICATION_ID = 1;
+
+ /**
+ * Called when message is received.
+ *
+ * @param from SenderID of the sender.
+ * @param data Data bundle containing message data as key/value pairs.
+ * For Set of keys use data.keySet().
+ */
+ @Override
+ public void onMessageReceived(String from, Bundle data) {
+ // Time to unparcel the bundle!
+ if (!data.isEmpty()) {
+ // TODO: gcm_default sender ID comes from the API console
+ String senderId = getString(R.string.gcm_defaultSenderId);
+ if (senderId.length() == 0) {
+ Toast.makeText(this, "SenderID string needs to be set", Toast.LENGTH_LONG).show();
+ }
+ // Not a bad idea to check that the message is coming from your server.
+ if ((senderId).equals(from)) {
+ // Process message and then post a notification of the received message.
+ try {
+ JSONObject jsonObject = new JSONObject(data.getString(EXTRA_DATA));
+ String weather = jsonObject.getString(EXTRA_WEATHER);
+ String location = jsonObject.getString(EXTRA_LOCATION);
+ String alert =
+ String.format(getString(R.string.gcm_weather_alert), weather, location);
+ sendNotification(alert);
+ } catch (JSONException e) {
+ // JSON parsing failed, so we just let this message go, since GCM is not one
+ // of our critical features.
+ }
+ }
+ Log.i(TAG, "Received: " + data.toString());
+ }
+ }
+
+ /**
+ * Put the message into a notification and post it.
+ * This is just one simple example of what you might choose to do with a GCM message.
+ *
+ * @param message The alert message to be posted.
+ */
+ private void sendNotification(String message) {
+ NotificationManager mNotificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ PendingIntent contentIntent =
+ PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);
+
+ // Notifications using both a large and a small icon (which yours should!) need the large
+ // icon as a bitmap. So we need to create that here from the resource ID, and pass the
+ // object along in our notification builder. Generally, you want to use the app icon as the
+ // small icon, so that users understand what app is triggering this notification.
+ Bitmap largeIcon = BitmapFactory.decodeResource(this.getResources(), R.drawable.art_storm);
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.art_clear)
+ .setLargeIcon(largeIcon)
+ .setContentTitle("Weather Alert!")
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_HIGH);
+ mBuilder.setContentIntent(contentIntent);
+ mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java
new file mode 100644
index 00000000..862bc476
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java
@@ -0,0 +1,36 @@
+/*
+ * 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.gcm;
+
+import android.content.Intent;
+import com.google.android.gms.iid.InstanceIDListenerService;
+
+public class MyInstanceIDListenerService extends InstanceIDListenerService {
+ private static final String TAG = "MyInstanceIDLS";
+
+ /**
+ * Called if InstanceID token is updated. This may occur if the security of
+ * the previous token had been compromised. This call is initiated by the
+ * InstanceID provider.
+ */
+ @Override
+ public void onTokenRefresh() {
+ // Fetch updated Instance ID token.
+ Intent intent = new Intent(this, RegistrationIntentService.class);
+ startService(intent);
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java
new file mode 100644
index 00000000..fae7157f
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java
@@ -0,0 +1,82 @@
+/*
+ * 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.gcm;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.example.android.sunshine.app.MainActivity;
+import com.example.android.sunshine.app.R;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+
+
+public class RegistrationIntentService extends IntentService {
+ private static final String TAG = "RegIntentService";
+
+ public RegistrationIntentService() {
+ super(TAG);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ try {
+ // In the (unlikely) event that multiple refresh operations occur simultaneously,
+ // ensure that they are processed sequentially.
+ synchronized (TAG) {
+ // Initially this call goes out to the network to retrieve the token, subsequent calls
+ // are local.
+ InstanceID instanceID = InstanceID.getInstance(this);
+
+ // TODO: gcm_default sender ID comes from the API console
+ String senderId = getString(R.string.gcm_defaultSenderId);
+ if ( senderId.length() != 0 ) {
+ String token = instanceID.getToken(senderId,
+ GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
+ sendRegistrationToServer(token);
+ }
+
+ // You should store a boolean that indicates whether the generated token has been
+ // sent to your server. If the boolean is false, send the token to your server,
+ // otherwise your server should have already received the token.
+ sharedPreferences.edit().putBoolean(MainActivity.SENT_TOKEN_TO_SERVER, true).apply();
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to complete token refresh", e);
+
+ // If an exception happens while fetching the new token or updating our registration data
+ // on a third-party server, this ensures that we'll attempt the update at a later time.
+ sharedPreferences.edit().putBoolean(MainActivity.SENT_TOKEN_TO_SERVER, false).apply();
+ }
+ }
+
+ /**
+ * Normally, you would want to persist the registration to third-party servers. Because we do
+ * not have a server, and are faking it with a website, you'll want to log the token instead.
+ * That way you can see the value in logcat, and note it for future use in the website.
+ *
+ * @param token The new token.
+ */
+ private void sendRegistrationToServer(String token) {
+ Log.i(TAG, "GCM Registration Token: " + token);
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java b/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java
new file mode 100644
index 00000000..dcfc24cb
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/muzei/WeatherMuzeiSource.java
@@ -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();
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java b/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java
index 22663933..9aa52c96 100644
--- a/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java
+++ b/app/src/main/java/com/example/android/sunshine/app/sync/SunshineSyncAdapter.java
@@ -2,6 +2,7 @@
import android.accounts.Account;
import android.accounts.AccountManager;
+import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
@@ -22,15 +23,19 @@
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.support.annotation.IntDef;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.format.Time;
import android.util.Log;
+import com.bumptech.glide.Glide;
+import com.example.android.sunshine.app.BuildConfig;
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 com.example.android.sunshine.app.muzei.WeatherMuzeiSource;
import org.json.JSONArray;
import org.json.JSONException;
@@ -40,12 +45,17 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Vector;
+import java.util.concurrent.ExecutionException;
public class SunshineSyncAdapter extends AbstractThreadedSyncAdapter {
public final String LOG_TAG = SunshineSyncAdapter.class.getSimpleName();
+ public static final String ACTION_DATA_UPDATED =
+ "com.example.android.sunshine.app.ACTION_DATA_UPDATED";
// 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;
@@ -67,6 +77,16 @@ public class SunshineSyncAdapter extends AbstractThreadedSyncAdapter {
private static final int INDEX_MIN_TEMP = 2;
private static final int INDEX_SHORT_DESC = 3;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({LOCATION_STATUS_OK, LOCATION_STATUS_SERVER_DOWN, LOCATION_STATUS_SERVER_INVALID, LOCATION_STATUS_UNKNOWN, LOCATION_STATUS_INVALID})
+ public @interface LocationStatus {}
+
+ public static final int LOCATION_STATUS_OK = 0;
+ public static final int LOCATION_STATUS_SERVER_DOWN = 1;
+ public static final int LOCATION_STATUS_SERVER_INVALID = 2;
+ public static final int LOCATION_STATUS_UNKNOWN = 3;
+ public static final int LOCATION_STATUS_INVALID = 4;
+
public SunshineSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@@ -74,7 +94,13 @@ public SunshineSyncAdapter(Context context, boolean 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());
+
+ // We no longer need just the location String, but also potentially the latitude and
+ // longitude, in case we are syncing based on a new Place Picker API result.
+ Context context = getContext();
+ String locationQuery = Utility.getPreferredLocation(context);
+ String locationLatitude = String.valueOf(Utility.getLocationLatitude(context));
+ String locationLongitude = String.valueOf(Utility.getLocationLongitude(context));
// These two need to be declared outside the try/catch
// so that they can be closed in the finally block.
@@ -95,15 +121,32 @@ public void onPerformSync(Account account, Bundle extras, String authority, Cont
final String FORECAST_BASE_URL =
"http://api.openweathermap.org/data/2.5/forecast/daily?";
final String QUERY_PARAM = "q";
+ final String LAT_PARAM = "lat";
+ final String LON_PARAM = "lon";
final String FORMAT_PARAM = "mode";
final String UNITS_PARAM = "units";
final String DAYS_PARAM = "cnt";
+ final String APPID_PARAM = "APPID";
+
+ Uri.Builder uriBuilder = Uri.parse(FORECAST_BASE_URL).buildUpon();
+
+ // Instead of always building the query based off of the location string, we want to
+ // potentially build a query using a lat/lon value. This will be the case when we are
+ // syncing based off of a new location from the Place Picker API. So we need to check
+ // if we have a lat/lon to work with, and use those when we do. Otherwise, the weather
+ // service may not understand the location address provided by the Place Picker API
+ // and the user could end up with no weather! The horror!
+ if (Utility.isLocationLatLonAvailable(context)) {
+ uriBuilder.appendQueryParameter(LAT_PARAM, locationLatitude)
+ .appendQueryParameter(LON_PARAM, locationLongitude);
+ } else {
+ uriBuilder.appendQueryParameter(QUERY_PARAM, locationQuery);
+ }
- Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon()
- .appendQueryParameter(QUERY_PARAM, locationQuery)
- .appendQueryParameter(FORMAT_PARAM, format)
+ Uri builtUri = uriBuilder.appendQueryParameter(FORMAT_PARAM, format)
.appendQueryParameter(UNITS_PARAM, units)
.appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
+ .appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY)
.build();
URL url = new URL(builtUri.toString());
@@ -132,6 +175,7 @@ public void onPerformSync(Account account, Bundle extras, String authority, Cont
if (buffer.length() == 0) {
// Stream was empty. No point in parsing.
+ setLocationStatus(getContext(), LOCATION_STATUS_SERVER_DOWN);
return;
}
forecastJsonStr = buffer.toString();
@@ -140,9 +184,11 @@ public void onPerformSync(Account account, Bundle extras, String authority, Cont
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.
+ setLocationStatus(getContext(), LOCATION_STATUS_SERVER_DOWN);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
e.printStackTrace();
+ setLocationStatus(getContext(), LOCATION_STATUS_SERVER_INVALID);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
@@ -201,8 +247,28 @@ private void getWeatherDataFromJson(String forecastJsonStr,
final String OWM_DESCRIPTION = "main";
final String OWM_WEATHER_ID = "id";
+ final String OWM_MESSAGE_CODE = "cod";
+
try {
JSONObject forecastJson = new JSONObject(forecastJsonStr);
+ Context context = getContext();
+
+ // do we have an error?
+ if ( forecastJson.has(OWM_MESSAGE_CODE) ) {
+ int errorCode = forecastJson.getInt(OWM_MESSAGE_CODE);
+
+ switch (errorCode) {
+ case HttpURLConnection.HTTP_OK:
+ break;
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ setLocationStatus(getContext(), LOCATION_STATUS_INVALID);
+ return;
+ default:
+ setLocationStatus(getContext(), LOCATION_STATUS_SERVER_DOWN);
+ return;
+ }
+ }
+
JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST);
JSONObject cityJson = forecastJson.getJSONObject(OWM_CITY);
@@ -300,14 +366,35 @@ private void getWeatherDataFromJson(String forecastJsonStr,
WeatherContract.WeatherEntry.COLUMN_DATE + " <= ?",
new String[] {Long.toString(dayTime.setJulianDay(julianStartDay-1))});
+ updateWidgets();
+ updateMuzei();
notifyWeather();
}
-
Log.d(LOG_TAG, "Sync Complete. " + cVVector.size() + " Inserted");
+ setLocationStatus(getContext(), LOCATION_STATUS_OK);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
e.printStackTrace();
+ setLocationStatus(getContext(), LOCATION_STATUS_SERVER_INVALID);
+ }
+ }
+
+ private void updateWidgets() {
+ Context context = getContext();
+ // Setting the package ensures that only components in our app will receive the broadcast
+ Intent dataUpdatedIntent = new Intent(ACTION_DATA_UPDATED)
+ .setPackage(context.getPackageName());
+ context.sendBroadcast(dataUpdatedIntent);
+ }
+
+ private void updateMuzei() {
+ // Muzei is only compatible with Jelly Bean MR1+ devices, so there's no need to update the
+ // Muzei background on lower API level devices
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ Context context = getContext();
+ context.startService(new Intent(ACTION_DATA_UPDATED)
+ .setClass(context, WeatherMuzeiSource.class));
}
}
@@ -341,8 +428,33 @@ private void notifyWeather() {
int iconId = Utility.getIconResourceForWeatherCondition(weatherId);
Resources resources = context.getResources();
- Bitmap largeIcon = BitmapFactory.decodeResource(resources,
- Utility.getArtResourceForWeatherCondition(weatherId));
+ int artResourceId = Utility.getArtResourceForWeatherCondition(weatherId);
+ String artUrl = Utility.getArtUrlForWeatherCondition(context, weatherId);
+
+ // On Honeycomb and higher devices, we can retrieve the size of the large icon
+ // Prior to that, we use a fixed size
+ @SuppressLint("InlinedApi")
+ int largeIconWidth = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+ ? resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width)
+ : resources.getDimensionPixelSize(R.dimen.notification_large_icon_default);
+ @SuppressLint("InlinedApi")
+ int largeIconHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+ ? resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height)
+ : resources.getDimensionPixelSize(R.dimen.notification_large_icon_default);
+
+ // Retrieve the large icon
+ Bitmap largeIcon;
+ try {
+ largeIcon = Glide.with(context)
+ .load(artUrl)
+ .asBitmap()
+ .error(artResourceId)
+ .fitCenter()
+ .into(largeIconWidth, largeIconHeight).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Error retrieving large icon from " + artUrl, e);
+ largeIcon = BitmapFactory.decodeResource(resources, artResourceId);
+ }
String title = context.getString(R.string.app_name);
// Define the text of the forecast.
@@ -355,7 +467,7 @@ private void notifyWeather() {
// notifications. Just throw in some data.
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(getContext())
- .setColor(resources.getColor(R.color.sunshine_light_blue))
+ .setColor(resources.getColor(R.color.primary_light))
.setSmallIcon(iconId)
.setLargeIcon(largeIcon)
.setContentTitle(title)
@@ -533,4 +645,17 @@ private static void onAccountCreated(Account newAccount, Context context) {
public static void initializeSyncAdapter(Context context) {
getSyncAccount(context);
}
+
+ /**
+ * Sets the location status into shared preference. This function should not be called from
+ * the UI thread because it uses commit to write to the shared preferences.
+ * @param c Context to get the PreferenceManager from.
+ * @param locationStatus The IntDef value to set
+ */
+ static private void setLocationStatus(Context c, @LocationStatus int locationStatus){
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(c);
+ SharedPreferences.Editor spe = sp.edit();
+ spe.putInt(c.getString(R.string.pref_location_status_key), locationStatus);
+ spe.commit();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java
new file mode 100644
index 00000000..85896eba
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetProvider.java
@@ -0,0 +1,89 @@
+package com.example.android.sunshine.app.widget;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.TaskStackBuilder;
+import android.widget.RemoteViews;
+
+import com.example.android.sunshine.app.DetailActivity;
+import com.example.android.sunshine.app.MainActivity;
+import com.example.android.sunshine.app.R;
+import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
+
+/**
+ * Provider for a scrollable weather detail widget
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class DetailWidgetProvider extends AppWidgetProvider {
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ // Perform this loop procedure for each App Widget that belongs to this provider
+ for (int appWidgetId : appWidgetIds) {
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_detail);
+
+ // Create an Intent to launch MainActivity
+ Intent intent = new Intent(context, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.widget, pendingIntent);
+
+ // Set up the collection
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ setRemoteAdapter(context, views);
+ } else {
+ setRemoteAdapterV11(context, views);
+ }
+ boolean useDetailActivity = context.getResources()
+ .getBoolean(R.bool.use_detail_activity);
+ Intent clickIntentTemplate = useDetailActivity
+ ? new Intent(context, DetailActivity.class)
+ : new Intent(context, MainActivity.class);
+ PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
+ .addNextIntentWithParentStack(clickIntentTemplate)
+ .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setPendingIntentTemplate(R.id.widget_list, clickPendingIntentTemplate);
+ views.setEmptyView(R.id.widget_list, R.id.widget_empty);
+
+ // Tell the AppWidgetManager to perform an update on the current app widget
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+ }
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ super.onReceive(context, intent);
+ if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(
+ new ComponentName(context, getClass()));
+ appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
+ }
+ }
+
+ /**
+ * Sets the remote adapter used to fill in the list items
+ *
+ * @param views RemoteViews to set the RemoteAdapter
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setRemoteAdapter(Context context, @NonNull final RemoteViews views) {
+ views.setRemoteAdapter(R.id.widget_list,
+ new Intent(context, DetailWidgetRemoteViewsService.class));
+ }
+
+ /**
+ * Sets the remote adapter used to fill in the list items
+ *
+ * @param views RemoteViews to set the RemoteAdapter
+ */
+ @SuppressWarnings("deprecation")
+ private void setRemoteAdapterV11(Context context, @NonNull final RemoteViews views) {
+ views.setRemoteAdapter(0, R.id.widget_list,
+ new Intent(context, DetailWidgetRemoteViewsService.class));
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java
new file mode 100644
index 00000000..f31c3ba9
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/DetailWidgetRemoteViewsService.java
@@ -0,0 +1,175 @@
+package com.example.android.sunshine.app.widget;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+import android.widget.AdapterView;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.Target;
+import com.example.android.sunshine.app.R;
+import com.example.android.sunshine.app.Utility;
+import com.example.android.sunshine.app.data.WeatherContract;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * RemoteViewsService controlling the data being shown in the scrollable weather detail widget
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class DetailWidgetRemoteViewsService extends RemoteViewsService {
+ public final String LOG_TAG = DetailWidgetRemoteViewsService.class.getSimpleName();
+ private static final String[] FORECAST_COLUMNS = {
+ WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry._ID,
+ WeatherContract.WeatherEntry.COLUMN_DATE,
+ WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
+ WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
+ WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
+ WeatherContract.WeatherEntry.COLUMN_MIN_TEMP
+ };
+ // these indices must match the projection
+ static final int INDEX_WEATHER_ID = 0;
+ static final int INDEX_WEATHER_DATE = 1;
+ static final int INDEX_WEATHER_CONDITION_ID = 2;
+ static final int INDEX_WEATHER_DESC = 3;
+ static final int INDEX_WEATHER_MAX_TEMP = 4;
+ static final int INDEX_WEATHER_MIN_TEMP = 5;
+
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ return new RemoteViewsFactory() {
+ private Cursor data = null;
+
+ @Override
+ public void onCreate() {
+ // Nothing to do
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ if (data != null) {
+ data.close();
+ }
+ // This method is called by the app hosting the widget (e.g., the launcher)
+ // However, our ContentProvider is not exported so it doesn't have access to the
+ // data. Therefore we need to clear (and finally restore) the calling identity so
+ // that calls use our process and permission
+ final long identityToken = Binder.clearCallingIdentity();
+ String location = Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
+ Uri weatherForLocationUri = WeatherContract.WeatherEntry
+ .buildWeatherLocationWithStartDate(location, System.currentTimeMillis());
+ data = getContentResolver().query(weatherForLocationUri,
+ FORECAST_COLUMNS,
+ null,
+ null,
+ WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (data != null) {
+ data.close();
+ data = null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return data == null ? 0 : data.getCount();
+ }
+
+ @Override
+ public RemoteViews getViewAt(int position) {
+ if (position == AdapterView.INVALID_POSITION ||
+ data == null || !data.moveToPosition(position)) {
+ return null;
+ }
+ RemoteViews views = new RemoteViews(getPackageName(),
+ R.layout.widget_detail_list_item);
+ int weatherId = data.getInt(INDEX_WEATHER_CONDITION_ID);
+ int weatherArtResourceId = Utility.getIconResourceForWeatherCondition(weatherId);
+ Bitmap weatherArtImage = null;
+ if ( !Utility.usingLocalGraphics(DetailWidgetRemoteViewsService.this) ) {
+ String weatherArtResourceUrl = Utility.getArtUrlForWeatherCondition(
+ DetailWidgetRemoteViewsService.this, weatherId);
+ try {
+ weatherArtImage = Glide.with(DetailWidgetRemoteViewsService.this)
+ .load(weatherArtResourceUrl)
+ .asBitmap()
+ .error(weatherArtResourceId)
+ .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Error retrieving large icon from " + weatherArtResourceUrl, e);
+ }
+ }
+ String description = data.getString(INDEX_WEATHER_DESC);
+ long dateInMillis = data.getLong(INDEX_WEATHER_DATE);
+ String formattedDate = Utility.getFriendlyDayString(
+ DetailWidgetRemoteViewsService.this, dateInMillis, false);
+ double maxTemp = data.getDouble(INDEX_WEATHER_MAX_TEMP);
+ double minTemp = data.getDouble(INDEX_WEATHER_MIN_TEMP);
+ String formattedMaxTemperature =
+ Utility.formatTemperature(DetailWidgetRemoteViewsService.this, maxTemp);
+ String formattedMinTemperature =
+ Utility.formatTemperature(DetailWidgetRemoteViewsService.this, minTemp);
+ if (weatherArtImage != null) {
+ views.setImageViewBitmap(R.id.widget_icon, weatherArtImage);
+ } else {
+ views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ setRemoteContentDescription(views, description);
+ }
+ views.setTextViewText(R.id.widget_date, formattedDate);
+ views.setTextViewText(R.id.widget_description, description);
+ views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
+ views.setTextViewText(R.id.widget_low_temperature, formattedMinTemperature);
+
+ final Intent fillInIntent = new Intent();
+ String locationSetting =
+ Utility.getPreferredLocation(DetailWidgetRemoteViewsService.this);
+ Uri weatherUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
+ locationSetting,
+ dateInMillis);
+ fillInIntent.setData(weatherUri);
+ views.setOnClickFillInIntent(R.id.widget_list_item, fillInIntent);
+ return views;
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private void setRemoteContentDescription(RemoteViews views, String description) {
+ views.setContentDescription(R.id.widget_icon, description);
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ return new RemoteViews(getPackageName(), R.layout.widget_detail_list_item);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (data.moveToPosition(position))
+ return data.getLong(INDEX_WEATHER_ID);
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java
new file mode 100644
index 00000000..0ab88541
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetIntentService.java
@@ -0,0 +1,151 @@
+/*
+ * 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.widget;
+
+import android.annotation.TargetApi;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.widget.RemoteViews;
+
+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;
+
+/**
+ * IntentService which handles updating all Today widgets with the latest data
+ */
+public class TodayWidgetIntentService extends IntentService {
+ private static final String[] FORECAST_COLUMNS = {
+ WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
+ WeatherContract.WeatherEntry.COLUMN_SHORT_DESC,
+ WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
+ WeatherContract.WeatherEntry.COLUMN_MIN_TEMP
+ };
+ // these indices must match the projection
+ private static final int INDEX_WEATHER_ID = 0;
+ private static final int INDEX_SHORT_DESC = 1;
+ private static final int INDEX_MAX_TEMP = 2;
+ private static final int INDEX_MIN_TEMP = 3;
+
+ public TodayWidgetIntentService() {
+ super("TodayWidgetIntentService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Retrieve all of the Today widget ids: these are the widgets we need to update
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
+ TodayWidgetProvider.class));
+
+ // Get today's data from the ContentProvider
+ String location = Utility.getPreferredLocation(this);
+ Uri weatherForLocationUri = WeatherContract.WeatherEntry.buildWeatherLocationWithStartDate(
+ location, System.currentTimeMillis());
+ Cursor data = getContentResolver().query(weatherForLocationUri, FORECAST_COLUMNS, null,
+ null, WeatherContract.WeatherEntry.COLUMN_DATE + " ASC");
+ if (data == null) {
+ return;
+ }
+ if (!data.moveToFirst()) {
+ data.close();
+ return;
+ }
+
+ // Extract the weather data from the Cursor
+ int weatherId = data.getInt(INDEX_WEATHER_ID);
+ int weatherArtResourceId = Utility.getArtResourceForWeatherCondition(weatherId);
+ String description = data.getString(INDEX_SHORT_DESC);
+ double maxTemp = data.getDouble(INDEX_MAX_TEMP);
+ double minTemp = data.getDouble(INDEX_MIN_TEMP);
+ String formattedMaxTemperature = Utility.formatTemperature(this, maxTemp);
+ String formattedMinTemperature = Utility.formatTemperature(this, minTemp);
+ data.close();
+
+ // Perform this loop procedure for each Today widget
+ for (int appWidgetId : appWidgetIds) {
+ // Find the correct layout based on the widget's width
+ int widgetWidth = getWidgetWidth(appWidgetManager, appWidgetId);
+ int defaultWidth = getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ int largeWidth = getResources().getDimensionPixelSize(R.dimen.widget_today_large_width);
+ int layoutId;
+ if (widgetWidth >= largeWidth) {
+ layoutId = R.layout.widget_today_large;
+ } else if (widgetWidth >= defaultWidth) {
+ layoutId = R.layout.widget_today;
+ } else {
+ layoutId = R.layout.widget_today_small;
+ }
+ RemoteViews views = new RemoteViews(getPackageName(), layoutId);
+
+ // Add the data to the RemoteViews
+ views.setImageViewResource(R.id.widget_icon, weatherArtResourceId);
+ // Content Descriptions for RemoteViews were only added in ICS MR1
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ setRemoteContentDescription(views, description);
+ }
+ views.setTextViewText(R.id.widget_description, description);
+ views.setTextViewText(R.id.widget_high_temperature, formattedMaxTemperature);
+ views.setTextViewText(R.id.widget_low_temperature, formattedMinTemperature);
+
+ // Create an Intent to launch MainActivity
+ Intent launchIntent = new Intent(this, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
+ views.setOnClickPendingIntent(R.id.widget, pendingIntent);
+
+ // Tell the AppWidgetManager to perform an update on the current app widget
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+ }
+
+ private int getWidgetWidth(AppWidgetManager appWidgetManager, int appWidgetId) {
+ // Prior to Jelly Bean, widgets were always their default size
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ return getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ }
+ // For Jelly Bean and higher devices, widgets can be resized - the current size can be
+ // retrieved from the newly added App Widget Options
+ return getWidgetWidthFromOptions(appWidgetManager, appWidgetId);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private int getWidgetWidthFromOptions(AppWidgetManager appWidgetManager, int appWidgetId) {
+ Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
+ if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)) {
+ int minWidthDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
+ // The width returned is in dp, but we'll convert it to pixels to match the other widths
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, minWidthDp,
+ displayMetrics);
+ }
+ return getResources().getDimensionPixelSize(R.dimen.widget_today_default_width);
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private void setRemoteContentDescription(RemoteViews views, String description) {
+ views.setContentDescription(R.id.widget_icon, description);
+ }
+}
diff --git a/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java
new file mode 100644
index 00000000..81c1dfe6
--- /dev/null
+++ b/app/src/main/java/com/example/android/sunshine/app/widget/TodayWidgetProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.widget;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+
+import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
+
+/**
+ * Provider for a horizontally expandable widget showing today's weather.
+ *
+ * Delegates widget updating to {@link TodayWidgetIntentService} to ensure that
+ * data retrieval is done on a background thread
+ */
+public class TodayWidgetProvider extends AppWidgetProvider {
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
+ }
+
+ @Override
+ public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
+ int appWidgetId, Bundle newOptions) {
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
+ }
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ super.onReceive(context, intent);
+ if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
+ context.startService(new Intent(context, TodayWidgetIntentService.class));
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi-v11/ic_status.png b/app/src/main/res/drawable-hdpi-v11/ic_status.png
new file mode 100755
index 00000000..b495a2d5
Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_status.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_clear.png b/app/src/main/res/drawable-hdpi/art_clear.png
index 6454f661..93ec0e1c 100755
Binary files a/app/src/main/res/drawable-hdpi/art_clear.png and b/app/src/main/res/drawable-hdpi/art_clear.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_clouds.png b/app/src/main/res/drawable-hdpi/art_clouds.png
index 14d4f7da..5aae4adb 100755
Binary files a/app/src/main/res/drawable-hdpi/art_clouds.png and b/app/src/main/res/drawable-hdpi/art_clouds.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_fog.png b/app/src/main/res/drawable-hdpi/art_fog.png
index 81a12213..97c339ca 100755
Binary files a/app/src/main/res/drawable-hdpi/art_fog.png and b/app/src/main/res/drawable-hdpi/art_fog.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_light_clouds.png b/app/src/main/res/drawable-hdpi/art_light_clouds.png
index f51c1bdb..5b008520 100755
Binary files a/app/src/main/res/drawable-hdpi/art_light_clouds.png and b/app/src/main/res/drawable-hdpi/art_light_clouds.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_light_rain.png b/app/src/main/res/drawable-hdpi/art_light_rain.png
index 01950751..9035f9cb 100755
Binary files a/app/src/main/res/drawable-hdpi/art_light_rain.png and b/app/src/main/res/drawable-hdpi/art_light_rain.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_rain.png b/app/src/main/res/drawable-hdpi/art_rain.png
index 1979544a..f8ca0aff 100755
Binary files a/app/src/main/res/drawable-hdpi/art_rain.png and b/app/src/main/res/drawable-hdpi/art_rain.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_snow.png b/app/src/main/res/drawable-hdpi/art_snow.png
index 512fcdd4..99a0a387 100755
Binary files a/app/src/main/res/drawable-hdpi/art_snow.png and b/app/src/main/res/drawable-hdpi/art_snow.png differ
diff --git a/app/src/main/res/drawable-hdpi/art_storm.png b/app/src/main/res/drawable-hdpi/art_storm.png
index ec8cc973..4eeb6f3a 100755
Binary files a/app/src/main/res/drawable-hdpi/art_storm.png and b/app/src/main/res/drawable-hdpi/art_storm.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_clear.png b/app/src/main/res/drawable-hdpi/ic_clear.png
index 3313c3ab..f23c6172 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_clear.png and b/app/src/main/res/drawable-hdpi/ic_clear.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_cloudy.png b/app/src/main/res/drawable-hdpi/ic_cloudy.png
index a5f1a5a5..461fb650 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_cloudy.png and b/app/src/main/res/drawable-hdpi/ic_cloudy.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_current_location.png b/app/src/main/res/drawable-hdpi/ic_current_location.png
new file mode 100644
index 00000000..85e38726
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_current_location.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_fog.png b/app/src/main/res/drawable-hdpi/ic_fog.png
index 990c8e34..6f15a126 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_fog.png and b/app/src/main/res/drawable-hdpi/ic_fog.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_light_clouds.png b/app/src/main/res/drawable-hdpi/ic_light_clouds.png
index fe38e489..c4f68f03 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_light_clouds.png and b/app/src/main/res/drawable-hdpi/ic_light_clouds.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_light_rain.png b/app/src/main/res/drawable-hdpi/ic_light_rain.png
index ae9468c3..89bba0bc 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_light_rain.png and b/app/src/main/res/drawable-hdpi/ic_light_rain.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_logo.png b/app/src/main/res/drawable-hdpi/ic_logo.png
index 22896379..0ba410ea 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_logo.png and b/app/src/main/res/drawable-hdpi/ic_logo.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_rain.png b/app/src/main/res/drawable-hdpi/ic_rain.png
index 0e858264..62b4d689 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_rain.png and b/app/src/main/res/drawable-hdpi/ic_rain.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_snow.png b/app/src/main/res/drawable-hdpi/ic_snow.png
index 0f8bfab9..481ef3d7 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_snow.png and b/app/src/main/res/drawable-hdpi/ic_snow.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_status.png b/app/src/main/res/drawable-hdpi/ic_status.png
new file mode 100755
index 00000000..7c54cd0a
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_status.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_storm.png b/app/src/main/res/drawable-hdpi/ic_storm.png
index 27e5429b..b8098326 100755
Binary files a/app/src/main/res/drawable-hdpi/ic_storm.png and b/app/src/main/res/drawable-hdpi/ic_storm.png differ
diff --git a/app/src/main/res/drawable-mdpi-v11/ic_status.png b/app/src/main/res/drawable-mdpi-v11/ic_status.png
new file mode 100755
index 00000000..c87ef7ec
Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_status.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_clear.png b/app/src/main/res/drawable-mdpi/art_clear.png
index 2cf330ac..3ae74979 100755
Binary files a/app/src/main/res/drawable-mdpi/art_clear.png and b/app/src/main/res/drawable-mdpi/art_clear.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_clouds.png b/app/src/main/res/drawable-mdpi/art_clouds.png
index 5aa10ca3..3bb970b4 100755
Binary files a/app/src/main/res/drawable-mdpi/art_clouds.png and b/app/src/main/res/drawable-mdpi/art_clouds.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_fog.png b/app/src/main/res/drawable-mdpi/art_fog.png
index 1357a247..e399249b 100755
Binary files a/app/src/main/res/drawable-mdpi/art_fog.png and b/app/src/main/res/drawable-mdpi/art_fog.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_light_clouds.png b/app/src/main/res/drawable-mdpi/art_light_clouds.png
index 7eecc6a7..3dd4e170 100755
Binary files a/app/src/main/res/drawable-mdpi/art_light_clouds.png and b/app/src/main/res/drawable-mdpi/art_light_clouds.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_light_rain.png b/app/src/main/res/drawable-mdpi/art_light_rain.png
index 8e601654..e482b38f 100755
Binary files a/app/src/main/res/drawable-mdpi/art_light_rain.png and b/app/src/main/res/drawable-mdpi/art_light_rain.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_rain.png b/app/src/main/res/drawable-mdpi/art_rain.png
index 3a518e59..038ea1ba 100755
Binary files a/app/src/main/res/drawable-mdpi/art_rain.png and b/app/src/main/res/drawable-mdpi/art_rain.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_snow.png b/app/src/main/res/drawable-mdpi/art_snow.png
index 7491c5ef..fb9933b5 100755
Binary files a/app/src/main/res/drawable-mdpi/art_snow.png and b/app/src/main/res/drawable-mdpi/art_snow.png differ
diff --git a/app/src/main/res/drawable-mdpi/art_storm.png b/app/src/main/res/drawable-mdpi/art_storm.png
index 9aee2f32..b312ba31 100755
Binary files a/app/src/main/res/drawable-mdpi/art_storm.png and b/app/src/main/res/drawable-mdpi/art_storm.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_clear.png b/app/src/main/res/drawable-mdpi/ic_clear.png
index b6a5426c..cc7edb7b 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_clear.png and b/app/src/main/res/drawable-mdpi/ic_clear.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_cloudy.png b/app/src/main/res/drawable-mdpi/ic_cloudy.png
index ef6f0253..07b34006 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_cloudy.png and b/app/src/main/res/drawable-mdpi/ic_cloudy.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_current_location.png b/app/src/main/res/drawable-mdpi/ic_current_location.png
new file mode 100644
index 00000000..5684aa7d
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_current_location.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_fog.png b/app/src/main/res/drawable-mdpi/ic_fog.png
index 95383c16..976d707d 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_fog.png and b/app/src/main/res/drawable-mdpi/ic_fog.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_light_clouds.png b/app/src/main/res/drawable-mdpi/ic_light_clouds.png
index 1aaf9256..85d2230d 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_light_clouds.png and b/app/src/main/res/drawable-mdpi/ic_light_clouds.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_light_rain.png b/app/src/main/res/drawable-mdpi/ic_light_rain.png
index 2b7133a6..fce4e0ef 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_light_rain.png and b/app/src/main/res/drawable-mdpi/ic_light_rain.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_logo.png b/app/src/main/res/drawable-mdpi/ic_logo.png
index 464a1022..bf2a868c 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_logo.png and b/app/src/main/res/drawable-mdpi/ic_logo.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_rain.png b/app/src/main/res/drawable-mdpi/ic_rain.png
index f7070560..19280cc1 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_rain.png and b/app/src/main/res/drawable-mdpi/ic_rain.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_snow.png b/app/src/main/res/drawable-mdpi/ic_snow.png
index 2970d9b7..a48a28bc 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_snow.png and b/app/src/main/res/drawable-mdpi/ic_snow.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_status.png b/app/src/main/res/drawable-mdpi/ic_status.png
new file mode 100755
index 00000000..161ae269
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_status.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_storm.png b/app/src/main/res/drawable-mdpi/ic_storm.png
index 40649b2e..2e2af644 100755
Binary files a/app/src/main/res/drawable-mdpi/ic_storm.png and b/app/src/main/res/drawable-mdpi/ic_storm.png differ
diff --git a/app/src/main/res/drawable-nodpi/ic_muzei.png b/app/src/main/res/drawable-nodpi/ic_muzei.png
new file mode 100755
index 00000000..b0fe3c49
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_muzei.png differ
diff --git a/app/src/main/res/drawable-nodpi/widget_preview_detail.png b/app/src/main/res/drawable-nodpi/widget_preview_detail.png
new file mode 100644
index 00000000..7e5a7969
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/widget_preview_detail.png differ
diff --git a/app/src/main/res/drawable-nodpi/widget_preview_today.png b/app/src/main/res/drawable-nodpi/widget_preview_today.png
new file mode 100644
index 00000000..7be01f0d
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/widget_preview_today.png differ
diff --git a/app/src/main/res/drawable-v21/today_touch_selector.xml b/app/src/main/res/drawable-v21/today_touch_selector.xml
index 24db9c97..90d8c5bf 100644
--- a/app/src/main/res/drawable-v21/today_touch_selector.xml
+++ b/app/src/main/res/drawable-v21/today_touch_selector.xml
@@ -20,10 +20,10 @@
+ android:drawable="@color/primary_light"/>
+ android:drawable="@color/primary_light"/>
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/touch_selector.xml b/app/src/main/res/drawable-v21/touch_selector.xml
index 40e060c0..e7b7f947 100644
--- a/app/src/main/res/drawable-v21/touch_selector.xml
+++ b/app/src/main/res/drawable-v21/touch_selector.xml
@@ -15,17 +15,10 @@
limitations under the License.
-->
-
-
- -
-
-
-
-
+ android:drawable="@color/activated"/>
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/touch_selector_activated.xml b/app/src/main/res/drawable-v21/touch_selector_activated.xml
new file mode 100644
index 00000000..04416609
--- /dev/null
+++ b/app/src/main/res/drawable-v21/touch_selector_activated.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/touch_selector_base.xml b/app/src/main/res/drawable-v21/touch_selector_base.xml
new file mode 100644
index 00000000..1de190be
--- /dev/null
+++ b/app/src/main/res/drawable-v21/touch_selector_base.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/touch_selector_white.xml b/app/src/main/res/drawable-v21/touch_selector_white.xml
new file mode 100644
index 00000000..5f566bbf
--- /dev/null
+++ b/app/src/main/res/drawable-v21/touch_selector_white.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_status.png b/app/src/main/res/drawable-xhdpi-v11/ic_status.png
new file mode 100755
index 00000000..254cf671
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_status.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_clear.png b/app/src/main/res/drawable-xhdpi/art_clear.png
index bfa8854e..f2c16460 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_clear.png and b/app/src/main/res/drawable-xhdpi/art_clear.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_clouds.png b/app/src/main/res/drawable-xhdpi/art_clouds.png
index d2d8a48e..e62f38fd 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_clouds.png and b/app/src/main/res/drawable-xhdpi/art_clouds.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_fog.png b/app/src/main/res/drawable-xhdpi/art_fog.png
index de312d31..8722f239 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_fog.png and b/app/src/main/res/drawable-xhdpi/art_fog.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_light_clouds.png b/app/src/main/res/drawable-xhdpi/art_light_clouds.png
index 6a3f64a6..64000f57 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_light_clouds.png and b/app/src/main/res/drawable-xhdpi/art_light_clouds.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_light_rain.png b/app/src/main/res/drawable-xhdpi/art_light_rain.png
index 10fc0bd2..973346ec 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_light_rain.png and b/app/src/main/res/drawable-xhdpi/art_light_rain.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_rain.png b/app/src/main/res/drawable-xhdpi/art_rain.png
index 03555f65..77315a51 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_rain.png and b/app/src/main/res/drawable-xhdpi/art_rain.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_snow.png b/app/src/main/res/drawable-xhdpi/art_snow.png
index 9b41604e..74f9975d 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_snow.png and b/app/src/main/res/drawable-xhdpi/art_snow.png differ
diff --git a/app/src/main/res/drawable-xhdpi/art_storm.png b/app/src/main/res/drawable-xhdpi/art_storm.png
index 72165118..5cc39c47 100755
Binary files a/app/src/main/res/drawable-xhdpi/art_storm.png and b/app/src/main/res/drawable-xhdpi/art_storm.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_clear.png b/app/src/main/res/drawable-xhdpi/ic_clear.png
index e6252d58..61e7f6d2 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_clear.png and b/app/src/main/res/drawable-xhdpi/ic_clear.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_cloudy.png b/app/src/main/res/drawable-xhdpi/ic_cloudy.png
index 4b5cd7c3..a0354aa0 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_cloudy.png and b/app/src/main/res/drawable-xhdpi/ic_cloudy.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_current_location.png b/app/src/main/res/drawable-xhdpi/ic_current_location.png
new file mode 100644
index 00000000..7faa3455
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_current_location.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_fog.png b/app/src/main/res/drawable-xhdpi/ic_fog.png
index 33c152a2..a7124f4e 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_fog.png and b/app/src/main/res/drawable-xhdpi/ic_fog.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_light_clouds.png b/app/src/main/res/drawable-xhdpi/ic_light_clouds.png
index 712cc739..a5284b6b 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_light_clouds.png and b/app/src/main/res/drawable-xhdpi/ic_light_clouds.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_light_rain.png b/app/src/main/res/drawable-xhdpi/ic_light_rain.png
index 5521b1b4..f7019f36 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_light_rain.png and b/app/src/main/res/drawable-xhdpi/ic_light_rain.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_logo.png b/app/src/main/res/drawable-xhdpi/ic_logo.png
index 02fc44ac..193fcd32 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_logo.png and b/app/src/main/res/drawable-xhdpi/ic_logo.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_rain.png b/app/src/main/res/drawable-xhdpi/ic_rain.png
index f3acb4d1..1989d52b 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_rain.png and b/app/src/main/res/drawable-xhdpi/ic_rain.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_snow.png b/app/src/main/res/drawable-xhdpi/ic_snow.png
index 791967b3..9490fbe2 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_snow.png and b/app/src/main/res/drawable-xhdpi/ic_snow.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_status.png b/app/src/main/res/drawable-xhdpi/ic_status.png
new file mode 100755
index 00000000..e30a0d12
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_status.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_storm.png b/app/src/main/res/drawable-xhdpi/ic_storm.png
index 3ddfade8..138c58c8 100755
Binary files a/app/src/main/res/drawable-xhdpi/ic_storm.png and b/app/src/main/res/drawable-xhdpi/ic_storm.png differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_status.png b/app/src/main/res/drawable-xxhdpi-v11/ic_status.png
new file mode 100755
index 00000000..4a98e40f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_status.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_clear.png b/app/src/main/res/drawable-xxhdpi/art_clear.png
index 55dc436e..80792e40 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_clear.png and b/app/src/main/res/drawable-xxhdpi/art_clear.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_clouds.png b/app/src/main/res/drawable-xxhdpi/art_clouds.png
index 13fe2722..61ae4bfe 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_clouds.png and b/app/src/main/res/drawable-xxhdpi/art_clouds.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_fog.png b/app/src/main/res/drawable-xxhdpi/art_fog.png
index 8e7574bc..ea2993f1 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_fog.png and b/app/src/main/res/drawable-xxhdpi/art_fog.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_light_clouds.png b/app/src/main/res/drawable-xxhdpi/art_light_clouds.png
index 8a33e1be..77b2fae6 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_light_clouds.png and b/app/src/main/res/drawable-xxhdpi/art_light_clouds.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_light_rain.png b/app/src/main/res/drawable-xxhdpi/art_light_rain.png
index 84437180..eacaf394 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_light_rain.png and b/app/src/main/res/drawable-xxhdpi/art_light_rain.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_rain.png b/app/src/main/res/drawable-xxhdpi/art_rain.png
index 921bb146..584c2e1d 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_rain.png and b/app/src/main/res/drawable-xxhdpi/art_rain.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_snow.png b/app/src/main/res/drawable-xxhdpi/art_snow.png
index b5892cea..d6a3ac97 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_snow.png and b/app/src/main/res/drawable-xxhdpi/art_snow.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/art_storm.png b/app/src/main/res/drawable-xxhdpi/art_storm.png
index 5f73b32a..2ae3de5e 100755
Binary files a/app/src/main/res/drawable-xxhdpi/art_storm.png and b/app/src/main/res/drawable-xxhdpi/art_storm.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_clear.png b/app/src/main/res/drawable-xxhdpi/ic_clear.png
index 221d1241..a1be87f8 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_clear.png and b/app/src/main/res/drawable-xxhdpi/ic_clear.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_cloudy.png b/app/src/main/res/drawable-xxhdpi/ic_cloudy.png
index c8c08b89..dcb12486 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_cloudy.png and b/app/src/main/res/drawable-xxhdpi/ic_cloudy.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_current_location.png b/app/src/main/res/drawable-xxhdpi/ic_current_location.png
new file mode 100644
index 00000000..d3a1ab08
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_current_location.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_fog.png b/app/src/main/res/drawable-xxhdpi/ic_fog.png
index 38250eb3..e65fcceb 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_fog.png and b/app/src/main/res/drawable-xxhdpi/ic_fog.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_light_clouds.png b/app/src/main/res/drawable-xxhdpi/ic_light_clouds.png
index 97fc9af5..b7152284 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_light_clouds.png and b/app/src/main/res/drawable-xxhdpi/ic_light_clouds.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_light_rain.png b/app/src/main/res/drawable-xxhdpi/ic_light_rain.png
index 4da9bb9a..2e9ea3c3 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_light_rain.png and b/app/src/main/res/drawable-xxhdpi/ic_light_rain.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_logo.png b/app/src/main/res/drawable-xxhdpi/ic_logo.png
index 9e04aad5..cb8558f9 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_logo.png and b/app/src/main/res/drawable-xxhdpi/ic_logo.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_rain.png b/app/src/main/res/drawable-xxhdpi/ic_rain.png
index c0d4d522..915fc212 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_rain.png and b/app/src/main/res/drawable-xxhdpi/ic_rain.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_snow.png b/app/src/main/res/drawable-xxhdpi/ic_snow.png
index 0ce80853..596b187e 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_snow.png and b/app/src/main/res/drawable-xxhdpi/ic_snow.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_status.png b/app/src/main/res/drawable-xxhdpi/ic_status.png
new file mode 100755
index 00000000..17a9d399
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_status.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_storm.png b/app/src/main/res/drawable-xxhdpi/ic_storm.png
index c1daf9c8..bec7a4d7 100755
Binary files a/app/src/main/res/drawable-xxhdpi/ic_storm.png and b/app/src/main/res/drawable-xxhdpi/ic_storm.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/mipmap/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/mipmap/ic_launcher.png
new file mode 100755
index 00000000..cc53ae90
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/mipmap/ic_launcher.png differ
diff --git a/app/src/main/res/drawable/today_touch_selector.xml b/app/src/main/res/drawable/today_touch_selector.xml
index c37aee3c..87ad6e55 100644
--- a/app/src/main/res/drawable/today_touch_selector.xml
+++ b/app/src/main/res/drawable/today_touch_selector.xml
@@ -16,13 +16,13 @@
-->
+ android:drawable="@color/primary_light"/>
+ android:drawable="@color/primary_light"/>
+ android:drawable="@color/primary_light"/>
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/touch_selector.xml b/app/src/main/res/drawable/touch_selector.xml
index cabcb067..71fddcaf 100644
--- a/app/src/main/res/drawable/touch_selector.xml
+++ b/app/src/main/res/drawable/touch_selector.xml
@@ -1,7 +1,6 @@
+
+
-
-
+
-
-
+
+
+
+
+
+
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/touch_selector_activated.xml b/app/src/main/res/drawable/touch_selector_activated.xml
new file mode 100644
index 00000000..02817e47
--- /dev/null
+++ b/app/src/main/res/drawable/touch_selector_activated.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/touch_selector_white.xml b/app/src/main/res/drawable/touch_selector_white.xml
new file mode 100644
index 00000000..77d1ecde
--- /dev/null
+++ b/app/src/main/res/drawable/touch_selector_white.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/fragment_main.xml b/app/src/main/res/layout-land/fragment_main.xml
new file mode 100644
index 00000000..d4be168e
--- /dev/null
+++ b/app/src/main/res/layout-land/fragment_main.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/list_item_forecast_today.xml b/app/src/main/res/layout-land/list_item_forecast_today.xml
new file mode 100644
index 00000000..40ee0620
--- /dev/null
+++ b/app/src/main/res/layout-land/list_item_forecast_today.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw600dp-port/activity_main.xml b/app/src/main/res/layout-sw600dp-port/activity_main.xml
new file mode 100644
index 00000000..5ed510d7
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/activity_main.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw600dp/activity_main.xml b/app/src/main/res/layout-sw600dp/activity_main.xml
index b4cf8521..5ac907f2 100644
--- a/app/src/main/res/layout-sw600dp/activity_main.xml
+++ b/app/src/main/res/layout-sw600dp/activity_main.xml
@@ -13,31 +13,105 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+ android:layout_height="match_parent">
-
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_alignEnd="@id/layout_center"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignRight="@id/layout_center"
+ android:layout_below="@id/appbar"
+ tools:layout="@android:layout/list_content"
+ android:choiceMode="singleChoice"
+ app:autoSelectView="true"
+ />
-
+
+ android:layout_alignLeft="@id/layout_center"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginTop="?attr/actionBarSize"
+ android:layout_alignStart="@id/layout_center"
+ android:elevation="@dimen/appbar_elevation"
+ android:layout_marginRight="@dimen/list_item_extra_padding"
+ android:layout_marginEnd="@dimen/list_item_extra_padding"
+ />
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw600dp/fragment_main.xml b/app/src/main/res/layout-sw600dp/fragment_main.xml
new file mode 100644
index 00000000..6b7e76c4
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp/fragment_main.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml
index de553001..280300c1 100644
--- a/app/src/main/res/layout/activity_detail.xml
+++ b/app/src/main/res/layout/activity_detail.xml
@@ -19,4 +19,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.sunshine.app.DetailActivity"
- tools:ignore="MergeRootFrame" />
\ No newline at end of file
+ tools:ignore="MergeRootFrame" />
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 66c5a988..aaabb202 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -14,10 +14,9 @@
limitations under the License.
-->
\ No newline at end of file
+ android:layout_height="wrap_content"
+ app:sharedElementTransitions="true"/>
diff --git a/app/src/main/res/layout/detail_extras_grid.xml b/app/src/main/res/layout/detail_extras_grid.xml
new file mode 100644
index 00000000..88e39382
--- /dev/null
+++ b/app/src/main/res/layout/detail_extras_grid.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/detail_today_grid.xml b/app/src/main/res/layout/detail_today_grid.xml
new file mode 100644
index 00000000..03e4c4c8
--- /dev/null
+++ b/app/src/main/res/layout/detail_today_grid.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml
index dd3b4f8b..881cc62d 100644
--- a/app/src/main/res/layout/fragment_detail.xml
+++ b/app/src/main/res/layout/fragment_detail.xml
@@ -14,113 +14,27 @@
limitations under the License.
-->
-
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
+ android:layout_height="?attr/actionBarSize"
+ android:background="@android:color/white" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
+
diff --git a/app/src/main/res/layout/fragment_detail_twopane.xml b/app/src/main/res/layout/fragment_detail_twopane.xml
new file mode 100644
index 00000000..7eec2254
--- /dev/null
+++ b/app/src/main/res/layout/fragment_detail_twopane.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_detail_wide.xml b/app/src/main/res/layout/fragment_detail_wide.xml
index 3c8c8896..b92648aa 100644
--- a/app/src/main/res/layout/fragment_detail_wide.xml
+++ b/app/src/main/res/layout/fragment_detail_wide.xml
@@ -14,109 +14,119 @@
limitations under the License.
-->
-
+ android:layout_height="match_parent">
-
-
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="match_parent"
+ app:columnCount="2"
+ android:background="@color/detail_accent_pane_background"
+ android:paddingEnd="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
+ >
-
-
+
-
-
-
-
-
+ android:id="@+id/detail_humidity_label_textview"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ android:text="@string/humidity"
+ android:textColor="@color/detail_accent_label"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ android:paddingLeft="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingBottom="@dimen/abc_list_item_padding_horizontal_material"
+ />
+ android:id="@+id/detail_humidity_textview"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ tools:text="38%"
+ android:textColor="@android:color/white"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ />
-
-
-
+ android:id="@+id/detail_pressure_label_textview"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ android:text="@string/pressure"
+ android:textColor="@color/detail_accent_label"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ android:paddingTop="@dimen/detail_view_extra_padding"
+ android:paddingLeft="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingBottom="@dimen/abc_list_item_padding_horizontal_material"
+ />
+
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ tools:text="995 hPa"
+ android:textColor="@android:color/white"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ />
-
-
-
-
-
+ android:id="@+id/detail_wind_label_textview"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ android:text="@string/wind"
+ android:textColor="@color/detail_accent_label"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ android:paddingTop="@dimen/detail_view_extra_padding"
+ android:paddingLeft="@dimen/abc_list_item_padding_horizontal_material"
+ android:paddingRight="@dimen/abc_list_item_padding_horizontal_material"
+ />
-
+ android:id="@+id/detail_wind_textview"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ tools:text="4km/h NW"
+ android:textColor="@android:color/white"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ />
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index 6cadcd8b..089e9a04 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -13,15 +13,67 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_main_base.xml b/app/src/main/res/layout/fragment_main_base.xml
new file mode 100644
index 00000000..26256281
--- /dev/null
+++ b/app/src/main/res/layout/fragment_main_base.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_base_forecast_today.xml b/app/src/main/res/layout/list_item_base_forecast_today.xml
new file mode 100644
index 00000000..86a3dc4c
--- /dev/null
+++ b/app/src/main/res/layout/list_item_base_forecast_today.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_forecast.xml b/app/src/main/res/layout/list_item_forecast.xml
index ea5e1566..a4c1c14b 100644
--- a/app/src/main/res/layout/list_item_forecast.xml
+++ b/app/src/main/res/layout/list_item_forecast.xml
@@ -15,68 +15,74 @@
limitations under the License.
-->
-
+
+ android:background="@drawable/touch_selector"
+ android:paddingLeft="@dimen/list_item_extra_padding"
+ android:paddingRight="@dimen/list_item_extra_padding"
+ >
-
+
+ android:layout_width="@dimen/list_icon"
+ android:layout_height="@dimen/list_icon"
+ android:layout_marginRight="@dimen/abc_list_item_padding_horizontal_material"
+ android:layout_marginEnd="@dimen/abc_list_item_padding_horizontal_material"
+ />
-
-
-
-
-
+ android:layout_width="0dp"
+ android:layout_weight="7"
+ android:orientation="vertical">
-
-
+
-
+
+
+ android:gravity="right"
+ android:layout_marginRight="@dimen/forecast_temperature_space"
+ android:layout_marginEnd="@dimen/forecast_temperature_space"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/primary_text"
+ android:textSize="@dimen/forecast_text_size"/>
+ android:gravity="right"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/forecast_low_text"
+ android:textSize="@dimen/forecast_text_size"/>
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_forecast_today.xml b/app/src/main/res/layout/list_item_forecast_today.xml
index abbc3d3d..1a3af298 100644
--- a/app/src/main/res/layout/list_item_forecast_today.xml
+++ b/app/src/main/res/layout/list_item_forecast_today.xml
@@ -1,3 +1,4 @@
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
\ No newline at end of file
+ />
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pref_current_location.xml b/app/src/main/res/layout/pref_current_location.xml
new file mode 100644
index 00000000..a5d79382
--- /dev/null
+++ b/app/src/main/res/layout/pref_current_location.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_detail.xml b/app/src/main/res/layout/widget_detail.xml
new file mode 100644
index 00000000..364d33c0
--- /dev/null
+++ b/app/src/main/res/layout/widget_detail.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_detail_list_item.xml b/app/src/main/res/layout/widget_detail_list_item.xml
new file mode 100644
index 00000000..2231b659
--- /dev/null
+++ b/app/src/main/res/layout/widget_detail_list_item.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_today.xml b/app/src/main/res/layout/widget_today.xml
new file mode 100644
index 00000000..caf01ed0
--- /dev/null
+++ b/app/src/main/res/layout/widget_today.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_today_large.xml b/app/src/main/res/layout/widget_today_large.xml
new file mode 100644
index 00000000..3838aafd
--- /dev/null
+++ b/app/src/main/res/layout/widget_today_large.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_today_small.xml b/app/src/main/res/layout/widget_today_small.xml
new file mode 100644
index 00000000..d9b2d196
--- /dev/null
+++ b/app/src/main/res/layout/widget_today_small.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/detailfragment.xml b/app/src/main/res/menu/detailfragment.xml
index b929cc89..c92c7c6e 100644
--- a/app/src/main/res/menu/detailfragment.xml
+++ b/app/src/main/res/menu/detailfragment.xml
@@ -18,6 +18,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+ android:icon="@drawable/abc_ic_menu_share_mtrl_alpha"
+ app:showAsAction="always"/>
diff --git a/app/src/main/res/transition-land-v21/details_window_enter_transition.xml b/app/src/main/res/transition-land-v21/details_window_enter_transition.xml
new file mode 100644
index 00000000..89466733
--- /dev/null
+++ b/app/src/main/res/transition-land-v21/details_window_enter_transition.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/transition-land-v21/details_window_return_transition.xml b/app/src/main/res/transition-land-v21/details_window_return_transition.xml
new file mode 100644
index 00000000..8662f005
--- /dev/null
+++ b/app/src/main/res/transition-land-v21/details_window_return_transition.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/transition-v21/details_window_enter_transition.xml b/app/src/main/res/transition-v21/details_window_enter_transition.xml
new file mode 100644
index 00000000..74a71b42
--- /dev/null
+++ b/app/src/main/res/transition-v21/details_window_enter_transition.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/transition-v21/details_window_return_transition.xml b/app/src/main/res/transition-v21/details_window_return_transition.xml
new file mode 100644
index 00000000..cf7009e9
--- /dev/null
+++ b/app/src/main/res/transition-v21/details_window_return_transition.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-land/refs.xml b/app/src/main/res/values-land/refs.xml
index 0fb3f377..3b8dd9ea 100644
--- a/app/src/main/res/values-land/refs.xml
+++ b/app/src/main/res/values-land/refs.xml
@@ -19,5 +19,5 @@
http://developer.android.com/training/multiscreen/screensizes.html#TaskUseAliasFilters
-->
- - @layout/fragment_detail_wide
+ - @layout/fragment_detail_wide
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp-land/refs.xml b/app/src/main/res/values-sw600dp-land/refs.xml
new file mode 100644
index 00000000..4f951bc1
--- /dev/null
+++ b/app/src/main/res/values-sw600dp-land/refs.xml
@@ -0,0 +1,19 @@
+
+
+
+ - @layout/fragment_detail_twopane
+
diff --git a/app/src/main/res/values-sw600dp-port/dimens.xml b/app/src/main/res/values-sw600dp-port/dimens.xml
new file mode 100644
index 00000000..f339956a
--- /dev/null
+++ b/app/src/main/res/values-sw600dp-port/dimens.xml
@@ -0,0 +1,22 @@
+
+
+
+ 80dp
+ @dimen/abc_list_item_padding_horizontal_material
+ @dimen/abc_list_item_padding_horizontal_material
+
+
diff --git a/app/src/main/res/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml
new file mode 100644
index 00000000..a2b6639a
--- /dev/null
+++ b/app/src/main/res/values-sw600dp/bools.xml
@@ -0,0 +1,4 @@
+
+
+ false
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml
new file mode 100644
index 00000000..a0a40bd9
--- /dev/null
+++ b/app/src/main/res/values-sw600dp/dimens.xml
@@ -0,0 +1,19 @@
+
+
+
+ 0dp
+
diff --git a/app/src/main/res/values-sw600dp/refs.xml b/app/src/main/res/values-sw600dp/refs.xml
index 6825d5d8..6e36e8d0 100644
--- a/app/src/main/res/values-sw600dp/refs.xml
+++ b/app/src/main/res/values-sw600dp/refs.xml
@@ -20,5 +20,5 @@
-->
- - @layout/fragment_detail_wide
+ - @layout/fragment_detail_wide
diff --git a/app/src/main/res/values-sw600dp/styles.xml b/app/src/main/res/values-sw600dp/styles.xml
index e1277703..26e7a125 100644
--- a/app/src/main/res/values-sw600dp/styles.xml
+++ b/app/src/main/res/values-sw600dp/styles.xml
@@ -15,7 +15,7 @@
-->
-
diff --git a/app/src/main/res/values-sw720dp-port/dimens.xml b/app/src/main/res/values-sw720dp-port/dimens.xml
new file mode 100644
index 00000000..6dea874d
--- /dev/null
+++ b/app/src/main/res/values-sw720dp-port/dimens.xml
@@ -0,0 +1,22 @@
+
+
+
+ 80dp
+ @dimen/abc_list_item_padding_horizontal_material
+ @dimen/abc_list_item_padding_horizontal_material
+
+
diff --git a/app/src/main/res/values-sw720dp/dimens.xml b/app/src/main/res/values-sw720dp/dimens.xml
new file mode 100644
index 00000000..2723df5d
--- /dev/null
+++ b/app/src/main/res/values-sw720dp/dimens.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ 64dp
+ @dimen/abc_list_item_padding_horizontal_material
+ @dimen/abc_action_bar_default_height_material
+
+ @dimen/abc_list_item_padding_horizontal_material
+ @dimen/abc_action_bar_default_height_material
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v11/bools.xml b/app/src/main/res/values-v11/bools.xml
new file mode 100644
index 00000000..d187294a
--- /dev/null
+++ b/app/src/main/res/values-v11/bools.xml
@@ -0,0 +1,4 @@
+
+
+ true
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml
index 92981e09..2e685a1c 100644
--- a/app/src/main/res/values-v14/styles.xml
+++ b/app/src/main/res/values-v14/styles.xml
@@ -22,7 +22,7 @@
\ No newline at end of file
diff --git a/app/src/main/res/values-v14/widget_dimens.xml b/app/src/main/res/values-v14/widget_dimens.xml
new file mode 100644
index 00000000..f02fa430
--- /dev/null
+++ b/app/src/main/res/values-v14/widget_dimens.xml
@@ -0,0 +1,18 @@
+
+
+
+ 0dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
index f17a657b..b20ad070 100644
--- a/app/src/main/res/values-v21/styles.xml
+++ b/app/src/main/res/values-v21/styles.xml
@@ -17,8 +17,18 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 5aa1addf..54060c0b 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -25,4 +25,16 @@
- @string/pref_units_metric
- @string/pref_units_imperial
+
+
+
+ - @string/pref_art_pack_label_sunshine
+ - @string/pref_art_pack_label_cute_dogs
+
+
+
+
+ - @string/pref_art_pack_sunshine
+ - @string/pref_art_pack_cute_dogs
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..23c756f2
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
new file mode 100644
index 00000000..11c11a98
--- /dev/null
+++ b/app/src/main/res/values/bools.xml
@@ -0,0 +1,5 @@
+
+
+ false
+ true
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f03f2572..dccc8f4a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -21,8 +21,22 @@
#646464
#000000
- #ff64c2f4
- #ff1ca8f4
- #0288D1
+
+ #03A9F4
+ #0288D1
+ #B3E5FC
+ #FFD740
+ #212121
+ #727272
+
+
+ #455A64
+ #90A4AE
+
+
+ #E0E0E0
+
+
+ #607D8B
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 84349ca0..b547465a 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -17,4 +17,61 @@
16dp
16dp
+
+ 48dp
+ 32dp
+
+
+ 96dp
+ 40dp
+
+
+ 32dp
+
+
+ 49dp
+ 8dp
+
+
+ @dimen/abc_list_item_padding_horizontal_material
+
+
+ 4dp
+
+
+ 24dp
+
+
+ 0dp
+ @dimen/abc_list_item_padding_horizontal_material
+ 16dp
+ @dimen/detail_view_padding
+
+ 6dp
+ 360dp
+
+
+ 110dp
+ 40dp
+ 40dp
+ @dimen/widget_today_default_height
+ 220dp
+
+ 250dp
+ 180dp
+ 220dp
+ @dimen/widget_detail_default_height
+
+
+ 24dp
+
+
+ 38dp
+
diff --git a/app/src/main/res/values/refs.xml b/app/src/main/res/values/refs.xml
new file mode 100644
index 00000000..1c8b748f
--- /dev/null
+++ b/app/src/main/res/values/refs.xml
@@ -0,0 +1,19 @@
+
+
+
+ - @layout/fragment_detail
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 138017e9..93ab7dea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -42,9 +42,23 @@
location
+
+ loc-status
+
+
+ loc-latitude
+ loc-longitude
+
94043
+
+ Use my location
+
+
+ Invalid Location (%1$s)"
+ Validating Location... (%1$s)"
+
enable_notifications
Weather Notifications
@@ -73,6 +87,23 @@
imperial
+
+ Icon Pack
+
+
+ Sunshine
+
+
+ Cute dogs
+
+
+ art_pack
+
+
+ https://raw.githubusercontent.com/udacity/Sunshine-Version-2/sunshine_master/app/src/main/res/drawable-xxhdpi/art_%s.png
+
+
+ https://raw.githubusercontent.com/udacity/Sunshine-Version-2/sunshine_master/app/src/main/res/drawable-mdpi/art_%s.png
Today
@@ -87,17 +118,20 @@
%1.0f\u00B0
+ Wind
- Wind: %1$1.0f mph %2$s
+ %1$1.0f mph %2$s
- Wind: %1$1.0f km/h %2$s
+ %1$1.0f km/h %2$s
+ Pressure
- Pressure: %1.0f hPa
+ %1.0f hPa
+ Humidity
- Humidity: %1.0f %%
+ %1.0f %%
sunshine.example.com
@@ -109,4 +143,97 @@
last_notification
-
\ No newline at end of file
+
+ Sunshine Today
+ Sunshine Details
+
+
+ Today\'s weather
+
+
+ No Weather Information Available
+ No weather information available. The network is not available to fetch weather data.
+ No weather information available. The server is not returning data.
+ No weather information available. The server is not returning valid data. Please check for an updated version of Sunshine.
+ No weather information available. The location in settings is not recognized by the weather server.
+
+
+ Forecast: %1$s
+ Forecast icon: %1$s
+ High: %1$s
+ Low: %1$s
+ Humidity: %1$s
+ Barometric Pressure: %1$s
+ Wind speed and direction: %1$s
+
+
+ Storm
+ Drizzle
+ Light Rain
+ Moderate Rain
+ Heavy Rain
+ Intense Rain
+ Extreme Rain
+ Freezing Rain
+ Light Shower
+ Shower
+ Heavy Shower
+ Ragged Shower
+ Light Snow
+ Snow
+ Heavy Snow
+ Sleet
+ Shower Sleet
+ Rain and Snow
+ Rain and Snow
+ Shower Snow
+ Shower Snow
+ Shower Snow
+ Mist
+ Smoke
+ Haze
+ Sand, Dust
+ Fog
+ Sand
+ Dust
+ Volcanic Ash
+ Squalls
+ Tornado
+ Clear
+ Mostly Clear
+ Scattered Clouds
+ Broken Clouds
+ Overcast Clouds
+ Tornado
+ Tropical Storm
+ Hurricane
+ Cold
+ Hot
+ Windy
+ Hail
+ Calm
+ Light Breeze
+ Gentle Breeze
+ Breeze
+ Fresh Breeze
+ Strong Breeze
+ High Wind
+ Gale
+ Severe Gale
+ Storm
+ Violent Storm
+ Hurricane
+
+ Unknown (%1$s)
+
+
+ TN_DetailIcon
+
+
+ Heads up: %1$s in %2$s!
+ // TODO: Get the SenderID from the Developer Console
+
+
+ Powered by Google
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index a1f6c433..097c557a 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -16,24 +16,26 @@
-
-
-
-
-
diff --git a/app/src/main/res/values/widget_dimens.xml b/app/src/main/res/values/widget_dimens.xml
new file mode 100644
index 00000000..848a8024
--- /dev/null
+++ b/app/src/main/res/values/widget_dimens.xml
@@ -0,0 +1,18 @@
+
+
+
+ 8dp
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml
index 7fe52ad0..34d1406a 100644
--- a/app/src/main/res/xml/pref_general.xml
+++ b/app/src/main/res/xml/pref_general.xml
@@ -14,16 +14,19 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+ android:singleLine="true"
+ custom:minLength="3"/>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/widget_info_today.xml b/app/src/main/res/xml/widget_info_today.xml
new file mode 100644
index 00000000..c36d5643
--- /dev/null
+++ b/app/src/main/res/xml/widget_info_today.xml
@@ -0,0 +1,26 @@
+
+
+
diff --git a/build.gradle b/build.gradle
index 6356aabd..cfad1887 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
+ classpath 'com.google.gms:google-services:1.3.0-beta1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files