/* * Copyright 2015 Google Inc. All rights reserved. * * 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.google.samples.apps.iosched.explore; import android.app.LoaderManager; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.graphics.Color; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.support.annotation.ColorInt; import android.support.design.widget.CollapsingToolbarLayout; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.google.samples.apps.iosched.Config; import com.google.samples.apps.iosched.R; import com.google.samples.apps.iosched.model.TagMetadata; import com.google.samples.apps.iosched.provider.ScheduleContract; import com.google.samples.apps.iosched.settings.SettingsUtils; import com.google.samples.apps.iosched.ui.BaseActivity; import com.google.samples.apps.iosched.ui.SearchActivity; import com.google.samples.apps.iosched.util.AnalyticsHelper; import com.google.samples.apps.iosched.util.ImageLoader; import com.google.samples.apps.iosched.util.TagUtils; import com.google.samples.apps.iosched.util.TimeUtils; import com.google.samples.apps.iosched.util.UIUtils; import java.util.ArrayList; import java.util.List; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; import static com.google.samples.apps.iosched.util.LogUtils.LOGE; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; /** * This activity displays all sessions based on the selected filters. * <p/> * It can either be invoked with specific filters or the user can choose the filters to use from the * alt_nav_bar. */ public class ExploreSessionsActivity extends BaseActivity implements Toolbar.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<Cursor> { public static final String EXTRA_FILTER_TAG = "com.google.samples.apps.iosched.explore.EXTRA_FILTER_TAG"; public static final String EXTRA_SHOW_LIVE_STREAM_SESSIONS = "com.google.samples.apps.iosched.explore.EXTRA_SHOW_LIVE_STREAM_SESSIONS"; // The saved instance state filters private static final String STATE_FILTER_TAGS = "com.google.samples.apps.iosched.explore.STATE_FILTER_TAGS"; private static final String STATE_CURRENT_URI = "com.google.samples.apps.iosched.explore.STATE_CURRENT_URI"; private static final String SCREEN_LABEL = "ExploreSessions"; private static final String TAG = makeLogTag(ExploreSessionsActivity.class); private static final int TAG_METADATA_TOKEN = 0x8; private static final int MODE_TIME_FIT = 1; private static final int MODE_EXPLORE = 2; private TagMetadata mTagMetadata; private TagFilterHolder mTagFilterHolder; // Keep track of the current URI. This can diverge from Intent.getData() if the user // dismisses a particular timeslot. At that point, the Activity switches the mode // as well as the Uri used. private Uri mCurrentUri; private ExploreSessionsFragment mFragment; private int mMode; private RecyclerView mFiltersList; private DrawerLayout mDrawerLayout; private CollapsingToolbarLayout mCollapsingToolbar; private ImageView mHeaderImage; private TextView mTitle; private View mTimeSlotLayout; private View mTimeSlotDivider; private ImageLoader mImageLoader; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.explore_sessions_act); // Transition will be postponed until header image is loaded supportPostponeEnterTransition(); mImageLoader = new ImageLoader(this); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mCollapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); mHeaderImage = (ImageView) findViewById(R.id.header_image); mTitle = (TextView) findViewById(R.id.title); mFiltersList = (RecyclerView) findViewById(R.id.filters); mTimeSlotLayout = findViewById(R.id.timeslot_view); mTimeSlotDivider = findViewById(R.id.timeslot_divider); TextView timeSlotTextView = (TextView) findViewById(R.id.timeslot); ImageButton dismissTimeSlotButton = (ImageButton) findViewById(R.id.close_timeslot); mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow_flipped, GravityCompat.END); mFragment = (ExploreSessionsFragment) getFragmentManager() .findFragmentById(R.id.explore_sessions_frag); if (savedInstanceState != null) { mTagFilterHolder = savedInstanceState.getParcelable(STATE_FILTER_TAGS); mCurrentUri = savedInstanceState.getParcelable(STATE_CURRENT_URI); } else if (getIntent() != null) { mCurrentUri = getIntent().getData(); } // Build the tag URI long[] interval = ScheduleContract.Sessions.getInterval(mCurrentUri); if (interval != null) { mMode = MODE_TIME_FIT; String title = getString(R.string.explore_sessions_time_slot_title, TimeUtils.getDayName(this, UIUtils.startTimeToDayIndex(interval[0])), UIUtils.formatTime(interval[0], this)); mTitle.setText(title); mHeaderImage.setScaleType(ImageView.ScaleType.FIT_CENTER); mHeaderImage.setImageResource(R.drawable.ic_hash_io_16_monochrome); mTimeSlotLayout.setVisibility(View.VISIBLE); mTimeSlotDivider.setVisibility(View.VISIBLE); timeSlotTextView.setText(title); dismissTimeSlotButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mTimeSlotLayout.setVisibility(View.GONE); mTimeSlotDivider.setVisibility(View.GONE); mMode = MODE_EXPLORE; mCurrentUri = null; reloadFragment(); } }); } else { mMode = MODE_EXPLORE; } // Add the back button to the toolbar. setToolbarAsUp(new View.OnClickListener() { @Override public void onClick(View view) { navigateUpOrBack(ExploreSessionsActivity.this, null); } }); getSupportActionBar().setDisplayShowTitleEnabled(false); // Start loading the tag metadata. This will in turn call the fragment with the // correct arguments. getLoaderManager().initLoader(TAG_METADATA_TOKEN, null, this); // ANALYTICS SCREEN: View the Explore Sessions screen // Contains: Nothing (Page name is a constant) AnalyticsHelper.sendScreenView(SCREEN_LABEL); } @Override protected void onPostCreate(final Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // This screen sets a status bar color at runtime. The base activity may overwrite this on // configuration changes so set the header again to ensure the correct result. setHeader(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); // Add the filter & search buttons to the toolbar. Toolbar toolbar = getToolbar(); toolbar.inflateMenu(R.menu.explore_sessions_filtered); toolbar.setOnMenuItemClickListener(this); return true; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(STATE_FILTER_TAGS, mTagFilterHolder); outState.putParcelable(STATE_CURRENT_URI, mCurrentUri); } @Override public boolean onMenuItemClick(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_filter: mDrawerLayout.openDrawer(GravityCompat.END); return true; case R.id.menu_search: // ANALYTICS EVENT: Click the search button on the ExploreSessions screen // Contains: No data (Just that a search occurred, no search term) AnalyticsHelper.sendEvent(SCREEN_LABEL, "launchsearch", ""); startActivity(new Intent(this, SearchActivity.class)); return true; } return false; } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { if (id == TAG_METADATA_TOKEN) { return TagMetadata.createCursorLoader(this); } return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { switch (loader.getId()) { case TAG_METADATA_TOKEN: mTagMetadata = new TagMetadata(cursor); onTagMetadataLoaded(); break; default: cursor.close(); } } @Override public void onBackPressed() { if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.END)) { mDrawerLayout.closeDrawer(GravityCompat.END); } else { super.onBackPressed(); } } private void onTagMetadataLoaded() { if (mTagFilterHolder == null) { // Use the Intent Extras to set up the TagFilterHolder mTagFilterHolder = new TagFilterHolder(); String tag = getIntent().getStringExtra(EXTRA_FILTER_TAG); TagMetadata.Tag userTag = mTagMetadata.getTag(tag); String userTagCategory = userTag == null ? null : userTag.getCategory(); if (tag != null && userTagCategory != null) { mTagFilterHolder.add(tag, userTagCategory); } mTagFilterHolder.setShowLiveStreamedSessions( getIntent().getBooleanExtra(EXTRA_SHOW_LIVE_STREAM_SESSIONS, false)); // update the selected filters using the following logic: // a) For onsite attendees, we should default to showing all 'types' // (i.e. Sessions, code labs, sandbox, misc). if (SettingsUtils.isAttendeeAtVenue(this)) { List<TagMetadata.Tag> tags = mTagMetadata.getTagsInCategory(Config.Tags.CATEGORY_TYPE); // Here we only add all 'types' if the user has not explicitly selected // one of the category_type tags. if (tags != null && !TextUtils.equals(userTagCategory, Config.Tags.CATEGORY_TYPE)) { for (TagMetadata.Tag theTag : tags) { mTagFilterHolder.add(theTag.getId(), theTag.getCategory()); } } } else { // b) For remote users, default to only showing Sessions that are Live streamed. TagMetadata.Tag theTag = mTagMetadata.getTag(Config.Tags.SESSIONS); if (!TextUtils.equals(theTag.getCategory(), userTagCategory)) { mTagFilterHolder.add(theTag.getId(), theTag.getCategory()); } mTagFilterHolder.setShowLiveStreamedSessions(true); } } reloadFragment(); mFiltersList.setAdapter(new FilterAdapter(mTagMetadata)); } /** * Set the activity title & header image. * <p/> * If the user is browsing a single track then it's name is shown as the title and a relevant * header image; otherwise a generic title and image is shown. */ private void setHeader() { if (mMode == MODE_EXPLORE && mTagMetadata != null) { String title = null; String headerImage = null; @ColorInt int trackColor = Color.TRANSPARENT; // If exactly one theme or one track is selected, show its title and image. int selectedTracks = mTagFilterHolder.getCountByCategory(Config.Tags.CATEGORY_TRACK); int selectedThemes = mTagFilterHolder.getCountByCategory(Config.Tags.CATEGORY_THEME); if (selectedThemes + selectedTracks == 1) { for (String tagId : mTagFilterHolder.getSelectedFilters()) { if (TagUtils.isTrackTag(tagId) || TagUtils.isThemeTag(tagId)) { TagMetadata.Tag selectedTag = mTagMetadata.getTag(tagId); title = selectedTag.getName(); headerImage = selectedTag.getPhotoUrl(); trackColor = selectedTag.getColor(); break; } } } if (title == null) { title = getString(R.string.title_explore); } mTitle.setText(title); if (headerImage != null) { mHeaderImage.setScaleType(ImageView.ScaleType.CENTER_CROP); mImageLoader.loadImage(headerImage, mHeaderImage, new RequestListener<String, Bitmap>() { @Override public boolean onException(final Exception e, final String model, final Target<Bitmap> target, final boolean isFirstResource) { supportStartPostponedEnterTransition(); return false; } @Override public boolean onResourceReady(final Bitmap resource, final String model, final Target<Bitmap> target, final boolean isFromMemoryCache, final boolean isFirstResource) { target.onResourceReady(resource, null); supportStartPostponedEnterTransition(); return true; } }); } else { mHeaderImage.setScaleType(ImageView.ScaleType.FIT_CENTER); mHeaderImage.setImageResource(R.drawable.ic_hash_io_16_monochrome); supportStartPostponedEnterTransition(); } final int statusBarColor = trackColor != Color.TRANSPARENT ? UIUtils.adjustColorForStatusBar(trackColor) : UIUtils.getThemeColor(this, R.attr.colorPrimaryDark, R.color.theme_primary_dark); mCollapsingToolbar.setContentScrimColor(trackColor); mDrawerLayout.setStatusBarBackgroundColor(statusBarColor); } } private void reloadFragment() { // Build the tag URI Uri uri = mCurrentUri; if (uri == null) { uri = ScheduleContract.Sessions.buildCategoryTagFilterUri( ScheduleContract.Sessions.CONTENT_URI, mTagFilterHolder.toStringArray(), mTagFilterHolder.getCategoryCount()); } else { // build a uri with the specific filters uri = ScheduleContract.Sessions.buildCategoryTagFilterUri(uri, mTagFilterHolder.toStringArray(), mTagFilterHolder.getCategoryCount()); } setHeader(); Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.putExtra(ExploreSessionsFragment.EXTRA_SHOW_LIVESTREAMED_SESSIONS, mTagFilterHolder.isShowLiveStreamedSessions()); LOGD(TAG, "Reloading fragment with categories " + mTagFilterHolder.getCategoryCount() + " uri: " + uri + " showLiveStreamedEvents: " + mTagFilterHolder.isShowLiveStreamedSessions()); mFragment.reloadFromArguments(intentToFragmentArguments(intent)); } private void updateFilters(final TagMetadata.Tag filter, final boolean enable) { if (enable) { mTagFilterHolder.add(filter.getId(), filter.getCategory()); } else { mTagFilterHolder.remove(filter.getId(), filter.getCategory()); } reloadFragment(); } @Override public void onLoaderReset(Loader<Cursor> loader) { } /** * Adapter for the list of filters that can be applied to this screen i.e. Theme, Session Type, * Live Streamed and Track. */ private class FilterAdapter extends RecyclerView.Adapter { private static final int TYPE_FILTER = 0; private static final int TYPE_LIVE_STREAM_FILTER = 1; private static final int TYPE_DIVIDER = 2; private final List mItems; private final LayoutInflater mInflater; FilterAdapter(TagMetadata filters) { mInflater = LayoutInflater.from(ExploreSessionsActivity.this); mItems = new ArrayList(); processFilters(filters); } @Override public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { switch (viewType) { case TYPE_FILTER: return createFilterViewHolder(parent); case TYPE_LIVE_STREAM_FILTER: return createLiveStreamFilterViewHolder(parent); case TYPE_DIVIDER: return createDividerViewHolder(parent); default: LOGE(TAG, "Unknown view type"); throw new IllegalArgumentException("Unknown view type"); } } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { switch (getItemViewType(position)) { case TYPE_FILTER: bindFilter((FilterViewHolder) holder, (TagMetadata.Tag) mItems.get(position)); break; case TYPE_LIVE_STREAM_FILTER: bindLiveStreamFilter((FilterViewHolder) holder); break; } } @Override public int getItemCount() { return mItems.size(); } @Override public int getItemViewType(final int position) { Object item = mItems.get(position); if (item instanceof LiveStream) { return TYPE_LIVE_STREAM_FILTER; } else if (item instanceof Divider) { return TYPE_DIVIDER; } return TYPE_FILTER; } /** * We transform the provided data into a structure suitable for the RecyclerView i.e. we * build up {@link #mItems}, adding 'marker' items for dividers & live stream. */ private void processFilters(TagMetadata tagMetadata) { List<TagMetadata.Tag> themes = tagMetadata.getTagsInCategory(Config.Tags.CATEGORY_THEME); if (themes != null && !themes.isEmpty()) { for (TagMetadata.Tag theme : themes) { mItems.add(theme); } } mItems.add(new Divider()); List<TagMetadata.Tag> sessionTypes = tagMetadata.getTagsInCategory(Config.Tags.CATEGORY_TYPE); if (sessionTypes != null && !sessionTypes.isEmpty()) { for (TagMetadata.Tag type : sessionTypes) { mItems.add(type); } } mItems.add(new Divider()); mItems.add(new LiveStream()); mItems.add(new Divider()); List<TagMetadata.Tag> topics = tagMetadata.getTagsInCategory(Config.Tags.CATEGORY_TRACK); if (topics != null && !topics.isEmpty()) { for (TagMetadata.Tag topic : topics) { mItems.add(topic); } } } private FilterViewHolder createFilterViewHolder(final ViewGroup parent) { final FilterViewHolder holder = new FilterViewHolder(mInflater.inflate( R.layout.explore_sessions_list_item_alt_drawer, parent, false)); holder.checkbox.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { final TagMetadata.Tag filter = (TagMetadata.Tag) mItems.get(holder.getAdapterPosition()); updateFilters(filter, holder.checkbox.isChecked()); } }); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { holder.checkbox.performClick(); } }); return holder; } private FilterViewHolder createLiveStreamFilterViewHolder(final ViewGroup parent) { final FilterViewHolder holder = new FilterViewHolder(mInflater.inflate( R.layout.explore_sessions_list_item_livestream_alt_drawer, parent, false)); holder.checkbox.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { mTagFilterHolder.setShowLiveStreamedSessions(holder.checkbox.isChecked()); reloadFragment(); } }); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { holder.checkbox.performClick(); } }); return holder; } private DividerViewHolder createDividerViewHolder(final ViewGroup parent) { // TODO we should use an ItemDecoration rather than a view return new DividerViewHolder(mInflater.inflate( R.layout.explore_sessions_list_item_alt_header, parent, false)); } private void bindFilter(final FilterViewHolder holder, final TagMetadata.Tag filter) { holder.label.setText(filter.getName()); holder.checkbox.setChecked(mTagFilterHolder.contains(filter.getId())); } private void bindLiveStreamFilter(final FilterViewHolder holder) { holder.checkbox.setChecked(mTagFilterHolder.isShowLiveStreamedSessions()); } } private static class Divider {} private static class LiveStream {} private static class FilterViewHolder extends RecyclerView.ViewHolder { final TextView label; final CheckBox checkbox; public FilterViewHolder(final View itemView) { super(itemView); label = (TextView) itemView.findViewById(R.id.text_view); checkbox = (CheckBox) itemView.findViewById(R.id.filter_checkbox); } } private static class DividerViewHolder extends RecyclerView.ViewHolder { public DividerViewHolder(final View itemView) { super(itemView); } } }