/* * 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.session; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CollapsingToolbarLayout; import android.support.graphics.drawable.AnimatedVectorDrawableCompat; import android.support.v4.app.ShareCompat; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.animation.LinearOutSlowInInterpolator; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.transition.Transition; 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.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import com.google.android.gms.appindexing.Action; import com.google.android.gms.appindexing.AppIndex; import com.google.android.gms.appindexing.Thing; import com.google.android.gms.common.api.GoogleApiClient; import com.google.samples.apps.iosched.BuildConfig; import com.google.samples.apps.iosched.Config; import com.google.samples.apps.iosched.R; import com.google.samples.apps.iosched.archframework.PresenterImpl; import com.google.samples.apps.iosched.archframework.UpdatableView; import com.google.samples.apps.iosched.archframework.UserActionEnum; import com.google.samples.apps.iosched.explore.ExploreSessionsActivity; import com.google.samples.apps.iosched.injection.ModelProvider; import com.google.samples.apps.iosched.map.MapActivity; import com.google.samples.apps.iosched.model.TagMetadata; import com.google.samples.apps.iosched.session.SessionDetailModel.SessionDetailQueryEnum; import com.google.samples.apps.iosched.session.SessionDetailModel.SessionDetailUserActionEnum; import com.google.samples.apps.iosched.ui.widget.CheckableFloatingActionButton; import com.google.samples.apps.iosched.ui.widget.MessageCardView; import com.google.samples.apps.iosched.util.AccountUtils; import com.google.samples.apps.iosched.util.AnalyticsHelper; import com.google.samples.apps.iosched.util.ImageLoader; import com.google.samples.apps.iosched.util.LogUtils; import com.google.samples.apps.iosched.util.SessionsHelper; import com.google.samples.apps.iosched.util.TimeUtils; import com.google.samples.apps.iosched.util.UIUtils; import com.google.samples.apps.iosched.util.YouTubeUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; /** * Displays the details about a session. The user can add/remove a session from the schedule, watch * a live stream if available, watch the session on YouTube, view the map, share the session, and * submit feedback. */ public class SessionDetailFragment extends Fragment implements UpdatableView<SessionDetailModel, SessionDetailQueryEnum, SessionDetailUserActionEnum> { private static final String TAG = LogUtils.makeLogTag(SessionDetailFragment.class); /** * Stores the session IDs for which the user has dismissed the "give feedback" card. This * information is kept for the duration of the app's execution so that if they say "No, thanks", * we don't show the card again for that session while the app is still executing. */ private static HashSet<String> sDismissedFeedbackCard = new HashSet<>(); private CheckableFloatingActionButton mAddScheduleFab; private AppBarLayout mAppBar; private CollapsingToolbarLayout mCollapsingToolbar; private Toolbar mToolbar; private TextView mTitle; private TextView mSubtitle; private TextView mAbstract; private TextView mLiveStreamedIndicator; private Button mWatchVideo; private LinearLayout mTags; private TextView mExtended; private ViewGroup mTagsContainer; private TextView mRequirements; private View mHeaderBox; private View mPhotoViewContainer; private ImageView mPhotoView; private ImageLoader mImageLoader; private Runnable mTimeHintUpdaterRunnable = null; private List<Runnable> mDeferredUiOperations = new ArrayList<>(); private Handler mHandler; private boolean mAnalyticsScreenViewHasFired; private UserActionListener mListener; private boolean mShowFab = false; private boolean mHasEnterTransition = false; private String mExtendedSessionUrl = null; private GoogleApiClient mClient; @Override public void addListener(UserActionListener listener) { mListener = listener; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); mAnalyticsScreenViewHasFired = false; mClient = new GoogleApiClient.Builder(getActivity()) .addApi(AppIndex.API) .enableAutoManage((SessionDetailActivity) getActivity(), null) .build(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.session_detail_frag, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mHandler = new Handler(); initPresenter(); initViews(); initViewListeners(); } @Override public void onAttach(final Activity activity) { super.onAttach(activity); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { final Transition sharedElementEnterTransition = activity.getWindow().getSharedElementEnterTransition(); if (sharedElementEnterTransition != null) { mHasEnterTransition = true; sharedElementEnterTransition.addListener(new UIUtils.TransitionListenerAdapter() { @Override public void onTransitionStart(final Transition transition) { enterTransitionStarted(); } @Override public void onTransitionEnd(final Transition transition) { enterTransitionFinished(); } }); } final Transition sharedElementReturnTransition = activity.getWindow().getSharedElementReturnTransition(); if (sharedElementReturnTransition != null) { sharedElementReturnTransition.addListener(new UIUtils.TransitionListenerAdapter() { @Override public void onTransitionStart(final Transition transition) { returnTransitionStarted(); } }); } } } @Override public void onResume() { super.onResume(); if (mTimeHintUpdaterRunnable != null) { mHandler.postDelayed(mTimeHintUpdaterRunnable, SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL); } } @Override public void onPause() { super.onPause(); mHandler.removeCallbacksAndMessages(null); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.session_detail, menu); if (!BuildConfig.ENABLE_MAP_IN_NAVIGATION) { MenuItem map = menu.findItem(R.id.menu_map_room); map.setVisible(false); } tryExecuteDeferredUiOperations(); } public void fireExtendedSessionIntent() { if (mExtendedSessionUrl != null) { Intent extendedSessionIntent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse( mExtendedSessionUrl)); startActivity(extendedSessionIntent); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_map_room: sendUserAction(SessionDetailUserActionEnum.SHOW_MAP, null); return true; case R.id.menu_share: sendUserAction(SessionDetailUserActionEnum.SHOW_SHARE, null); return true; } return false; } private void sendUserAction(UserActionEnum action, Bundle args) { mListener.onUserAction(action, args); } private void initPresenter() { SessionDetailModel model = ModelProvider.provideSessionDetailModel( ((SessionDetailActivity) getActivity()).getSessionUri(), getContext(), new SessionsHelper(getActivity()), getLoaderManager()); PresenterImpl presenter = new PresenterImpl(model, this, SessionDetailUserActionEnum.values(), SessionDetailQueryEnum.values()); presenter.loadInitialQueries(); } private void initViews() { final ViewGroup root = (ViewGroup) getActivity().findViewById(R.id.session_detail_frag); mAppBar = (AppBarLayout) root.findViewById(R.id.appbar); mCollapsingToolbar = (CollapsingToolbarLayout) mAppBar.findViewById(R.id.collapsing_toolbar); mHeaderBox = mAppBar.findViewById(R.id.header_session); mToolbar = (Toolbar) mHeaderBox.findViewById(R.id.toolbar); mTitle = (TextView) mHeaderBox.findViewById(R.id.session_title); mSubtitle = (TextView) mHeaderBox.findViewById(R.id.session_subtitle); mPhotoViewContainer = mCollapsingToolbar.findViewById(R.id.session_photo_container); mPhotoView = (ImageView) mPhotoViewContainer.findViewById(R.id.session_photo); mWatchVideo = (Button) mCollapsingToolbar.findViewById(R.id.watch); final ViewGroup details = (ViewGroup) root.findViewById(R.id.details_container); mAbstract = (TextView) details.findViewById(R.id.session_abstract); mLiveStreamedIndicator = (TextView) details.findViewById(R.id.live_streamed_indicator); mRequirements = (TextView) details.findViewById(R.id.session_requirements); mTags = (LinearLayout) details.findViewById(R.id.session_tags); mExtended = (TextView) details.findViewById(R.id.extended_session_button); mTagsContainer = (ViewGroup) details.findViewById(R.id.session_tags_container); mAddScheduleFab = (CheckableFloatingActionButton) root.findViewById(R.id.add_schedule_button); mImageLoader = new ImageLoader(getContext()); } @Override public void displayData(SessionDetailModel data, SessionDetailQueryEnum query) { switch (query) { case SESSIONS: displaySessionData(data); displayTrackColor(data); break; case FEEDBACK: displayFeedbackData(data); break; case SPEAKERS: displaySpeakersData(data); break; case TAG_METADATA: displayTags(data); displayTrackColor(data); break; default: break; } } private void initViewListeners() { mAddScheduleFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { boolean isInSchedule = !((CheckableFloatingActionButton) view).isChecked(); showInSchedule(isInSchedule); if (isInSchedule) { sendUserAction(SessionDetailUserActionEnum.STAR, null); } else { sendUserAction(SessionDetailUserActionEnum.UNSTAR, null); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mAddScheduleFab.announceForAccessibility(isInSchedule ? getString(R.string.session_details_a11y_session_added) : getString(R.string.session_details_a11y_session_removed)); } } }); } private void showInSchedule(boolean isInSchedule) { mAddScheduleFab.setChecked(isInSchedule); if (isInSchedule) { AnimatedVectorDrawableCompat addToSchedule = AnimatedVectorDrawableCompat .create(getContext(), R.drawable.avd_add_to_schedule); mAddScheduleFab.setImageDrawable(addToSchedule); addToSchedule.start(); } else { AnimatedVectorDrawableCompat removeFromSchedule = AnimatedVectorDrawableCompat .create(getContext(), R.drawable.avd_remove_from_schedule); mAddScheduleFab.setImageDrawable(removeFromSchedule); removeFromSchedule.start(); } mAddScheduleFab.setContentDescription(getString(isInSchedule ? R.string.remove_from_schedule : R.string.add_to_schedule)); } @Override public void displayErrorMessage(SessionDetailQueryEnum query) { // Not showing any error } @Override public void displayUserActionResult(SessionDetailModel data, SessionDetailUserActionEnum userAction, boolean success) { switch (userAction) { case SHOW_MAP: Intent intentShowMap = new Intent(getActivity().getApplicationContext(), MapActivity.class); intentShowMap.putExtra(MapActivity.EXTRA_ROOM, data.getRoomId()); intentShowMap.putExtra(MapActivity.EXTRA_DETACHED_MODE, true); getActivity().startActivity(intentShowMap); break; case SHOW_SHARE: ShareCompat.IntentBuilder builder = ShareCompat.IntentBuilder.from(getActivity()).setType( "text/plain").setText(getActivity() .getString(R.string.share_template, data.getSessionTitle(), BuildConfig.CONFERENCE_HASHTAG, data.getSessionUrl())); Intent intentShare = Intent.createChooser( builder.getIntent(), getString(R.string.title_share)); intentShare.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getActivity().startActivity(intentShare); break; case EXTENDED: fireExtendedSessionIntent(); break; default: // Other user actions are completely handled in model break; } } @Override public Uri getDataUri(SessionDetailQueryEnum query) { switch (query) { case SESSIONS: return ((SessionDetailActivity) getActivity()).getSessionUri(); default: return null; } } @Override public Context getContext() { return getActivity(); } private void displaySessionData(final SessionDetailModel data) { mTitle.setText(data.getSessionTitle()); mSubtitle.setText(data.getSessionSubtitle()); try { AppIndex.AppIndexApi.start(mClient, getActionForTitle(data.getSessionTitle())); } catch (Throwable e) { // Nothing to do if indexing fails. } if (data.shouldShowHeaderImage()) { mImageLoader.loadImage(data.getPhotoUrl(), mPhotoView); } else { mPhotoViewContainer.setVisibility(View.GONE); ViewCompat.setFitsSystemWindows(mAppBar, false); // This is hacky but the collapsing toolbar requires a minimum height to enable // the status bar scrim feature; set 1px. When there is no image, this would leave // a 1px gap so we offset with a negative margin. ((ViewGroup.MarginLayoutParams) mCollapsingToolbar.getLayoutParams()).topMargin = -1; } tryExecuteDeferredUiOperations(); // Handle Keynote as a special case, where the user cannot remove it // from the schedule (it is auto added to schedule on sync) mShowFab = (AccountUtils.hasActiveAccount(getContext()) && !data.isKeynote()); mAddScheduleFab.setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE); displayTags(data); if (!data.isKeynote()) { showInScheduleDeferred(data.isInSchedule()); } if (!TextUtils.isEmpty(data.getSessionAbstract())) { UIUtils.setTextMaybeHtml(mAbstract, data.getSessionAbstract()); mAbstract.setVisibility(View.VISIBLE); } else { mAbstract.setVisibility(View.GONE); } // Build requirements section final View requirementsBlock = getActivity().findViewById(R.id.session_requirements_block); final String sessionRequirements = data.getRequirements(); if (!TextUtils.isEmpty(sessionRequirements)) { UIUtils.setTextMaybeHtml(mRequirements, sessionRequirements); requirementsBlock.setVisibility(View.VISIBLE); } else { requirementsBlock.setVisibility(View.GONE); } final ViewGroup relatedVideosBlock = (ViewGroup) getActivity().findViewById( R.id.related_videos_block); relatedVideosBlock.setVisibility(View.GONE); updateEmptyView(data); updateTimeBasedUi(data); if (data.getLiveStreamVideoWatched()) { mPhotoView.setColorFilter(getContext().getResources().getColor( R.color.played_video_tint)); mWatchVideo.setText(getString(R.string.session_replay)); } if (data.hasLiveStream()) { mWatchVideo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String videoId = YouTubeUtils.getVideoIdFromSessionData(data.getYouTubeUrl(), data.getLiveStreamId()); YouTubeUtils.showYouTubeVideo(videoId, getActivity()); } }); } fireAnalyticsScreenView(data.getSessionTitle()); mTimeHintUpdaterRunnable = new Runnable() { @Override public void run() { if (getActivity() == null) { // Do not post a delayed message if the activity is detached. return; } updateTimeBasedUi(data); mHandler.postDelayed(mTimeHintUpdaterRunnable, SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL); } }; mHandler.postDelayed(mTimeHintUpdaterRunnable, SessionDetailConstants.TIME_HINT_UPDATE_INTERVAL); if (!mHasEnterTransition) { // No enter transition so update UI manually enterTransitionFinished(); } if (BuildConfig.ENABLE_EXTENDED_SESSION_URL && data.shouldShowExtendedSessionLink()) { mExtendedSessionUrl = data.getExtendedSessionUrl(); if (!TextUtils.isEmpty(mExtendedSessionUrl)) { mExtended.setText(R.string.description_extended); mExtended.setVisibility(View.VISIBLE); mExtended.setClickable(true); mExtended.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { sendUserAction(SessionDetailUserActionEnum.EXTENDED, null); } }); } } } /** * Update the header box background color & status bar color depending upon which track this * session belongs to. * <p> * Note this requires both the {@link SessionDetailQueryEnum#SESSIONS} & * {@link SessionDetailQueryEnum#TAG_METADATA) queries to have returned. */ private void displayTrackColor(SessionDetailModel data) { if (data.isSessionTrackColorAvailable()) { int trackColor = data.getSessionTrackColor(); if (trackColor == Color.TRANSPARENT) { trackColor = UIUtils.getThemeColor(getContext(), R.attr.colorPrimary, R.color.theme_primary); } final Drawable background = mHeaderBox.getBackground(); if (background instanceof ColorDrawable && ((ColorDrawable) background).getColor() == trackColor) { return; } // Animate the color change to make the transition smoother final ObjectAnimator color = ObjectAnimator.ofInt(mHeaderBox, UIUtils.BACKGROUND_COLOR, trackColor); color.setEvaluator(new ArgbEvaluator()); if (mHasEnterTransition) { color.setStartDelay(200L); } color.setDuration(300L); color.start(); if (mCollapsingToolbar.getFitsSystemWindows() && mPhotoViewContainer.getVisibility() == View.VISIBLE) { // immersive+photo mCollapsingToolbar.setStatusBarScrimColor(trackColor); } else { UIUtils.adjustAndSetStatusBarColor(getActivity(), trackColor); } } } private void enterTransitionStarted() { mAddScheduleFab.setVisibility(View.INVISIBLE); mToolbar.setAlpha(0f); } /** * Finish any UI setup that should be deferred until the enter transition has completed. */ private void enterTransitionFinished() { if (mShowFab) { mAddScheduleFab.show(); } if (mToolbar.getAlpha() != 1f) { mToolbar.animate() .alpha(1f) .setDuration(200L) .setInterpolator(new LinearOutSlowInInterpolator()) .start(); } } private void returnTransitionStarted() { // Fade the header bar for a smoother transition. final ObjectAnimator color = ObjectAnimator.ofInt(mHeaderBox, UIUtils.BACKGROUND_COLOR, ContextCompat.getColor(getContext(), R.color.background)); color.setEvaluator(new ArgbEvaluator()); color.setDuration(200L); color.start(); // Also fade out the toolbar and FAB mToolbar.animate() .alpha(0f) .setDuration(200L) .start(); mAddScheduleFab.hide(); } /** * Sends a screen view to Google Analytics, if a screenview hasn't already been sent since the * fragment was loaded. This prevents background syncs from causing superflous screen views. * * @param sessionTitle The name of the session being tracked. */ private void fireAnalyticsScreenView(String sessionTitle) { if (!mAnalyticsScreenViewHasFired) { // ANALYTICS SCREEN: View the Session Details page for a specific session. // Contains: The session title. AnalyticsHelper.sendScreenView("Session: " + sessionTitle); mAnalyticsScreenViewHasFired = true; } } private void displayFeedbackData(SessionDetailModel data) { if (data.hasFeedback()) { final MessageCardView giveFeedbackCardView = (MessageCardView) getActivity().findViewById(R.id.give_feedback_card); if (giveFeedbackCardView != null) { giveFeedbackCardView.setVisibility(View.GONE); } } LOGD(TAG, "User " + (data.hasFeedback() ? "already gave" : "has not given") + " feedback for session."); } private void displaySpeakersData(SessionDetailModel data) { final ViewGroup speakersGroup = (ViewGroup) getActivity() .findViewById(R.id.session_speakers_block); // Remove all existing speakers (everything but first child, which is the header) for (int i = speakersGroup.getChildCount() - 1; i >= 1; i--) { speakersGroup.removeViewAt(i); } final LayoutInflater inflater = getActivity().getLayoutInflater(); boolean hasSpeakers = false; List<SessionDetailModel.Speaker> speakers = data.getSpeakers(); for (final SessionDetailModel.Speaker speaker : speakers) { String speakerHeader = speaker.getName(); if (!TextUtils.isEmpty(speaker.getCompany())) { speakerHeader += ", " + speaker.getCompany(); } final View speakerView = inflater .inflate(R.layout.speaker_detail, speakersGroup, false); final TextView speakerHeaderView = (TextView) speakerView .findViewById(R.id.speaker_header); final ImageView speakerImageView = (ImageView) speakerView .findViewById(R.id.speaker_image); final TextView speakerAbstractView = (TextView) speakerView .findViewById(R.id.speaker_abstract); final ImageView plusOneIcon = (ImageView) speakerView.findViewById(R.id.gplus_icon_box); final ImageView twitterIcon = (ImageView) speakerView.findViewById( R.id.twitter_icon_box); setUpSpeakerSocialIcon(speaker, twitterIcon, speaker.getTwitterUrl(), UIUtils.TWITTER_COMMON_NAME, UIUtils.TWITTER_PACKAGE_NAME); setUpSpeakerSocialIcon(speaker, plusOneIcon, speaker.getPlusoneUrl(), UIUtils.GOOGLE_PLUS_COMMON_NAME, UIUtils.GOOGLE_PLUS_PACKAGE_NAME); // A speaker may have both a Twitter and GPlus page, only a Twitter page or only a // GPlus page, or neither. By default, align the Twitter icon to the right and the GPlus // icon to its left. If only a single icon is displayed, align it to the right. determineSocialIconPlacement(plusOneIcon, twitterIcon); if (!TextUtils.isEmpty(speaker.getImageUrl()) && mImageLoader != null) { mImageLoader.loadImage(speaker.getImageUrl(), speakerImageView); } speakerHeaderView.setText(speakerHeader); speakerImageView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); UIUtils.setTextMaybeHtml(speakerAbstractView, speaker.getAbstract()); if (!TextUtils.isEmpty(speaker.getUrl())) { speakerImageView.setEnabled(true); speakerImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent speakerProfileIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(speaker.getUrl())); speakerProfileIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); UIUtils.preferPackageForIntent(getActivity(), speakerProfileIntent, UIUtils.GOOGLE_PLUS_PACKAGE_NAME); getActivity().startActivity(speakerProfileIntent); } }); } else { speakerImageView.setEnabled(false); speakerImageView.setOnClickListener(null); } speakersGroup.addView(speakerView); hasSpeakers = true; } speakersGroup.setVisibility(hasSpeakers ? View.VISIBLE : View.GONE); updateEmptyView(data); } /** * Determines visibility of a social icon, sets up a click listener to allow the user to * navigate to the social network associated with the icon, and sets up a content description * for the icon. */ private void setUpSpeakerSocialIcon(final SessionDetailModel.Speaker speaker, ImageView socialIcon, final String socialUrl, String socialNetworkName, final String packageName) { if (socialUrl == null || socialUrl.isEmpty()) { socialIcon.setVisibility(View.GONE); } else { socialIcon.setContentDescription(getString( R.string.speaker_social_page, socialNetworkName, speaker.getName()) ); socialIcon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { UIUtils.fireSocialIntent( getActivity(), Uri.parse(socialUrl), packageName ); } }); } } /** * Aligns the Twitter icon the parent bottom right. Aligns the G+ icon to the left of the * Twitter icon if it is present. Otherwise, aligns the G+ icon to the parent bottom right. */ private void determineSocialIconPlacement(ImageView plusOneIcon, ImageView twitterIcon) { if (plusOneIcon.getVisibility() == View.VISIBLE) { // Set the dimensions of the G+ button. int socialIconDimension = getResources().getDimensionPixelSize( R.dimen.social_icon_box_size); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( socialIconDimension, socialIconDimension); params.addRule(RelativeLayout.BELOW, R.id.speaker_abstract); params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); if (twitterIcon.getVisibility() == View.VISIBLE) { params.addRule(RelativeLayout.LEFT_OF, R.id.twitter_icon_box); } else { params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); } plusOneIcon.setLayoutParams(params); } } private void updateEmptyView(SessionDetailModel data) { getActivity().findViewById(android.R.id.empty).setVisibility( (data.getSessionTitle() != null && data.getSpeakers().size() == 0 && !data.hasSummaryContent()) ? View.VISIBLE : View.GONE); } private void updateTimeBasedUi(SessionDetailModel data) { // Show "Live streamed" label for all live-streamed sessions unless it has ended mLiveStreamedIndicator.setVisibility( (data.hasLiveStream() && !data.hasSessionEnded()) ? View.VISIBLE : View.GONE); if (data.showLiveStream()) { // Show the play button and text only once the session is about to start. mWatchVideo.setVisibility(View.VISIBLE); if (data.hasSessionEnded()) { mWatchVideo.setText(getString(R.string.session_watch)); // TODO: implement Replay. } else { mWatchVideo.setText(getString(R.string.session_watch_live)); } } else { mWatchVideo.setVisibility(View.GONE); } // If the session is done, hide the FAB, and show the "Give feedback" card. if (data.isSessionReadyForFeedback()) { mShowFab = false; mAddScheduleFab.setVisibility(View.GONE); if (!data.hasFeedback() && data.isInScheduleWhenSessionFirstLoaded() && !sDismissedFeedbackCard.contains(data.getSessionId())) { showGiveFeedbackCard(data); } } String timeHint = ""; if (TimeUtils.hasConferenceEnded(getContext())) { // No time hint to display. timeHint = ""; } else if (data.hasSessionEnded()) { timeHint = getString(R.string.time_hint_session_ended); } else if (data.isSessionOngoing()) { long minutesAgo = data.minutesSinceSessionStarted(); if (minutesAgo > 1) { timeHint = getString(R.string.time_hint_started_min, minutesAgo); } else { timeHint = getString(R.string.time_hint_started_just); } } else { long minutesUntilStart = data.minutesUntilSessionStarts(); if (minutesUntilStart > 0 && minutesUntilStart <= SessionDetailConstants.HINT_TIME_BEFORE_SESSION_MIN) { if (minutesUntilStart > 1) { timeHint = getString(R.string.time_hint_about_to_start_min, minutesUntilStart); } else { timeHint = getString(R.string.time_hint_about_to_start_shortly, minutesUntilStart); } } } final TextView timeHintView = (TextView) getActivity().findViewById(R.id.time_hint); if (!TextUtils.isEmpty(timeHint)) { timeHintView.setVisibility(View.VISIBLE); timeHintView.setText(timeHint); } else { timeHintView.setVisibility(View.GONE); } } private void displayTags(SessionDetailModel data) { if (data.getTagMetadata() == null || data.getTagsString() == null) { mTagsContainer.setVisibility(View.GONE); return; } if (TextUtils.isEmpty(data.getTagsString())) { mTagsContainer.setVisibility(View.GONE); } else { mTagsContainer.setVisibility(View.VISIBLE); mTags.removeAllViews(); LayoutInflater inflater = LayoutInflater.from(getContext()); String[] tagIds = data.getTagsString().split(","); List<TagMetadata.Tag> tags = new ArrayList<TagMetadata.Tag>(); for (String tagId : tagIds) { if (Config.Tags.SESSIONS.equals(tagId) || Config.Tags.SPECIAL_KEYNOTE.equals(tagId)) { continue; } TagMetadata.Tag tag = data.getTagMetadata().getTag(tagId); if (tag == null) { continue; } tags.add(tag); } if (tags.size() == 0) { mTagsContainer.setVisibility(View.GONE); return; } Collections.sort(tags, TagMetadata.TAG_DISPLAY_ORDER_COMPARATOR); for (final TagMetadata.Tag tag : tags) { TextView chipView = (TextView) inflater.inflate( R.layout.include_session_tag_chip, mTags, false); chipView.setText(tag.getName()); chipView.setContentDescription(tag.getName()); chipView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getContext(), ExploreSessionsActivity.class) .putExtra(ExploreSessionsActivity.EXTRA_FILTER_TAG, tag.getId()) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); getActivity().startActivity(intent); } }); mTags.addView(chipView); } } } private void showGiveFeedbackCard(final SessionDetailModel data) { final MessageCardView messageCardView = (MessageCardView) getActivity().findViewById( R.id.give_feedback_card); messageCardView.show(); messageCardView.setListener(new MessageCardView.OnMessageCardButtonClicked() { @Override public void onMessageCardButtonClicked(String tag) { if (getResources().getString(R.string.tag_give_feedback).equals(tag)) { sendUserAction(SessionDetailUserActionEnum.GIVE_FEEDBACK, null); Intent intent = data.getFeedbackIntent(); getActivity().startActivity(intent); } else { sDismissedFeedbackCard.add(data.getSessionId()); messageCardView.dismiss(); } } }); } private void showInScheduleDeferred(final boolean isInSchedule) { mDeferredUiOperations.add(new Runnable() { @Override public void run() { if (mAddScheduleFab.isChecked() != isInSchedule) { mAddScheduleFab.setChecked(isInSchedule); mAddScheduleFab.setImageResource(isInSchedule ? R.drawable.ic_session_in_schedule : R.drawable.ic_add_to_schedule); mAddScheduleFab.setContentDescription(getString(isInSchedule ? R.string.remove_from_schedule_desc : R.string.add_to_schedule_desc)); } } }); tryExecuteDeferredUiOperations(); } private void tryExecuteDeferredUiOperations() { for (Runnable r : mDeferredUiOperations) { r.run(); mDeferredUiOperations.clear(); } } /* * Event structure: * Category -> "Session Details" * Action -> Link Text * Label -> Session's Title * Value -> 0. */ private void fireLinkEvent(int actionId, SessionDetailModel data) { // ANALYTICS EVENT: Click on a link in the Session Details page. // Contains: The link's name and the session title. AnalyticsHelper.sendEvent("Session", getString(actionId), data.getSessionTitle()); } private Action getActionForTitle(String title) { Uri sessionUri = ((SessionDetailActivity) getActivity()).getSessionUri(); String uuid = sessionUri.toString().substring(sessionUri.toString().lastIndexOf("/") + 1); Uri uri = new Uri.Builder() .scheme(Config.HTTPS) .authority(BuildConfig.PRODUCTION_WEBSITE_HOST_NAME) .path(BuildConfig.WEB_URL_SCHEDULE_PATH) .appendQueryParameter(Config.SESSION_ID_URL_QUERY_KEY, uuid) .build(); // Build a schema.org Thing that represents the session details currently displayed. Its // name is the session's title, and its URL is a deep link back to this // SessionDetailFragment. Thing session = new Thing.Builder() .setName(title) .setUrl(uri) .build(); // Build a schema.org Action that represents a user viewing this session screen. This Action // is then ready to be passed to the App Indexing API. Read more about the API here: // https://developers.google.com/app-indexing/introduction#android. return new Action.Builder(Action.TYPE_VIEW) .setObject(session) .build(); } }