Java tutorial
/* * Copyright 2012 Google Inc. * * 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.android.apps.iosched.ui; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Presentation; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Color; import android.media.MediaRouter; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.BaseColumns; import android.support.v7.app.ActionBar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.ViewPager; import android.support.v4.widget.CursorAdapter; import android.text.TextUtils; import android.view.*; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.TabHost; import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; import com.google.analytics.tracking.android.EasyTracker; import com.google.android.apps.iosched.Config; import com.google.android.apps.iosched.R; import com.google.android.apps.iosched.provider.ScheduleContract; import com.google.android.apps.iosched.provider.ScheduleContract.Sessions; import com.google.android.apps.iosched.provider.ScheduleContract.Tracks; import com.google.android.apps.iosched.util.SessionsHelper; import com.google.android.apps.iosched.util.UIUtils; import com.google.android.youtube.player.YouTubeInitializationResult; import com.google.android.youtube.player.YouTubePlayer; import com.google.android.youtube.player.YouTubePlayerSupportFragment; import java.util.ArrayList; import java.util.HashMap; import static com.google.android.apps.iosched.util.LogUtils.LOGD; import static com.google.android.apps.iosched.util.LogUtils.LOGE; import static com.google.android.apps.iosched.util.LogUtils.makeLogTag; /** * An activity that displays the session live stream video which is pulled in from YouTube. The * UI adapts for both phone and tablet. As we want to prevent the YouTube player from restarting * and buffering again on orientation change, we handle configuration changes manually. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class SessionLivestreamActivity extends BaseActivity implements LoaderCallbacks<Cursor>, YouTubePlayer.OnInitializedListener, YouTubePlayer.PlayerStateChangeListener, YouTubePlayer.OnFullscreenListener, ActionBar.OnNavigationListener { private static final String TAG = makeLogTag(SessionLivestreamActivity.class); private static final String EXTRA_PREFIX = "com.google.android.iosched.extra."; private static final int YOUTUBE_RECOVERY_RESULT = 1; private static final String LOADER_SESSIONS_ARG = "futureSessions"; public static final String EXTRA_YOUTUBE_URL = EXTRA_PREFIX + "youtube_url"; public static final String EXTRA_TRACK = EXTRA_PREFIX + "track"; public static final String EXTRA_TITLE = EXTRA_PREFIX + "title"; public static final String EXTRA_ABSTRACT = EXTRA_PREFIX + "abstract"; public static final String KEYNOTE_TRACK_NAME = "Keynote"; private static final String TAG_SESSION_SUMMARY = "session_summary"; private static final String TAG_CAPTIONS = "captions"; private static final int TABNUM_SESSION_SUMMARY = 0; private static final int TABNUM_SOCIAL_STREAM = 1; private static final int TABNUM_LIVE_CAPTIONS = 2; private static final String EXTRA_TAB_STATE = "tag"; private static final int STREAM_REFRESH_TIME = 5 * 60 * 1000; // 5 minutes private boolean mIsTablet; private boolean mIsFullscreen = false; private boolean mLoadFromExtras = false; private boolean mTrackPlay = true; private TabHost mTabHost; private TabsAdapter mTabsAdapter; private YouTubePlayer mYouTubePlayer; private YouTubePlayerSupportFragment mYouTubeFragment; private LinearLayout mPlayerContainer; private String mYouTubeVideoId; private LinearLayout mMainLayout; private LinearLayout mVideoLayout; private LinearLayout mExtraLayout; private FrameLayout mPresentationControls; private FrameLayout mSummaryLayout; private FrameLayout mFullscreenCaptions; private MenuItem mCaptionsMenuItem; private MenuItem mShareMenuItem; private MenuItem mPresentationMenuItem; private Runnable mShareMenuDeferredSetup; private Runnable mSessionSummaryDeferredSetup; private SessionShareData mSessionShareData; private boolean mSessionsFound; private boolean isKeynote = false; private Handler mHandler = new Handler(); private int mYouTubeFullscreenFlags; private LivestreamAdapter mLivestreamAdapter; private Uri mSessionUri; private String mSessionId; private String mTrackName; private MediaRouter mMediaRouter; private YouTubePresentation mPresentation; private MediaRouter.SimpleCallback mMediaRouterCallback; @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override protected void onCreate(Bundle savedInstanceState) { if (UIUtils.hasICS()) { // We can't use this mode on HC as compatible ActionBar doesn't work well with the YT // player in full screen mode (no overlays allowed). requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); } super.onCreate(savedInstanceState); setContentView(R.layout.activity_session_livestream); mIsTablet = UIUtils.isHoneycombTablet(this); // Set up YouTube player mYouTubeFragment = (YouTubePlayerSupportFragment) getSupportFragmentManager() .findFragmentById(R.id.livestream_player); mYouTubeFragment.initialize(Config.YOUTUBE_API_KEY, this); // Views that are common over all layouts mMainLayout = (LinearLayout) findViewById(R.id.livestream_mainlayout); mPresentationControls = (FrameLayout) findViewById(R.id.presentation_controls); adjustMainLayoutForActionBar(); mPlayerContainer = (LinearLayout) findViewById(R.id.livestream_player_container); mFullscreenCaptions = (FrameLayout) findViewById(R.id.fullscreen_captions); final LayoutParams params = (LayoutParams) mFullscreenCaptions.getLayoutParams(); params.setMargins(0, getActionBarHeightPx(), 0, getActionBarHeightPx()); mFullscreenCaptions.setLayoutParams(params); ViewPager viewPager = (ViewPager) findViewById(R.id.pager); viewPager.setOffscreenPageLimit(2); if (!mIsTablet) { viewPager.setPageMarginDrawable(R.drawable.grey_border_inset_lr); } viewPager.setPageMargin(getResources().getDimensionPixelSize(R.dimen.page_margin_width)); // Set up tabs w/ViewPager mTabHost = (TabHost) findViewById(android.R.id.tabhost); mTabHost.setup(); mTabsAdapter = new TabsAdapter(this, mTabHost, viewPager); if (mIsTablet) { // Tablet UI specific views getSupportFragmentManager().beginTransaction() .add(R.id.livestream_summary, new SessionSummaryFragment(), TAG_SESSION_SUMMARY).commit(); mVideoLayout = (LinearLayout) findViewById(R.id.livestream_videolayout); mExtraLayout = (LinearLayout) findViewById(R.id.livestream_extralayout); mSummaryLayout = (FrameLayout) findViewById(R.id.livestream_summary); } else { // Handset UI specific views mTabsAdapter.addTab(getString(R.string.session_livestream_info), new SessionSummaryFragment(), TABNUM_SESSION_SUMMARY); } mTabsAdapter.addTab(getString(R.string.title_stream), new SocialStreamFragment(), TABNUM_SOCIAL_STREAM); mTabsAdapter.addTab(getString(R.string.session_livestream_captions), new SessionLiveCaptionsFragment(), TABNUM_LIVE_CAPTIONS); if (savedInstanceState != null) { mTabHost.setCurrentTabByTag(savedInstanceState.getString(EXTRA_TAB_STATE)); } // Reload all other data in this activity reloadFromIntent(getIntent()); // Update layout based on current configuration updateLayout(getResources().getConfiguration()); // Set up action bar if (!mLoadFromExtras) { // Start sessions query to populate action bar navigation spinner getSupportLoaderManager().initLoader(SessionsQuery._TOKEN, null, this); // Set up action bar mLivestreamAdapter = new LivestreamAdapter(getSupportActionBar().getThemedContext()); final ActionBar actionBar = getSupportActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); actionBar.setListNavigationCallbacks(mLivestreamAdapter, this); actionBar.setDisplayShowTitleEnabled(false); } // Media Router and Presentation set up if (UIUtils.hasJellyBeanMR1()) { mMediaRouterCallback = new MediaRouter.SimpleCallback() { @Override public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { LOGD(TAG, "onRouteSelected: type=" + type + ", info=" + info); updatePresentation(); } @Override public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) { LOGD(TAG, "onRouteUnselected: type=" + type + ", info=" + info); updatePresentation(); } @Override public void onRoutePresentationDisplayChanged(MediaRouter router, MediaRouter.RouteInfo info) { LOGD(TAG, "onRoutePresentationDisplayChanged: info=" + info); updatePresentation(); } }; mMediaRouter = (MediaRouter) getSystemService(Context.MEDIA_ROUTER_SERVICE); final ImageButton playPauseButton = (ImageButton) findViewById(R.id.play_pause_button); playPauseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mYouTubePlayer != null) { if (mYouTubePlayer.isPlaying()) { mYouTubePlayer.pause(); playPauseButton.setImageResource(R.drawable.ic_livestream_play); } else { mYouTubePlayer.play(); playPauseButton.setImageResource(R.drawable.ic_livestream_pause); } } } }); updatePresentation(); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override protected void onResume() { super.onResume(); if (mSessionSummaryDeferredSetup != null) { mSessionSummaryDeferredSetup.run(); mSessionSummaryDeferredSetup = null; } mHandler.postDelayed(mStreamRefreshRunnable, STREAM_REFRESH_TIME); if (UIUtils.hasJellyBeanMR1()) { mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_LIVE_VIDEO, mMediaRouterCallback); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override protected void onPause() { super.onPause(); mHandler.removeCallbacks(mStreamRefreshRunnable); if (UIUtils.hasJellyBeanMR1()) { mMediaRouter.removeCallback(mMediaRouterCallback); } } @Override public void onStop() { super.onStop(); // Dismiss the presentation when the activity is not visible if (mPresentation != null) { LOGD(TAG, "Dismissing presentation because the activity is no longer visible."); mPresentation.dismiss(); mPresentation = null; } } /** * Reloads all data in the activity and fragments from a given intent * @param intent The intent to load from */ private void reloadFromIntent(Intent intent) { final String youtubeUrl = intent.getStringExtra(EXTRA_YOUTUBE_URL); // Check if youtube url is set as an extra first if (youtubeUrl != null) { mLoadFromExtras = true; String trackName = intent.getStringExtra(EXTRA_TRACK); String actionBarTitle; if (trackName == null) { actionBarTitle = getString(R.string.session_livestream_title); } else { isKeynote = KEYNOTE_TRACK_NAME.equals(trackName); actionBarTitle = trackName + " - " + getString(R.string.session_livestream_title); } getSupportActionBar().setTitle(actionBarTitle); updateSessionViews(youtubeUrl, intent.getStringExtra(EXTRA_TITLE), intent.getStringExtra(EXTRA_ABSTRACT), UIUtils.CONFERENCE_HASHTAG, trackName); } else { // Otherwise load from session uri reloadFromUri(intent.getData()); } } /** * Reloads all data in the activity and fragments from a given uri * @param newUri The session uri to load from */ private void reloadFromUri(Uri newUri) { mSessionUri = newUri; if (mSessionUri != null && mSessionUri.getPathSegments().size() >= 2) { mSessionId = Sessions.getSessionId(mSessionUri); getSupportLoaderManager().restartLoader(SessionSummaryQuery._TOKEN, null, this); } else { // No session uri, get out mSessionUri = null; finish(); } } /** * Helper method to start this activity using only extras (rather than session uri). * @param context The package context * @param youtubeUrl The youtube url or video id to load * @param track The track title (appears as part of action bar title), can be null * @param title The title to show in the session info fragment, can be null * @param sessionAbstract The session abstract to show in the session info fragment, can be null */ public static void startFromExtras(Context context, String youtubeUrl, String track, String title, String sessionAbstract) { if (youtubeUrl == null) { return; } final Intent i = new Intent(); i.setClass(context, SessionLivestreamActivity.class); i.putExtra(EXTRA_YOUTUBE_URL, youtubeUrl); i.putExtra(EXTRA_TRACK, track); i.putExtra(EXTRA_TITLE, title); i.putExtra(EXTRA_ABSTRACT, sessionAbstract); context.startActivity(i); } /** * Start a keynote live stream from extras. This sets the track name correctly so captions * will be correctly displayed. * * This will play the video given in youtubeUrl, however if a number ranging from 1-99 is * passed in this param instead it will be parsed and used to lookup a youtube ID from a * google spreadsheet using the param as the spreadsheet row number. */ public static void startKeynoteFromExtras(Context context, String youtubeUrl, String title, String sessionAbstract) { startFromExtras(context, youtubeUrl, KEYNOTE_TRACK_NAME, title, sessionAbstract); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mTabHost != null) { outState.putString(EXTRA_TAB_STATE, mTabHost.getCurrentTabTag()); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.session_livestream, menu); mCaptionsMenuItem = menu.findItem(R.id.menu_captions); mPresentationMenuItem = menu.findItem(R.id.menu_presentation); mPresentationMenuItem.setVisible(mPresentation != null); updatePresentationMenuItem(mPresentation != null); mShareMenuItem = menu.findItem(R.id.menu_share); if (mShareMenuDeferredSetup != null) { mShareMenuDeferredSetup.run(); } if (!mIsTablet && Configuration.ORIENTATION_LANDSCAPE == getResources().getConfiguration().orientation) { mCaptionsMenuItem.setVisible(true); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_captions: if (mIsFullscreen) { if (mFullscreenCaptions.getVisibility() == View.GONE) { mFullscreenCaptions.setVisibility(View.VISIBLE); SessionLiveCaptionsFragment captionsFragment; captionsFragment = (SessionLiveCaptionsFragment) getSupportFragmentManager() .findFragmentByTag(TAG_CAPTIONS); if (captionsFragment == null) { captionsFragment = new SessionLiveCaptionsFragment(); captionsFragment.setDarkTheme(true); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.add(R.id.fullscreen_captions, captionsFragment, TAG_CAPTIONS); ft.commit(); } captionsFragment.setTrackName(mTrackName); return true; } } mFullscreenCaptions.setVisibility(View.GONE); break; case R.id.menu_share: if (mSessionShareData != null) { new SessionsHelper(this).shareSession(this, R.string.share_livestream_template, mSessionShareData.title, mSessionShareData.hashtag, mSessionShareData.sessionUrl); return true; } break; case R.id.menu_presentation: if (mPresentation != null) { mPresentation.dismiss(); } else { updatePresentation(); } break; } return super.onOptionsItemSelected(item); } @Override public void onConfigurationChanged(Configuration newConfig) { // Need to handle configuration changes so as not to interrupt YT player and require // buffering again updateLayout(newConfig); super.onConfigurationChanged(newConfig); } @Override public boolean onNavigationItemSelected(int itemPosition, long itemId) { final Cursor cursor = (Cursor) mLivestreamAdapter.getItem(itemPosition); String trackName = cursor.getString(SessionsQuery.TRACK_NAME); int trackColor = cursor.getInt(SessionsQuery.TRACK_COLOR); setActionBarTrackIcon(trackName, trackColor); final String sessionId = cursor.getString(SessionsQuery.SESSION_ID); if (sessionId != null) { reloadFromUri(Sessions.buildSessionUri(sessionId)); return true; } return false; } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch (id) { case SessionSummaryQuery._TOKEN: return new CursorLoader(this, Sessions.buildWithTracksUri(mSessionId), SessionSummaryQuery.PROJECTION, null, null, null); case SessionsQuery._TOKEN: boolean futureSessions = false; if (args != null) { futureSessions = args.getBoolean(LOADER_SESSIONS_ARG, false); } final long currentTime = UIUtils.getCurrentTime(this); String selection = Sessions.LIVESTREAM_SELECTION + " and "; String[] selectionArgs; if (!futureSessions) { selection += Sessions.AT_TIME_SELECTION; selectionArgs = Sessions.buildAtTimeSelectionArgs(currentTime); } else { selection += Sessions.UPCOMING_SELECTION; selectionArgs = Sessions.buildUpcomingSelectionArgs(currentTime); } return new CursorLoader(this, Sessions.buildWithTracksUri(), SessionsQuery.PROJECTION, selection, selectionArgs, null); } return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { switch (loader.getId()) { case SessionSummaryQuery._TOKEN: loadSession(data); break; case SessionsQuery._TOKEN: mLivestreamAdapter.swapCursor(data); if (data != null && data.getCount() > 0) { mSessionsFound = true; final int selected = locateSelectedItem(data); getSupportActionBar().setSelectedNavigationItem(selected); } else if (mSessionsFound) { mSessionsFound = false; final Bundle bundle = new Bundle(); bundle.putBoolean(LOADER_SESSIONS_ARG, true); getSupportLoaderManager().restartLoader(SessionsQuery._TOKEN, bundle, this); } else { finish(); } break; } } /** * Locates which item should be selected in the action bar drop-down spinner based on the * current active session uri * @param data The data * @return The row num of the item that should be selected or 0 if not found */ private int locateSelectedItem(Cursor data) { int selected = 0; if (data != null && (mSessionId != null || mTrackName != null)) { final boolean findNextSessionByTrack = mTrackName != null; while (data.moveToNext()) { if (findNextSessionByTrack) { if (mTrackName.equals(data.getString(SessionsQuery.TRACK_NAME))) { selected = data.getPosition(); mTrackName = null; break; } } else { if (mSessionId.equals(data.getString(SessionsQuery.SESSION_ID))) { selected = data.getPosition(); } } } } return selected; } @Override public void onLoaderReset(Loader<Cursor> loader) { switch (loader.getId()) { case SessionsQuery._TOKEN: mLivestreamAdapter.swapCursor(null); break; } } @Override public void onFullscreen(boolean fullscreen) { layoutFullscreenVideo(fullscreen); } @Override public void onLoading() { } @Override public void onLoaded(String s) { } @Override public void onAdStarted() { } @Override public void onVideoStarted() { } @Override public void onVideoEnded() { } @Override public void onError(YouTubePlayer.ErrorReason errorReason) { Toast.makeText(this, R.string.session_livestream_error_playback, Toast.LENGTH_LONG).show(); LOGE(TAG, errorReason.toString()); } private void loadSession(Cursor data) { if (data != null && data.moveToFirst()) { mTrackName = data.getString(SessionSummaryQuery.TRACK_NAME); if (TextUtils.isEmpty(mTrackName)) { mTrackName = KEYNOTE_TRACK_NAME; isKeynote = true; } final long currentTime = UIUtils.getCurrentTime(this); if (currentTime > data.getLong(SessionSummaryQuery.BLOCK_END)) { getSupportLoaderManager().restartLoader(SessionsQuery._TOKEN, null, this); return; } updateTagStreamFragment(data.getString(SessionSummaryQuery.HASHTAGS)); updateSessionViews(data.getString(SessionSummaryQuery.LIVESTREAM_URL), data.getString(SessionSummaryQuery.TITLE), data.getString(SessionSummaryQuery.ABSTRACT), data.getString(SessionSummaryQuery.HASHTAGS), mTrackName); } } /** * Updates views that rely on session data from explicit strings. */ private void updateSessionViews(final String youtubeUrl, final String title, final String sessionAbstract, final String hashTag, final String trackName) { if (youtubeUrl == null) { // Get out, nothing to do here finish(); return; } mTrackName = trackName; String youtubeVideoId = youtubeUrl; if (youtubeUrl.startsWith("http")) { final Uri youTubeUri = Uri.parse(youtubeUrl); youtubeVideoId = youTubeUri.getQueryParameter("v"); } // Play the video playVideo(youtubeVideoId); if (mTrackPlay) { EasyTracker.getTracker().sendView("Live Streaming: " + title); LOGD("Tracker", "Live Streaming: " + title); } final String newYoutubeUrl = Config.YOUTUBE_SHARE_URL_PREFIX + youtubeVideoId; mSessionShareData = new SessionShareData(title, hashTag, newYoutubeUrl); mShareMenuDeferredSetup = new Runnable() { @Override public void run() { new SessionsHelper(SessionLivestreamActivity.this).tryConfigureShareMenuItem(mShareMenuItem, R.string.share_livestream_template, title, hashTag, newYoutubeUrl); } }; if (mShareMenuItem != null) { mShareMenuDeferredSetup.run(); mShareMenuDeferredSetup = null; } mSessionSummaryDeferredSetup = new Runnable() { @Override public void run() { updateSessionSummaryFragment(title, sessionAbstract); updateSessionLiveCaptionsFragment(trackName); } }; if (!mLoadFromExtras) { mSessionSummaryDeferredSetup.run(); mSessionSummaryDeferredSetup = null; } } private void updateSessionSummaryFragment(String title, String sessionAbstract) { SessionSummaryFragment sessionSummaryFragment = null; if (mIsTablet) { sessionSummaryFragment = (SessionSummaryFragment) getSupportFragmentManager() .findFragmentByTag(TAG_SESSION_SUMMARY); } else { if (mTabsAdapter.mFragments.containsKey(TABNUM_SESSION_SUMMARY)) { sessionSummaryFragment = (SessionSummaryFragment) mTabsAdapter.mFragments .get(TABNUM_SESSION_SUMMARY); } } if (sessionSummaryFragment != null) { sessionSummaryFragment.setSessionSummaryInfo( isKeynote ? getString(R.string.session_livestream_keynote_title, title) : title, (isKeynote && TextUtils.isEmpty(sessionAbstract)) ? getString(R.string.session_livestream_keynote_desc) : sessionAbstract); } } private Runnable mStreamRefreshRunnable = new Runnable() { @Override public void run() { if (mTabsAdapter != null && mTabsAdapter.mFragments != null && mTabsAdapter.mFragments.containsKey(TABNUM_SOCIAL_STREAM)) { final SocialStreamFragment socialStreamFragment = (SocialStreamFragment) mTabsAdapter.mFragments .get(TABNUM_SOCIAL_STREAM); socialStreamFragment.refresh(); } mHandler.postDelayed(mStreamRefreshRunnable, STREAM_REFRESH_TIME); } }; private void updateTagStreamFragment(String trackHashTag) { String hashTags = UIUtils.CONFERENCE_HASHTAG; if (!TextUtils.isEmpty(trackHashTag)) { hashTags += " " + trackHashTag; } if (mTabsAdapter.mFragments.containsKey(TABNUM_SOCIAL_STREAM)) { final SocialStreamFragment socialStreamFragment = (SocialStreamFragment) mTabsAdapter.mFragments .get(TABNUM_SOCIAL_STREAM); socialStreamFragment.refresh(hashTags); } } private void updateSessionLiveCaptionsFragment(String trackName) { if (mTabsAdapter.mFragments.containsKey(TABNUM_LIVE_CAPTIONS)) { final SessionLiveCaptionsFragment captionsFragment = (SessionLiveCaptionsFragment) mTabsAdapter.mFragments .get(TABNUM_LIVE_CAPTIONS); captionsFragment.setTrackName(trackName); } } private void playVideo(String videoId) { if ((TextUtils.isEmpty(mYouTubeVideoId) || !mYouTubeVideoId.equals(videoId)) && !TextUtils.isEmpty(videoId)) { mYouTubeVideoId = videoId; if (mYouTubePlayer != null) { mYouTubePlayer.loadVideo(mYouTubeVideoId); } mTrackPlay = true; if (mSessionShareData != null) { mSessionShareData.sessionUrl = Config.YOUTUBE_SHARE_URL_PREFIX + mYouTubeVideoId; } } } @Override public Intent getParentActivityIntent() { if (mLoadFromExtras || isKeynote || mSessionUri == null) { return new Intent(this, HomeActivity.class); } else { return new Intent(Intent.ACTION_VIEW, mSessionUri); } } @Override public void onInitializationSuccess(YouTubePlayer.Provider provider, YouTubePlayer youTubePlayer, boolean wasRestored) { // Set up YouTube player mYouTubePlayer = youTubePlayer; mYouTubePlayer.setPlayerStateChangeListener(this); mYouTubePlayer.setFullscreen(mIsFullscreen); mYouTubePlayer.setOnFullscreenListener(this); // YouTube player flags: use a custom full screen layout; let the YouTube player control // the system UI (hiding navigation controls, ActionBar etc); and let the YouTube player // handle the orientation state of the activity. mYouTubeFullscreenFlags = YouTubePlayer.FULLSCREEN_FLAG_CUSTOM_LAYOUT | YouTubePlayer.FULLSCREEN_FLAG_CONTROL_SYSTEM_UI | YouTubePlayer.FULLSCREEN_FLAG_CONTROL_ORIENTATION; // On smaller screen devices always switch to full screen in landscape mode if (!mIsTablet) { mYouTubeFullscreenFlags |= YouTubePlayer.FULLSCREEN_FLAG_ALWAYS_FULLSCREEN_IN_LANDSCAPE; } // Apply full screen flags setFullscreenFlags(); // If a presentation display is available, set YouTube player style to minimal if (mPresentation != null) { mYouTubePlayer.setPlayerStyle(YouTubePlayer.PlayerStyle.MINIMAL); } // Load the requested video if (!TextUtils.isEmpty(mYouTubeVideoId)) { mYouTubePlayer.loadVideo(mYouTubeVideoId); } updatePresentation(); } @Override public void onInitializationFailure(YouTubePlayer.Provider provider, YouTubeInitializationResult result) { LOGE(TAG, result.toString()); if (result.isUserRecoverableError()) { result.getErrorDialog(this, YOUTUBE_RECOVERY_RESULT).show(); } else { String errorMessage = getString(R.string.session_livestream_error_init, result.toString()); Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == YOUTUBE_RECOVERY_RESULT) { if (mYouTubeFragment != null) { mYouTubeFragment.initialize(Config.YOUTUBE_API_KEY, this); } } } /** * Updates the layout based on the provided configuration */ private void updateLayout(Configuration config) { if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (mIsTablet) { layoutTabletForLandscape(); } else { layoutPhoneForLandscape(); } } else { if (mIsTablet) { layoutTabletForPortrait(); } else { layoutPhoneForPortrait(); } } } private void layoutPhoneForLandscape() { layoutFullscreenVideo(true); } private void layoutPhoneForPortrait() { layoutFullscreenVideo(false); if (mTabsAdapter.mFragments.containsKey(TABNUM_SESSION_SUMMARY)) { ((SessionSummaryFragment) mTabsAdapter.mFragments.get(TABNUM_SESSION_SUMMARY)).updateViews(); } } private void layoutTabletForLandscape() { mMainLayout.setOrientation(LinearLayout.HORIZONTAL); final LayoutParams videoLayoutParams = (LayoutParams) mVideoLayout.getLayoutParams(); videoLayoutParams.height = LayoutParams.MATCH_PARENT; videoLayoutParams.width = 0; videoLayoutParams.weight = 1; mVideoLayout.setLayoutParams(videoLayoutParams); final LayoutParams extraLayoutParams = (LayoutParams) mExtraLayout.getLayoutParams(); extraLayoutParams.height = LayoutParams.MATCH_PARENT; extraLayoutParams.width = 0; extraLayoutParams.weight = 1; mExtraLayout.setLayoutParams(extraLayoutParams); } private void layoutTabletForPortrait() { mMainLayout.setOrientation(LinearLayout.VERTICAL); final LayoutParams videoLayoutParams = (LayoutParams) mVideoLayout.getLayoutParams(); videoLayoutParams.width = LayoutParams.MATCH_PARENT; if (mLoadFromExtras) { // Loading from extras, let the top fragment wrap_content videoLayoutParams.height = LayoutParams.WRAP_CONTENT; videoLayoutParams.weight = 0; } else { // Otherwise the session description will be longer, give it some space videoLayoutParams.height = 0; videoLayoutParams.weight = 7; } mVideoLayout.setLayoutParams(videoLayoutParams); final LayoutParams extraLayoutParams = (LayoutParams) mExtraLayout.getLayoutParams(); extraLayoutParams.width = LayoutParams.MATCH_PARENT; extraLayoutParams.height = 0; extraLayoutParams.weight = 5; mExtraLayout.setLayoutParams(extraLayoutParams); } private void layoutFullscreenVideo(boolean fullscreen) { if (mIsFullscreen != fullscreen) { mIsFullscreen = fullscreen; if (mIsTablet) { // Tablet specific full screen layout layoutTabletFullscreen(fullscreen); } else { // Phone specific full screen layout layoutPhoneFullscreen(fullscreen); } // Full screen layout changes for all form factors final LayoutParams params = (LayoutParams) mPlayerContainer.getLayoutParams(); if (fullscreen) { if (mCaptionsMenuItem != null) { mCaptionsMenuItem.setVisible(true); } params.height = LayoutParams.MATCH_PARENT; mMainLayout.setPadding(0, 0, 0, 0); } else { getSupportActionBar().show(); if (mCaptionsMenuItem != null) { mCaptionsMenuItem.setVisible(false); } mFullscreenCaptions.setVisibility(View.GONE); params.height = LayoutParams.WRAP_CONTENT; adjustMainLayoutForActionBar(); } View youTubePlayerView = mYouTubeFragment.getView(); if (youTubePlayerView != null) { ViewGroup.LayoutParams playerParams = mYouTubeFragment.getView().getLayoutParams(); playerParams.height = fullscreen ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT; youTubePlayerView.setLayoutParams(playerParams); } mPlayerContainer.setLayoutParams(params); } } private void adjustMainLayoutForActionBar() { if (UIUtils.hasICS()) { // On ICS+ we use FEATURE_ACTION_BAR_OVERLAY so full screen mode doesn't need to // re-adjust layouts when hiding action bar. To account for this we add action bar // height pack to the padding when not in full screen mode. mMainLayout.setPadding(0, getActionBarHeightPx(), 0, 0); } } /** * Adjusts tablet layouts for full screen video. * * @param fullscreen True to layout in full screen, false to switch to regular layout */ private void layoutTabletFullscreen(boolean fullscreen) { if (fullscreen) { mExtraLayout.setVisibility(View.GONE); mSummaryLayout.setVisibility(View.GONE); mVideoLayout.setPadding(0, 0, 0, 0); final LayoutParams videoLayoutParams = (LayoutParams) mVideoLayout.getLayoutParams(); videoLayoutParams.setMargins(0, 0, 0, 0); mVideoLayout.setLayoutParams(videoLayoutParams); } else { final int padding = getResources().getDimensionPixelSize(R.dimen.multipane_half_padding); mExtraLayout.setVisibility(View.VISIBLE); mSummaryLayout.setVisibility(View.VISIBLE); mVideoLayout.setBackgroundResource(R.drawable.grey_frame_on_white); final LayoutParams videoLayoutParams = (LayoutParams) mVideoLayout.getLayoutParams(); videoLayoutParams.setMargins(padding, padding, padding, padding); mVideoLayout.setLayoutParams(videoLayoutParams); } } /** * Adjusts phone layouts for full screen video. */ private void layoutPhoneFullscreen(boolean fullscreen) { if (fullscreen) { mTabHost.setVisibility(View.GONE); if (mYouTubePlayer != null) { mYouTubePlayer.setFullscreen(true); } } else { mTabHost.setVisibility(View.VISIBLE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } private int getActionBarHeightPx() { int[] attrs = new int[] { android.R.attr.actionBarSize }; return (int) getTheme().obtainStyledAttributes(attrs).getDimension(0, 0f); } /** * Adapter that backs the action bar drop-down spinner. */ private class LivestreamAdapter extends CursorAdapter { LayoutInflater mLayoutInflater; public LivestreamAdapter(Context context) { super(context, null, 0); if (UIUtils.hasICS()) { mLayoutInflater = (LayoutInflater) getSupportActionBar().getThemedContext() .getSystemService(LAYOUT_INFLATER_SERVICE); } else { mLayoutInflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE); } } @Override public Object getItem(int position) { mCursor.moveToPosition(position); return mCursor; } @Override public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { // Inflate view that appears in the drop-down spinner views return mLayoutInflater.inflate(R.layout.spinner_item_session_livestream, parent, false); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { // Inflate view that appears in the selected spinner return mLayoutInflater.inflate(android.R.layout.simple_spinner_item, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { // Bind view that appears in the selected spinner or in the drop-down final TextView titleView = (TextView) view.findViewById(android.R.id.text1); final TextView subTitleView = (TextView) view.findViewById(android.R.id.text2); String trackName = cursor.getString(SessionsQuery.TRACK_NAME); if (TextUtils.isEmpty(trackName)) { trackName = getString(R.string.app_name); } else { trackName = getString(R.string.session_livestream_track_title, trackName); } String sessionTitle = cursor.getString(SessionsQuery.TITLE); if (subTitleView != null) { // Drop-down view titleView.setText(trackName); subTitleView.setText(sessionTitle); } else { // Selected view titleView.setText(getString(R.string.session_livestream_title) + ": " + trackName); } } } /** * Adapter that backs the ViewPager tabs on the phone UI. */ private static class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener { private final FragmentActivity mContext; private final TabHost mTabHost; private final ViewPager mViewPager; public final HashMap<Integer, Fragment> mFragments; private final ArrayList<Integer> mTabNums; private int tabCount = 0; static class DummyTabFactory implements TabHost.TabContentFactory { private final Context mContext; public DummyTabFactory(Context context) { mContext = context; } @Override public View createTabContent(String tag) { View v = new View(mContext); v.setMinimumWidth(0); v.setMinimumHeight(0); return v; } } @SuppressLint("UseSparseArrays") public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager) { super(activity.getSupportFragmentManager()); mFragments = new HashMap<Integer, Fragment>(3); mContext = activity; mTabHost = tabHost; mViewPager = pager; mTabHost.setOnTabChangedListener(this); mViewPager.setAdapter(this); mViewPager.setOnPageChangeListener(this); mTabNums = new ArrayList<Integer>(3); } public void addTab(String tabTitle, Fragment newFragment, int tabId) { ViewGroup tabWidget = (ViewGroup) mTabHost.findViewById(android.R.id.tabs); TextView tabIndicatorView = (TextView) mContext.getLayoutInflater() .inflate(R.layout.tab_indicator_color, tabWidget, false); tabIndicatorView.setText(tabTitle); final TabHost.TabSpec tabSpec = mTabHost.newTabSpec(String.valueOf(tabCount++)); tabSpec.setIndicator(tabIndicatorView); tabSpec.setContent(new DummyTabFactory(mContext)); mTabHost.addTab(tabSpec); mFragments.put(tabId, newFragment); mTabNums.add(tabId); notifyDataSetChanged(); } @Override public int getCount() { return mFragments.size(); } @Override public Fragment getItem(int position) { return mFragments.get(mTabNums.get(position)); } @Override public void onTabChanged(String tabId) { int position = mTabHost.getCurrentTab(); mViewPager.setCurrentItem(position); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { // Unfortunately when TabHost changes the current tab, it kindly also takes care of // putting focus on it when not in touch mode. The jerk. This hack tries to prevent // this from pulling focus out of our ViewPager. TabWidget widget = mTabHost.getTabWidget(); int oldFocusability = widget.getDescendantFocusability(); widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); mTabHost.setCurrentTab(position); widget.setDescendantFocusability(oldFocusability); } @Override public void onPageScrollStateChanged(int state) { } } /** * Simple fragment that inflates a session summary layout that displays session title and * abstract. */ public static class SessionSummaryFragment extends Fragment { private TextView mTitleView; private TextView mAbstractView; private String mTitle; private String mAbstract; public SessionSummaryFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_session_summary, null); mTitleView = (TextView) view.findViewById(R.id.session_title); mAbstractView = (TextView) view.findViewById(R.id.session_abstract); updateViews(); return view; } public void setSessionSummaryInfo(String sessionTitle, String sessionAbstract) { mTitle = sessionTitle; mAbstract = sessionAbstract; updateViews(); } public void updateViews() { if (mTitleView != null && mAbstractView != null) { mTitleView.setText(mTitle); if (!TextUtils.isEmpty(mAbstract)) { mAbstractView.setVisibility(View.VISIBLE); mAbstractView.setText(mAbstract); } else { mAbstractView.setVisibility(View.GONE); } } } } /** * Simple fragment that shows the live captions. */ public static class SessionLiveCaptionsFragment extends Fragment { private static final String CAPTIONS_DARK_THEME_URL_PARAM = "&theme=dark"; private FrameLayout mContainer; private WebView mWebView; private TextView mNoCaptionsTextView; private boolean mDarkTheme = false; private String mSessionTrack; public SessionLiveCaptionsFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_session_captions, null); mContainer = (FrameLayout) view.findViewById(R.id.session_caption_container); if (!UIUtils.isTablet(getActivity())) { mContainer.setBackgroundColor(Color.WHITE); } mNoCaptionsTextView = (TextView) view.findViewById(android.R.id.empty); mWebView = (WebView) view.findViewById(R.id.session_caption_area); mWebView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { // Disable text selection in WebView (doesn't work well with the YT player // triggering full screen chrome after a timeout) return true; } }); mWebView.setWebViewClient(new WebViewClient() { @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { showNoCaptionsAvailable(); } }); updateViews(); return view; } public void setTrackName(String sessionTrack) { mSessionTrack = sessionTrack; updateViews(); } public void setDarkTheme(boolean dark) { mDarkTheme = dark; } @SuppressLint("SetJavaScriptEnabled") public void updateViews() { if (mWebView != null && !TextUtils.isEmpty(mSessionTrack)) { if (mDarkTheme) { mWebView.setBackgroundColor(Color.BLACK); mContainer.setBackgroundColor(Color.BLACK); mNoCaptionsTextView.setTextColor(Color.WHITE); } else { mWebView.setBackgroundColor(Color.WHITE); mContainer.setBackgroundResource(R.drawable.grey_frame_on_white); } String captionsUrl; final String trackLowerCase = mSessionTrack.toLowerCase(); if (mSessionTrack.equals(KEYNOTE_TRACK_NAME) || trackLowerCase.equals(Config.PRIMARY_LIVESTREAM_TRACK)) { // if keynote or primary track, use primary captions url captionsUrl = Config.PRIMARY_LIVESTREAM_CAPTIONS_URL; } else if (trackLowerCase.equals(Config.SECONDARY_LIVESTREAM_TRACK)) { // else if secondary track use secondary captions url captionsUrl = Config.SECONDARY_LIVESTREAM_CAPTIONS_URL; } else { // otherwise we don't have captions captionsUrl = null; } if (captionsUrl != null) { if (mDarkTheme) { captionsUrl += CAPTIONS_DARK_THEME_URL_PARAM; } mWebView.getSettings().setJavaScriptEnabled(true); mWebView.loadUrl(captionsUrl); mNoCaptionsTextView.setVisibility(View.GONE); mWebView.setVisibility(View.VISIBLE); } else { showNoCaptionsAvailable(); } } } private void showNoCaptionsAvailable() { mWebView.setVisibility(View.GONE); mNoCaptionsTextView.setVisibility(View.VISIBLE); } } /* * Presentation and Media Router methods and classes. This allows use of an externally * connected display to show video content full screen while still showing the regular views * on the primary device display. */ /** * Updates the presentation display. If a suitable external display is attached. The video * will be displayed on that screen instead. If not, the video will be displayed normally. */ private void updatePresentation() { updatePresentation(false); } /** * Updates the presentation display. If a suitable external display is attached. The video * will be displayed on that screen instead. If not, the video will be displayed normally. * @param forceDismiss If true, the external display will be dismissed even if it is still * available. This is useful for if the user wants to explicitly toggle * the external display off even if it's still connected. Setting this to * false will adjust the display based on if a suitable external display * is available or not. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void updatePresentation(boolean forceDismiss) { if (!UIUtils.hasJellyBeanMR1()) { return; } // Get the current route and its presentation display MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO); Display presentationDisplay = route != null ? route.getPresentationDisplay() : null; // Dismiss the current presentation if the display has changed if (mPresentation != null && (mPresentation.getDisplay() != presentationDisplay || forceDismiss)) { hidePresentation(); } // Show a new presentation if needed. if (mPresentation == null && presentationDisplay != null && !forceDismiss) { showPresentation(presentationDisplay); } // Show/hide toggle presentation action item if (mPresentationMenuItem != null) { mPresentationMenuItem.setVisible(presentationDisplay != null); } } /** * Shows the Presentation on the designated Display. Some UI components are also updated: * a play/pause button is displayed where the video would normally be; the YouTube player style * is updated to MINIMAL (more suitable for an external display); and an action item is updated * to indicate the external display is being used. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void showPresentation(Display presentationDisplay) { LOGD(TAG, "Showing video presentation on display: " + presentationDisplay); if (mIsFullscreen) { if (!mIsTablet) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } if (mYouTubePlayer != null) { mYouTubePlayer.setFullscreen(false); } } View presentationView = mYouTubeFragment.getView(); ((ViewGroup) presentationView.getParent()).removeView(presentationView); mPresentation = new YouTubePresentation(this, presentationDisplay, presentationView); mPresentation.setOnDismissListener(mOnDismissListener); try { mPresentation.show(); mPresentationControls.setVisibility(View.VISIBLE); updatePresentationMenuItem(true); if (mYouTubePlayer != null) { mYouTubePlayer.setPlayerStyle(YouTubePlayer.PlayerStyle.MINIMAL); mYouTubePlayer.play(); setFullscreenFlags(true); } } catch (WindowManager.InvalidDisplayException e) { LOGE(TAG, "Couldn't show presentation! Display was removed in the meantime.", e); hidePresentation(); } } /** * Hides the Presentation. Some UI components are also updated: * the video view is returned to its normal place on the primary display; the YouTube player * style is updated to the default; and an action item is updated to indicate the external * display is no longer in use. */ private void hidePresentation() { LOGD(TAG, "Hiding video presentation"); View presentationView = mPresentation.getYouTubeFragmentView(); ((ViewGroup) presentationView.getParent()).removeView(presentationView); mPlayerContainer.addView(presentationView); mPresentationControls.setVisibility(View.GONE); updatePresentationMenuItem(false); mPresentation.dismiss(); mPresentation = null; if (mYouTubePlayer != null) { mYouTubePlayer.setPlayerStyle(YouTubePlayer.PlayerStyle.DEFAULT); mYouTubePlayer.play(); setFullscreenFlags(false); } } /** * Updates the presentation action item. Toggles the icon between two drawables to indicate * if the external display is currently being used or not. * * @param enabled If the external display is enabled or not. */ private void updatePresentationMenuItem(boolean enabled) { if (mPresentationMenuItem != null) { mPresentationMenuItem.setIcon( enabled ? R.drawable.ic_media_route_on_holo_light : R.drawable.ic_media_route_off_holo_light); } } /** * Applies YouTube player full screen flags. Calls through to * {@link #setFullscreenFlags(boolean)}. */ private void setFullscreenFlags() { setFullscreenFlags(true); } /** * Applies YouTube player full screen flags plus forces portrait orientation on small screens * when presentation (secondary display) is enabled (as the full screen layout won't have * anything to display when an external display is connected). * * @param usePresentationMode Whether or not to use presentation mode. This is used when an * external display is connected and the user toggles between * presentation mode. */ private void setFullscreenFlags(boolean usePresentationMode) { if (mYouTubePlayer != null) { if (usePresentationMode && mPresentation != null && !mIsTablet) { mYouTubePlayer.setFullscreenControlFlags(0); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { mYouTubePlayer.setFullscreenControlFlags(mYouTubeFullscreenFlags); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } } /** * Listens for when the Presentation (which is a type of Dialog) is dismissed. */ private final DialogInterface.OnDismissListener mOnDismissListener = new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { if (dialog == mPresentation) { LOGD(TAG, "Presentation was dismissed."); updatePresentation(true); } } }; /** * The main subclass of Presentation that is used to show content on an externally connected * display. There is no way to add a {@link android.app.Fragment}, and therefore a * YouTubePlayerFragment to a {@link android.app.Presentation} (which is just a subclass of * {@link android.app.Dialog}) so instead we pass in a View directly which is the extracted * YouTubePlayerView from the {@link SessionLivestreamActivity} YouTubePlayerFragment. Not * ideal and fairly hacky but there aren't any better alternatives unfortunately. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private final static class YouTubePresentation extends Presentation { private View mYouTubeFragmentView; public YouTubePresentation(Context context, Display display, View presentationView) { super(context, display); mYouTubeFragmentView = presentationView; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(mYouTubeFragmentView); } public View getYouTubeFragmentView() { return mYouTubeFragmentView; } } private static class SessionShareData { String title; String hashtag; String sessionUrl; public SessionShareData(String title, String hashTag, String url) { this.title = title; hashtag = hashTag; sessionUrl = url; } } /** * Single session query */ public interface SessionSummaryQuery { final static int _TOKEN = 0; final static String[] PROJECTION = { ScheduleContract.Sessions.SESSION_ID, ScheduleContract.Sessions.SESSION_TITLE, ScheduleContract.Sessions.SESSION_ABSTRACT, ScheduleContract.Sessions.SESSION_HASHTAGS, ScheduleContract.Sessions.SESSION_LIVESTREAM_URL, Tracks.TRACK_NAME, ScheduleContract.Blocks.BLOCK_START, ScheduleContract.Blocks.BLOCK_END, }; final static int ID = 0; final static int TITLE = 1; final static int ABSTRACT = 2; final static int HASHTAGS = 3; final static int LIVESTREAM_URL = 4; final static int TRACK_NAME = 5; final static int BLOCK_START = 6; final static int BLOCK_END = 7; } /** * List of sessions query */ public interface SessionsQuery { final static int _TOKEN = 1; final static String[] PROJECTION = { BaseColumns._ID, Sessions.SESSION_ID, Sessions.SESSION_TITLE, Tracks.TRACK_NAME, Tracks.TRACK_COLOR, ScheduleContract.Blocks.BLOCK_START, ScheduleContract.Blocks.BLOCK_END, }; final static int _ID = 0; final static int SESSION_ID = 1; final static int TITLE = 2; final static int TRACK_NAME = 3; final static int TRACK_COLOR = 4; final static int BLOCK_START = 5; final static int BLOCK_END = 6; } }