/* * 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.gtv; 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.sync.SyncHelper; import com.google.android.apps.iosched.ui.BaseActivity; import com.google.android.apps.iosched.ui.SessionLivestreamActivity.SessionSummaryFragment; import com.google.android.apps.iosched.util.UIUtils; import com.google.android.youtube.api.YouTube; import com.google.android.youtube.api.YouTubePlayer; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.BaseColumns; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.method.LinkMovementMethod; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.IOException; 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; /** * A Google TV home activity for Google I/O 2012 that displays session live streams pulled in * from YouTube. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class GoogleTVSessionLivestreamActivity extends BaseActivity implements LoaderCallbacks<Cursor>, YouTubePlayer.OnPlaybackEventsListener, YouTubePlayer.OnFullscreenListener, OnItemClickListener { private static final String TAG = makeLogTag(GoogleTVSessionLivestreamActivity.class); private static final String TAG_SESSION_SUMMARY = "session_summary"; private static final int UPCOMING_SESSIONS_QUERY_ID = 3; private static final String PROMO_VIDEO_URL = "http://www.youtube.com/watch?v=gTA-5HM8Zhs"; private boolean mIsFullscreen = false; private boolean mTrackPlay = true; private YouTubePlayer mYouTubePlayer; private FrameLayout mPlayerContainer; private String mYouTubeVideoId; private LinearLayout mMainLayout; private LinearLayout mVideoLayout; private LinearLayout mExtraLayout; private FrameLayout mSummaryLayout; private ListView mLiveListView; private SyncHelper mGTVSyncHelper; private LivestreamAdapter mLivestreamAdapter; private String mSessionId; private Handler mHandler = new Handler(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); YouTube.initialize(this, Config.YOUTUBE_API_KEY); setContentView(R.layout.activity_session_livestream); // Set up YouTube player mYouTubePlayer = (YouTubePlayer) getSupportFragmentManager().findFragmentById( R.id.livestream_player); mYouTubePlayer.setOnPlaybackEventsListener(this); mYouTubePlayer.enableCustomFullscreen(this); mYouTubePlayer.setFullscreenControlFlags( YouTubePlayer.FULLSCREEN_FLAG_CONTROL_SYSTEM_UI); // Views that are common over all layouts mMainLayout = (LinearLayout) findViewById(R.id.livestream_mainlayout); mPlayerContainer = (FrameLayout) findViewById(R.id.livestream_player_container); // 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.googletv_livesextra_layout); mSummaryLayout = (FrameLayout) findViewById(R.id.livestream_summary); // Reload all other data in this activity reloadFromUri(getIntent().getData()); // Start sessions query to populate action bar navigation spinner getSupportLoaderManager().initLoader(SessionsQuery._TOKEN, null, this); // Set up left side listview mLivestreamAdapter = new LivestreamAdapter(this); mLiveListView = (ListView) findViewById(R.id.live_session_list); mLiveListView.setAdapter(mLivestreamAdapter); mLiveListView.setOnItemClickListener(this); mLiveListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); mLiveListView.setSelector(android.R.color.transparent); getSupportActionBar().hide(); // Sync data from Conference API new SyncOperationTask().execute((Void) null); } /** * Reloads all data in the activity and fragments from a given uri */ private void reloadFromUri(Uri newUri) { if (newUri != null && newUri.getPathSegments().size() >= 2) { mSessionId = Sessions.getSessionId(newUri); getSupportLoaderManager().restartLoader(SessionSummaryQuery._TOKEN, null, this); } } @Override public void onBackPressed() { if (mIsFullscreen) { // Exit full screen mode on back key mYouTubePlayer.setFullscreen(false); } else { super.onBackPressed(); } } @Override public void onItemClick(AdapterView<?> adapterView, View view, int itemPosition, long itemId) { final Cursor cursor = (Cursor) mLivestreamAdapter.getItem(itemPosition); final String sessionId = cursor.getString(SessionsQuery.SESSION_ID); if (sessionId != null) { reloadFromUri(Sessions.buildSessionUri(sessionId)); } } @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: final long currentTime = UIUtils.getCurrentTime(this); String selection = Sessions.LIVESTREAM_SELECTION + " and " + Sessions.AT_TIME_SELECTION; String[] selectionArgs = Sessions.buildAtTimeSelectionArgs(currentTime); return new CursorLoader(this, Sessions.buildWithTracksUri(), SessionsQuery.PROJECTION, selection, selectionArgs, null); case UPCOMING_SESSIONS_QUERY_ID: final long newCurrentTime = UIUtils.getCurrentTime(this); String sessionsSelection = Sessions.LIVESTREAM_SELECTION + " and " + Sessions.UPCOMING_SELECTION; String[] sessionsSelectionArgs = Sessions.buildUpcomingSelectionArgs(newCurrentTime); return new CursorLoader(this, Sessions.buildWithTracksUri(), SessionsQuery.PROJECTION, sessionsSelection, sessionsSelectionArgs, null); } return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mHandler.removeCallbacks(mNextSessionStartsInCountdownRunnable); switch (loader.getId()) { case SessionSummaryQuery._TOKEN: loadSession(data); break; case SessionsQuery._TOKEN: mLivestreamAdapter.swapCursor(data); final int selected = locateSelectedItem(data); if (data.getCount() == 0) { handleNoLiveSessionsAvailable(); } else { mLiveListView.setSelection(selected); mLiveListView.requestFocus(selected); final Cursor cursor = (Cursor) mLivestreamAdapter.getItem(selected); final String sessionId = cursor.getString(SessionsQuery.SESSION_ID); if (sessionId != null) { reloadFromUri(Sessions.buildSessionUri(sessionId)); } } break; case UPCOMING_SESSIONS_QUERY_ID: if (data != null && data.getCount() > 0) { data.moveToFirst(); handleUpdateNextUpcomingSession(data); } break; } } public void handleNoLiveSessionsAvailable() { getSupportLoaderManager().initLoader(UPCOMING_SESSIONS_QUERY_ID, null, this); updateSessionViews(PROMO_VIDEO_URL, getString(R.string.missed_io_title), getString(R.string.missed_io_subtitle), UIUtils.CONFERENCE_HASHTAG); //Make link in abstract view clickable TextView abstractLinkTextView = (TextView) findViewById(R.id.session_abstract); abstractLinkTextView.setMovementMethod(new LinkMovementMethod()); } private String mNextSessionTitle; private long mNextSessionStartTime; private final Runnable mNextSessionStartsInCountdownRunnable = new Runnable() { public void run() { int remainingSec = (int) Math.max(0, (mNextSessionStartTime - UIUtils.getCurrentTime( GoogleTVSessionLivestreamActivity.this)) / 1000); final int secs = remainingSec % 86400; final int days = remainingSec / 86400; final String str; if (days == 0) { str = getResources().getString( R.string.starts_in_template_0_days, DateUtils.formatElapsedTime(secs)); } else { str = getResources().getQuantityString( R.plurals.starts_in_template, days, days, DateUtils.formatElapsedTime(secs)); } updateSessionSummaryFragment(mNextSessionTitle, str); if (remainingSec == 0) { // Next session starting now! mHandler.postDelayed(mRefreshSessionsRunnable, 1000); return; } // Repost ourselves to keep updating countdown mHandler.postDelayed(mNextSessionStartsInCountdownRunnable, 1000); } }; private final Runnable mRefreshSessionsRunnable = new Runnable() { @Override public void run() { getSupportLoaderManager().restartLoader(SessionsQuery._TOKEN, null, GoogleTVSessionLivestreamActivity.this); } }; public void handleUpdateNextUpcomingSession(Cursor data) { mNextSessionTitle = getString(R.string.next_live_stream_session_template, data.getString(SessionsQuery.TITLE)); mNextSessionStartTime = data.getLong(SessionsQuery.BLOCK_START); updateSessionViews(PROMO_VIDEO_URL, mNextSessionTitle, "", UIUtils.CONFERENCE_HASHTAG); // Begin countdown til next session mHandler.post(mNextSessionStartsInCountdownRunnable); } /** * Locates which item should be selected in the action bar drop-down spinner based on the * current active session uri */ private int locateSelectedItem(Cursor data) { int selected = 0; if (data != null && mSessionId != null) { while (data.moveToNext()) { 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 onLoaded(String s) { } @Override public void onPlaying() { } @Override public void onPaused() { } @Override public void onBuffering(boolean b) { } @Override public void onEnded() { mHandler.post(mRefreshSessionsRunnable); } @Override public void onError() { Toast.makeText(this, R.string.session_livestream_error, Toast.LENGTH_LONG).show(); LOGE(TAG, getString(R.string.session_livestream_error)); } private void loadSession(Cursor data) { if (data != null && data.moveToFirst()) { // Schedule a data refresh after the session ends. // NOTE: using postDelayed instead of postAtTime helps during debugging, using // mock times. mHandler.postDelayed(mRefreshSessionsRunnable, data.getLong(SessionSummaryQuery.BLOCK_END) + 1000 - UIUtils.getCurrentTime(this)); updateSessionViews( data.getString(SessionSummaryQuery.LIVESTREAM_URL), data.getString(SessionSummaryQuery.TITLE), data.getString(SessionSummaryQuery.ABSTRACT), data.getString(SessionSummaryQuery.HASHTAGS)); } } /** * 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) { if (youtubeUrl == null) { // Get out, nothing to do here Toast.makeText(this, R.string.error_tv_no_url, Toast.LENGTH_SHORT).show(); LOGE(TAG, getString(R.string.error_tv_no_url)); return; } String youtubeVideoId = youtubeUrl; if (youtubeUrl.startsWith("http")) { final Uri youTubeUri = Uri.parse(youtubeUrl); youtubeVideoId = youTubeUri.getQueryParameter("v"); } playVideo(youtubeVideoId); if (mTrackPlay) { EasyTracker.getTracker().trackView("Live Streaming: " + title); LOGD("Tracker", "Live Streaming: " + title); } updateSessionSummaryFragment(title, sessionAbstract); mLiveListView.requestFocus(); } private void updateSessionSummaryFragment(String title, String sessionAbstract) { SessionSummaryFragment sessionSummaryFragment = (SessionSummaryFragment) getSupportFragmentManager().findFragmentByTag(TAG_SESSION_SUMMARY); if (sessionSummaryFragment != null) { sessionSummaryFragment.setSessionSummaryInfo(title, sessionAbstract); } } private void playVideo(String videoId) { if ((mYouTubeVideoId == null || !mYouTubeVideoId.equals(videoId)) && !TextUtils.isEmpty(videoId)) { mYouTubeVideoId = videoId; mYouTubePlayer.loadVideo(mYouTubeVideoId); mTrackPlay = true; } else { mTrackPlay = false; } } private void layoutFullscreenVideo(boolean fullscreen) { if (mIsFullscreen != fullscreen) { mIsFullscreen = fullscreen; mYouTubePlayer.setFullscreen(fullscreen); layoutGoogleTVFullScreen(fullscreen); // Full screen layout changes for all form factors final LayoutParams params = (LayoutParams) mPlayerContainer.getLayoutParams(); if (fullscreen) { params.height = LayoutParams.MATCH_PARENT; mMainLayout.setPadding(0, 0, 0, 0); } else { params.height = LayoutParams.WRAP_CONTENT; } mPlayerContainer.setLayoutParams(params); } } /** * Adjusts tablet layouts for full screen video. * * @param fullscreen True to layout in full screen, false to switch to regular layout */ private void layoutGoogleTVFullScreen(boolean fullscreen) { if (fullscreen) { mExtraLayout.setVisibility(View.GONE); mSummaryLayout.setVisibility(View.GONE); mMainLayout.setPadding(0, 0, 0, 0); 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); mMainLayout.setPadding(padding, padding, padding, padding); mVideoLayout.setBackgroundResource(R.drawable.grey_frame_on_white); final LayoutParams videoLayoutParams = (LayoutParams) mVideoLayout.getLayoutParams(); videoLayoutParams.setMargins(padding, padding, padding, padding); mVideoLayout.setLayoutParams(videoLayoutParams); } } /** * Adapter that backs the action bar drop-down spinner. */ private class LivestreamAdapter extends CursorAdapter { private LayoutInflater mLayoutInflater; public LivestreamAdapter(Context context) { super(context, null, false); 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 side list view return mLayoutInflater.inflate( R.layout.list_item_live_session, parent, false); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { // Inflate view that appears in the side list view return mLayoutInflater.inflate(R.layout.list_item_live_session, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { final TextView titleView = (TextView) view.findViewById(R.id.live_session_title); final TextView subTitleView = (TextView) view.findViewById(R.id.live_session_subtitle); 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); } cursor.getInt(SessionsQuery.TRACK_COLOR); String sessionTitle = cursor.getString(SessionsQuery.TITLE); if (subTitleView != null) { titleView.setText(trackName); subTitleView.setText(sessionTitle); } else { // Selected view titleView.setText(getString(R.string.session_livestream_title) + ": " + trackName); } } } // Enabling media keys for Google TV Devices @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PAUSE: mYouTubePlayer.pause(); return true; case KeyEvent.KEYCODE_MEDIA_PLAY: mYouTubePlayer.play(); return true; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: mYouTubePlayer.seekRelativeMillis(20000); return true; case KeyEvent.KEYCODE_MEDIA_REWIND: mYouTubePlayer.seekRelativeMillis(-20000); return true; default: return super.onKeyDown(keyCode, event); } } // Need to sync with the conference API to get the livestream URLs private class SyncOperationTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { // Perform a sync using SyncHelper if (mGTVSyncHelper == null) { mGTVSyncHelper = new SyncHelper(getApplicationContext()); } try { mGTVSyncHelper.performSync(null, SyncHelper.FLAG_SYNC_LOCAL | SyncHelper.FLAG_SYNC_REMOTE); } catch (IOException e) { LOGE(TAG, "Error loading data for Google I/O 2012.", e); } return null; } } /** * Single session query */ public interface SessionSummaryQuery { int _TOKEN = 0; String[] PROJECTION = { ScheduleContract.Sessions.SESSION_ID, ScheduleContract.Sessions.SESSION_TITLE, ScheduleContract.Sessions.SESSION_ABSTRACT, ScheduleContract.Sessions.SESSION_HASHTAGS, ScheduleContract.Sessions.SESSION_LIVESTREAM_URL, ScheduleContract.Tracks.TRACK_NAME, ScheduleContract.Blocks.BLOCK_START, ScheduleContract.Blocks.BLOCK_END, }; int SESSION_ID = 0; int TITLE = 1; int ABSTRACT = 2; int HASHTAGS = 3; int LIVESTREAM_URL = 4; int TRACK_NAME = 5; int BLOCK_START= 6; int BLOCK_END = 7; } /** * List of sessions query */ public interface SessionsQuery { int _TOKEN = 1; String[] PROJECTION = { BaseColumns._ID, Sessions.SESSION_ID, Sessions.SESSION_TITLE, ScheduleContract.Tracks.TRACK_NAME, ScheduleContract.Tracks.TRACK_COLOR, ScheduleContract.Blocks.BLOCK_START, ScheduleContract.Blocks.BLOCK_END, }; int ID = 0; int SESSION_ID = 1; int TITLE = 2; int TRACK_NAME = 3; int TRACK_COLOR = 4; int BLOCK_START= 5; int BLOCK_END = 6; } }