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