/*
Viewer for Khan Academy
Copyright (C) 2012 Concentric Sky, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.concentricsky.android.khanacademy.app;
import static com.concentricsky.android.khanacademy.Constants.ACTION_BADGE_EARNED;
import static com.concentricsky.android.khanacademy.Constants.ACTION_DOWNLOAD_PROGRESS_UPDATE;
import static com.concentricsky.android.khanacademy.Constants.ACTION_OFFLINE_VIDEO_SET_CHANGED;
import static com.concentricsky.android.khanacademy.Constants.ACTION_TOAST;
import static com.concentricsky.android.khanacademy.Constants.DEFAULT_VIDEO_ID;
import static com.concentricsky.android.khanacademy.Constants.EXTRA_BADGE;
import static com.concentricsky.android.khanacademy.Constants.EXTRA_MESSAGE;
import static com.concentricsky.android.khanacademy.Constants.EXTRA_STATUS;
import static com.concentricsky.android.khanacademy.Constants.PARAM_PROGRESS_DONE;
import static com.concentricsky.android.khanacademy.Constants.PARAM_PROGRESS_UNKNOWN;
import static com.concentricsky.android.khanacademy.Constants.PARAM_TOPIC_ID;
import static com.concentricsky.android.khanacademy.Constants.PARAM_USERVIDEO_POINTS;
import static com.concentricsky.android.khanacademy.Constants.PARAM_VIDEO_ID;
import static com.concentricsky.android.khanacademy.Constants.PARAM_VIDEO_PLAY_STATE;
import static com.concentricsky.android.khanacademy.Constants.PARAM_VIDEO_POSITION;
import static com.concentricsky.android.khanacademy.Constants.TAG_CAPTION_FRAGMENT;
import static com.concentricsky.android.khanacademy.Constants.TAG_VIDEO_FRAGMENT;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import android.app.AlertDialog;
import android.app.FragmentTransaction;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.text.format.Time;
import android.text.method.ScrollingMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ShareActionProvider;
import android.widget.TextView;
import android.widget.Toast;
import com.concentricsky.android.khan.R;
import com.concentricsky.android.khanacademy.Constants;
import com.concentricsky.android.khanacademy.MainMenuDelegate;
import com.concentricsky.android.khanacademy.data.KADataService;
import com.concentricsky.android.khanacademy.data.KADataService.ServiceUnavailableException;
import com.concentricsky.android.khanacademy.data.db.Badge;
import com.concentricsky.android.khanacademy.data.db.User;
import com.concentricsky.android.khanacademy.data.db.UserVideo;
import com.concentricsky.android.khanacademy.data.db.Video;
import com.concentricsky.android.khanacademy.data.remote.KAAPIAdapter;
import com.concentricsky.android.khanacademy.util.Log;
import com.concentricsky.android.khanacademy.util.ObjectCallback;
import com.concentricsky.android.khanacademy.util.OfflineVideoManager;
import com.concentricsky.android.khanacademy.views.ThumbnailWrapper;
import com.concentricsky.android.khanacademy.views.VideoController;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.GenericRawResults;
import com.j256.ormlite.dao.RawRowMapper;
import com.j256.ormlite.stmt.PreparedQuery;
import com.j256.ormlite.stmt.QueryBuilder;
public class VideoDetailActivity extends KADataServiceProviderActivityBase
implements VideoFragment.Callbacks, CaptionFragment.Callbacks {
public static final String LOG_TAG = VideoDetailActivity.class.getSimpleName();
private static final int POINTS_GONE = -1;
private static final int NAV_HIDE_DELAY = 3000; // ms
private String videoId;
private String topicId;
private Video video;
private UserVideo userVideo;
private View headerView;
private TextView pointsView;
private VideoFragment videoFragment;
private CaptionFragment captionFragment;
private MainMenuDelegate mainMenuDelegate;
private Menu mainMenu;
private KADataService dataService;
private ShareActionProvider shareActionProvider;
private int currentOrientation;
private boolean navVis = true;
private boolean videoIsDownloaded = false;
private boolean isFullscreen;
private long downTime;
private String nextVideoId;
private boolean shouldShowVideoControls;
private boolean isBigScreen;
private Time rightNow = new Time();
// As of 12/17/12, looks like most of these values are unnecessary. Point gain appears to be limited server side,
// so even if the user skips ahead they will not be granted too many points.
private int lastPost; // in seconds
private float percentLastSaved;
private boolean saving = false;
private int desiredSeekPosition;
private boolean isVideoPlayerPrepared;
private Handler handler = new Handler();
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_BADGE_EARNED.equals(intent.getAction()) && dataService != null) {
Badge badge = (Badge) intent.getSerializableExtra(EXTRA_BADGE);
dataService.getAPIAdapter().toastBadge(badge);
} else if (ACTION_DOWNLOAD_PROGRESS_UPDATE.equals(intent.getAction())) {
@SuppressWarnings("unchecked")
final Map<String, Integer> youtubeIdToPct = (Map<String, Integer>) intent.getSerializableExtra(EXTRA_STATUS);
if (video != null) {
Integer pct = youtubeIdToPct.get(video.getYoutube_id());
if (pct != null) {
VideoDetailActivity.this.prepareDownloadActionItem(
mainMenu.findItem(R.id.menu_download), pct);
}
}
} else if (ACTION_OFFLINE_VIDEO_SET_CHANGED.equals(intent.getAction())) {
prepareDownloadActionItem(mainMenu.findItem(R.id.menu_download), PARAM_PROGRESS_UNKNOWN);
} else if (ACTION_TOAST.equals(intent.getAction())) {
Toast.makeText(VideoDetailActivity.this, intent.getStringExtra(EXTRA_MESSAGE), Toast.LENGTH_SHORT).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.activity_video_detail);
Intent intent = getIntent();
videoId =
savedInstanceState != null && savedInstanceState.containsKey(PARAM_VIDEO_ID)
? savedInstanceState.getString(PARAM_VIDEO_ID)
: intent != null && intent.hasExtra(PARAM_VIDEO_ID)
? intent.getStringExtra(PARAM_VIDEO_ID)
: DEFAULT_VIDEO_ID;
topicId =
savedInstanceState != null && savedInstanceState.containsKey(PARAM_TOPIC_ID)
? savedInstanceState.getString(PARAM_TOPIC_ID)
: intent != null && intent.hasExtra(PARAM_TOPIC_ID)
? intent.getStringExtra(PARAM_TOPIC_ID)
: null;
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(final KADataService dataService) {
VideoDetailActivity.this.dataService = dataService;
dataService.getAPIAdapter().registerUserUpdateListener(userUpdateListener);
setCurrentVideo(videoId, false);
if (shareActionProvider != null) {
shareActionProvider.setShareIntent(prepareShareIntent(video));
}
User user = getCurrentUser();
setUserVideo(user, video);
setupUIForCurrentVideo();
restoreVideoProgress();
}
});
}
@Override
protected void onStart() {
super.onStart();
if (dataService != null) {
setupUIForCurrentVideo();
}
View rightPane = findViewById(R.id.detail_right_container);
isBigScreen = rightPane != null;
if (mainMenu != null) {
// If mainMenu is null, this activity is being created and this will happen in onCreateOptionsMenu instead.
MenuItem dlItem = mainMenu.findItem(R.id.menu_download);
prepareDownloadActionItem(dlItem, PARAM_PROGRESS_UNKNOWN);
}
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_BADGE_EARNED);
filter.addAction(ACTION_OFFLINE_VIDEO_SET_CHANGED);
filter.addAction(ACTION_DOWNLOAD_PROGRESS_UPDATE);
filter.addAction(ACTION_TOAST);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
}
@Override
protected void onResume() {
super.onResume();
findViewById(R.id.video_fragment_container).setOnTouchListener(videoTouchListener);
if (dataService != null) {
restoreVideoProgress();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// super.onSaveInstanceState(outState);
// Video id, position, and whether it was playing is enough to restore state.
if (video != null) {
// Use actual video id here, not readable_id. We want this specific video back so it has the correct parent.
outState.putString(PARAM_VIDEO_ID, video.getId());
outState.putString(PARAM_TOPIC_ID, topicId);
}
if (videoFragment != null) {
outState.putInt(PARAM_VIDEO_POSITION, videoFragment.getVideoPosition());
outState.putBoolean(PARAM_VIDEO_PLAY_STATE, videoFragment.isPlaying());
}
}
@Override
protected void onPause() {
saveVideoProgress();
isVideoPlayerPrepared = false;
if (videoFragment != null) {
videoFragment.dispose();
videoFragment = null;
}
View container = findViewById(R.id.video_fragment_container);
if (container != null) {
container.setOnTouchListener(null);
}
super.onPause();
}
@Override
protected void onStop() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
View video = findViewById(R.id.video_fragment_container);
if (video != null) {
video.setOnTouchListener(null);
}
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (dataService != null) {
dataService.getAPIAdapter().unregisterUserUpdateListener(userUpdateListener);
dataService = null;
}
if (shareActionProvider != null) {
shareActionProvider.setOnShareTargetSelectedListener(null);
shareActionProvider = null;
}
}
@Override
public void onBackPressed() {
if (isFullscreen() && isBigScreen) {
if (isPortrait()) {
goPortrait();
} else {
goLandscape();
}
} else {
super.onBackPressed();
}
}
private View.OnTouchListener videoTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent e) {
if (getNavVisibility() && e.getAction() == MotionEvent.ACTION_UP && downTime != e.getDownTime()) {
toggleNavVisibility(); // this also removes navHider callbacks.
} else if (!getNavVisibility() && e.getAction() == MotionEvent.ACTION_DOWN) {
downTime = e.getDownTime();
toggleNavVisibility(); // this also removes navHider callbacks.
if (videoFragment != null && videoFragment.isPlaying()) {
handler.postDelayed(navHider, NAV_HIDE_DELAY);
}
}
return false;
}
};
private KAAPIAdapter.UserUpdateListener userUpdateListener = new KAAPIAdapter.UserUpdateListener() {
@Override
public void onUserUpdate(User user) {
Log.d(LOG_TAG, "onUserUpdate");
boolean loggedIn = user != null;
// Look up or refresh the UserVideo.
if (loggedIn && dataService != null) {
try {
Dao<UserVideo, Integer> userVideoDao = dataService.getHelper().getUserVideoDao();
if (userVideo == null || userVideo.getUser() == null) {
Map<String, Object> values = new HashMap<String, Object>();
values.put("user_id", user.getNickname());
values.put("video_id", video.getReadable_id());
List<UserVideo> results = userVideoDao.queryForFieldValues(values);
if (results.size() > 0) {
Log.d(LOG_TAG, String.format("found %d results. setting first", results.size()));
userVideo = results.get(0);
}
} else {
userVideoDao.refresh(userVideo);
}
// Show the points badge.
setPoints(userVideo.getPoints());
} catch (SQLException e) {
e.printStackTrace();
}
} else {
Log.d(LOG_TAG, String.format("user: %s, dataService: %s", loggedIn ? user.getNickname() : "null", dataService));
// User just logged out (or we strangely lost db connectivity since the last update).
userVideo = null;
}
}
};
private void setUserVideo(User user, Video video) {
Log.d(LOG_TAG, String.format("setUserVideo: %s, %s",
user == null ? "null" : user.getNickname(),
video == null ? "null" : video.getReadable_id()));
if (user != null && video != null) {
Dao<UserVideo, Integer> userVideoDao = null;
try {
userVideoDao = getDataService().getHelper().getUserVideoDao();
QueryBuilder<UserVideo, Integer> q = userVideoDao.queryBuilder();
q.orderBy("points", false); // In case any duplicates have slipped in, use highest point total.
q.where().eq("user_id", user.getNickname())
.and().eq("video_id", video.getReadable_id());
PreparedQuery<UserVideo> pq = q.prepare();
userVideo = userVideoDao.queryForFirst(pq);
if (userVideo == null) {
// This is possible if the user is watching this video for the first time.
userVideo = new UserVideo();
userVideo.setUser(user);
userVideo.setVideo_id(video.getReadable_id());
// Better save here, as we need the UserVideo to have an id before we post a progress update.
userVideoDao.create(userVideo);
} else {
Log.d(LOG_TAG, "userVideo exists (" + userVideo.getPoints() + ") last watched: " + userVideo.getLast_watched());
}
} catch (SQLException e) {
// Fail silently if we can't find the UserVideo.
e.printStackTrace();
} catch (ServiceUnavailableException e) {
e.printStackTrace();
}
// Show the points badge.
setPoints(userVideo.getPoints());
} else if (video != null) {
userVideo = new UserVideo();
userVideo.setVideo_id(video.getReadable_id());
}
}
private void setCurrentVideo(String readableId, boolean replace) {
try {
Dao<Video, String> videoDao = getDataService().getHelper().getVideoDao();
QueryBuilder<Video, String> q = videoDao.queryBuilder();
q.where().eq("readable_id", readableId);
video = videoDao.queryForFirst(q.prepare());
if (topicId == null) {
// This *should* never be the case.
topicId = video.getParentTopic().getId();
}
// Grab next video id.
Log.d(LOG_TAG, "looking up next video");
RawRowMapper<Video> mapper = videoDao.getRawRowMapper();
GenericRawResults<Video> results = videoDao.queryRaw(
"select video.* from video,topicvideo where topicvideo.topic_id=? and topicvideo.video_id=video.readable_id and video.seq>? order by video.seq limit 1",
mapper, new String[] {topicId, "" + video.getSeq()});
final Video v = results.getFirstResult();
if (v != null) {
nextVideoId = v.getId();
Log.d(LOG_TAG, " -> " + nextVideoId);
if (mainMenu != null) {
mainMenu.findItem(R.id.menu_next).setEnabled(true).setVisible(true);
}
} else {
Log.d(LOG_TAG, " -> oops");
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ServiceUnavailableException e) {
e.printStackTrace();
}
rightNow.setToNow();
lastPost = (int) (rightNow.toMillis(true) / 1000);
percentLastSaved = 0;
saving = false;
}
private void setupUIForCurrentVideo() {
if (videoFragment != null) {
videoFragment.dispose();
}
Bundle args = new Bundle();
args.putString(Constants.PARAM_VIDEO_ID, videoId);
videoFragment = new VideoFragment();
videoFragment.registerCallbacks(this);
videoFragment.setArguments(args);
FragmentTransaction tx = getFragmentManager().beginTransaction()
.replace(R.id.video_fragment_container, videoFragment, TAG_VIDEO_FRAGMENT);
currentOrientation = getResources().getConfiguration().orientation;
onOrientationChanged(currentOrientation);
if (isPortrait()) {
captionFragment = new CaptionFragment();
captionFragment.registerCallbacks(this);
if (userVideo != null && userVideo.getUser() != null) {
args.putInt(PARAM_USERVIDEO_POINTS, userVideo.getPoints());
}
captionFragment.setArguments(args);
tx.replace(R.id.detail_bottom_container, captionFragment, TAG_CAPTION_FRAGMENT);
}
tx.commit();
}
@Override
public void onVideoPrepared() {
Log.d(LOG_TAG, "onVideoPrepared");
isVideoPlayerPrepared = true;
shouldShowVideoControls = true;
setControlsVisible(getNavVisibility());
videoFragment.seekTo(desiredSeekPosition);
}
@Override
public void onVideoStarted() {
Log.d(LOG_TAG, "onVideoStarted");
rightNow.setToNow();
lastPost = (int) (rightNow.toMillis(true) / 1000);
saveVideoProgress();
handler.removeCallbacks(navHider);
handler.postDelayed(navHider, NAV_HIDE_DELAY);
}
@Override
public void onVideoStopped() {
Log.d(LOG_TAG, "onVideoStopped");
setNavVisibility(true);
saveVideoProgress();
}
@Override
public void onVideoCompleted() {
Log.d(LOG_TAG, "onVideoCompleted");
isVideoPlayerPrepared = false;
}
@Override
public void onPositionUpdate(int ms) {
if (captionFragment != null) {
captionFragment.onPositionUpdate(ms);
}
if (getCurrentUserId() == null) {
return;
}
rightNow.setToNow();
int secondsNow = (int) (rightNow.toMillis(true) / 1000);
boolean enoughTimeHasPassed = secondsNow - lastPost > Constants.LOG_INTERVAL_SECONDS;
float percent = videoFragment.getPercentWatched();
boolean enoughVideoHasPlayed = percent > percentLastSaved + Constants.LOG_INTERVAL_PERCENT;
if (enoughTimeHasPassed && enoughVideoHasPlayed) {
// TODO : respect me
boolean offline = false;
if (!offline) {
postVideoProgress();
}
}
}
@Override
public void onFullscreenToggleRequested() {
if (isFullscreen()) {
if (isPortrait()) {
goPortrait();
} else {
goLandscape();
}
} else {
goFullscreen();
}
}
/**
* Seek video to last watched position.
*/
private void restoreVideoProgress() {
Log.d(LOG_TAG, "restoreVideoProgress");
desiredSeekPosition = 0;
if (userVideo != null) {
int sec = userVideo.getLast_second_watched();
if (video.getDuration() - sec > 1) {
desiredSeekPosition = sec * 1000;
if (isVideoPlayerPrepared) {
videoFragment.seekTo(desiredSeekPosition);
}
}
}
}
/**
* Update userVideo with latest data and save it.
*/
private void saveVideoProgress() {
Log.d(LOG_TAG, "saveVideoProgress");
if (videoFragment != null && userVideo != null && userVideo.getUser() != null) {
int secondsWatched = videoFragment.getClampedSecondsWatchedSince(lastPost);
int lastSecondWatched = videoFragment.getSecondsWatched();
Log.d(LOG_TAG, String.format("last: %d, total: %d", lastSecondWatched, secondsWatched));
desiredSeekPosition = 1000 * lastSecondWatched;
// Update the UserVideo object and save it to the db.
userVideo.setLast_second_watched(lastSecondWatched);
userVideo.setSeconds_watched(secondsWatched);
try {
dataService.getHelper().getUserVideoDao().update(userVideo);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* Post relevant userVideo data to khan servers.
*/
private void postVideoProgress() {
if (!saving) {
Log.d(LOG_TAG, "postVideoProgress");
saving = true;
final float percent = videoFragment.getPercentWatched();
Log.d(LOG_TAG, String.format("%%: %f", percent));
Runnable success = new Runnable() {
public void run() {
rightNow.setToNow();
lastPost = (int) (rightNow.toMillis(true) / 1000);
finishVideoProgressSave(percent);
}
};
Runnable error = new Runnable() {
public void run() {
finishVideoProgressSave(percentLastSaved);
}
};
// Save progress to ensure correct values are on the userVideo before posting.
saveVideoProgress();
// The api adapter causes a user update after posting this.
dataService.getAPIAdapter().postVideoProgress(userVideo, success, error);
}
}
private void finishVideoProgressSave(float percent) {
percentLastSaved = percent;
saving = false;
}
@Override
public void onPositionRequested(int ms) {
if (videoFragment != null) {
videoFragment.seekTo(ms);
}
}
@Override
public void onCaptionsUnavailable() {
Log.d(LOG_TAG, "onCaptionsUnavailable");
if (isBigScreen) {
findViewById(R.id.detail_right_header).setVisibility(View.GONE);
}
}
@Override
public void onCaptionsLoaded() {
if (isBigScreen) {
findViewById(R.id.detail_right_header).setVisibility(View.VISIBLE);
}
}
@Override
public void onDownloadRequested(Video video) {
OfflineVideoManager ovm;
try {
ovm = getDataService().getOfflineVideoManager();
ovm.downloadVideo(video);
} catch (ServiceUnavailableException e) {
e.printStackTrace();
}
}
private String getCurrentUserId() {
return getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.getString(Constants.SETTING_USERID, null);
}
/**
* Get the current user.
*
* @return the user or null.
*/
private User getCurrentUser() {
Log.v(LOG_TAG, "getCurrentUser");
String userid = getCurrentUserId();
if (userid == null) { // no user saved
Log.v(LOG_TAG, " --> null (no id)");
return null;
}
User user = null;
try {
user = getDataService().getHelper().getUserDao().queryForId(userid);
} catch (SQLException e) {
// That's fine; pretend no user was logged in.
e.printStackTrace();
} catch (ServiceUnavailableException e) {
// That's fine; pretend no user was logged in.
e.printStackTrace();
}
Log.v(LOG_TAG, String.format(" --> %s", user == null ? "null" : user.getNickname()));
return user;
}
private boolean isPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
private boolean isFullscreen() {
return isFullscreen;
}
private void launchVideoDetailActivity(String videoId, String topicId) {
// TODO : animate the transition
Intent intent = new Intent(this, VideoDetailActivity.class);
intent.putExtra(PARAM_VIDEO_ID, videoId);
intent.putExtra(PARAM_TOPIC_ID, topicId);
startActivity(intent);
}
private void launchListActivity(String topicId, Class<?> activityClass) {
Intent intent = new Intent(this, activityClass);
intent.putExtra(PARAM_TOPIC_ID, topicId);
// ALWAYS goes to this video's parent topic. If this assumption breaks, then we must rethink the clear_top flag.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
private Intent prepareShareIntent(Video video) {
Intent intent = new Intent(Intent.ACTION_SEND);
String url = video.getKa_url();
intent.putExtra("title", video.getTitle());
intent.putExtra("url", url);
intent.putExtra("desc", video.getDescription());
intent.setType("text/plain");
return intent;
}
private void prepareDownloadActionItem(MenuItem item, int downloadPercent) {
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
int dlRes = R.drawable.av_download;
int removeRes = R.drawable.content_discard;
switch (downloadPercent) {
case PARAM_PROGRESS_DONE:
videoIsDownloaded = true;
item.setEnabled(true).setTitle("Downloaded").setIcon(getResources().getDrawable(removeRes));
break;
case PARAM_PROGRESS_UNKNOWN:
if (video != null) {
try {
dataService.getHelper().getVideoDao().refresh(video);
} catch (SQLException e) {
e.printStackTrace();
}
switch (video.getDownload_status()) {
case Video.DL_STATUS_COMPLETE:
videoIsDownloaded = true;
item.setEnabled(true).setTitle("Downloaded").setIcon(getResources().getDrawable(removeRes));
break;
case Video.DL_STATUS_IN_PROGRESS:
videoIsDownloaded = false;
item.setEnabled(false).setTitle("Downloading").setIcon(getResources().getDrawable(dlRes));
break;
case Video.DL_STATUS_NOT_STARTED:
default:
videoIsDownloaded = false;
item.setEnabled(true).setTitle("Download").setIcon(getResources().getDrawable(dlRes));
}
}
break;
default:
videoIsDownloaded = false;
item.setEnabled(false).setTitle(downloadPercent + "%").setIcon(null);
}
}
private void prepareShareActionItem(MenuItem shareItem) {
shareActionProvider = (ShareActionProvider) shareItem.getActionProvider();
shareActionProvider.setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
shareActionProvider.setOnShareTargetSelectedListener(shareTargetSelectedListener);
shareActionProvider.setShareIntent(prepareShareIntent(video));
}
ShareActionProvider.OnShareTargetSelectedListener shareTargetSelectedListener = new ShareActionProvider.OnShareTargetSelectedListener() {
@Override
public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
Log.d(LOG_TAG, "onShareTargetSelected: " + intent.getStringExtra(Intent.EXTRA_TEXT));
String lowerFqn = intent.getComponent().getClassName().toLowerCase(Locale.US);
// Twitter
if (lowerFqn.contains("twitter") || lowerFqn.contains("tweet")) {
// Twitter clients use the EXTRA_TEXT only, with 140c limit.
int tweetLength = 140;
String tweetFormat = "just learned about \"%s\" (%s) via @khanacademy";
// As an example, the following tweet is 127 characters.
// just learned about 'Lebron Asks: What are the chances of making 10 free throws in a row?' (20 char link is here) via @khanacademy
// Links in tweets take exactly 20 characters (https://support.twitter.com/articles/78124-how-to-post-links-urls-in-tweets).
int linkLength = 20;
int usedLength = tweetFormat.length() - "%s".length() + linkLength; // We know the length the link will take.
usedLength -= "%s".length(); // The title will replace an instance of this string.
// Truncate the title to fit the tweet.
int remainingSpace = tweetLength - usedLength;
String title = intent.getStringExtra("title");
if (title.length() > remainingSpace) {
title = title.substring(0, remainingSpace - 3) + "...";
}
String tweet = String.format(tweetFormat, title, intent.getStringExtra("url"));
intent.putExtra(Intent.EXTRA_TEXT, tweet);
}
else {
String subject = "just learned about " + intent.getStringExtra("title");
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
String bodyFormat = "\"%s\" (%s) is one of nearly 4,000 great educational videos at http://www.khanacademy.org/.";
String body = String.format(bodyFormat, intent.getStringExtra("title"), intent.getStringExtra("url"));
String desc = intent.getStringExtra("desc");
if (desc != null && desc.length() > 0) {
body += "\n\nVideo description: " + desc;
}
intent.putExtra(Intent.EXTRA_TEXT, body);
}
// In docs for this method: "NOTE: Modifying the intent is not permitted and any changes to the latter will be ignored."
// Since we need to modify the intent (that's the whole point of this callback, isn't it?!), we launch the activity ourselves.
// Launch in new task, hopefully avoiding the case where the user already has facebook open to another activity and we don't see the share activity.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mainMenuDelegate = new MainMenuDelegate(this);
mainMenu = menu;
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.video_detail, menu);
MenuItem dlItem = menu.findItem(R.id.menu_download);
prepareDownloadActionItem(dlItem, PARAM_PROGRESS_UNKNOWN);
MenuItem shareItem = menu.findItem(R.id.menu_share);
prepareShareActionItem(shareItem);
if (nextVideoId == null) {
mainMenu.findItem(R.id.menu_next).setVisible(false).setEnabled(false);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
Log.d(LOG_TAG, "onPrepareOptionsMenu");
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(KADataService dataService) {
User user = dataService.getAPIAdapter().getCurrentUser();
boolean show = user != null;
mainMenu.findItem(R.id.menu_logout).setEnabled(show).setVisible(show);
}
});
return true;
}
private void promptAndDeleteDownloadedVideo(final Video video) {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_delete_video))
.setPositiveButton(getString(R.string.button_confirm_delete), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
deleteDownloadedVideo(video);
VideoDetailActivity.this.prepareDownloadActionItem(
mainMenu.findItem(R.id.menu_download), PARAM_PROGRESS_UNKNOWN);
}
})
.setNegativeButton(getString(R.string.button_cancel), null)
.show();
}
private void deleteDownloadedVideo(Video video) {
final Set<Video> toDelete = new HashSet<Video>(1);
toDelete.add(video);
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(KADataService dataService) {
dataService.getOfflineVideoManager().deleteOfflineVideos(toDelete);
Toast.makeText(VideoDetailActivity.this, getString(R.string.msg_deleted), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
android.util.Log.w(LOG_TAG, "onOptionsItemSelected");
Log.d(LOG_TAG, "onOptionsItemSelected");
if (mainMenuDelegate.onOptionsItemSelected(item)) {
return true;
}
switch (item.getItemId()) {
case R.id.menu_next:
if (nextVideoId != null && topicId != null) {
launchVideoDetailActivity(nextVideoId, topicId);
}
return true;
case R.id.menu_logout:
dataService.getAPIAdapter().logout();
return true;
case R.id.menu_download:
if (!videoIsDownloaded) {
onDownloadRequested(video);
} else {
promptAndDeleteDownloadedVideo(video);
}
return true;
case android.R.id.home:
// VideoList for this video's parent topic.
if (isFullscreen() && isBigScreen) {
if (isPortrait()) {
goPortrait();
} else {
goLandscape();
}
} else {
launchListActivity(topicId, VideoListActivity.class);
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
if (config.orientation != currentOrientation) onOrientationChanged(config.orientation);
}
private void onOrientationChanged(final int orientation) {
currentOrientation = orientation;
if (!isFullscreen || !isBigScreen) {
switch (orientation) {
case Configuration.ORIENTATION_LANDSCAPE:
goLandscape();
break;
case Configuration.ORIENTATION_PORTRAIT:
goPortrait();
break;
default:
}
}
}
private void createAndAttachCaptionFragment(int containerId) {
FragmentTransaction tx = getFragmentManager().beginTransaction();
if (captionFragment != null) {
tx.remove(captionFragment);
}
captionFragment = new CaptionFragment();
Bundle args = new Bundle();
if (video != null) {
args.putString(Constants.PARAM_VIDEO_ID, video.getId());
if (videoFragment != null) {
args.putInt(PARAM_VIDEO_POSITION, videoFragment.getVideoPosition());
}
}
// Set args even if empty to avoid possible NPE inside CaptionFragment.
captionFragment.setArguments(args);
captionFragment.registerCallbacks(this);
tx.replace(containerId, captionFragment);
tx.commit();
// Force execute, so we can populateHeader afterward.
getFragmentManager().executePendingTransactions();
}
private void createAndAttachHeader() {
FrameLayout container = (FrameLayout) findViewById(R.id.detail_header_container);
if (headerView == null) {
headerView = getLayoutInflater().inflate(R.layout.video_header, container, true);
populateHeader();
pointsView = (TextView) findViewById(R.id.video_points);
if (userVideo != null && userVideo.getUser() != null) {
setPoints(userVideo.getPoints());
}
}
}
private void populateHeader() {
View headerView = findViewById(R.id.video_header);
Log.d(LOG_TAG, "populateHeader: header is " + (headerView == null ? "null" : "not null"));
if (video != null && headerView != null) {
((TextView) headerView.findViewById(R.id.video_title)).setText(video.getTitle());
String desc = video.getDescription();
TextView descView = (TextView) headerView.findViewById(R.id.video_description);
if (desc != null && desc.length() > 0) {
descView.setText(desc);
descView.setVisibility(View.VISIBLE);
descView.setMovementMethod(new ScrollingMovementMethod());
} else {
descView.setVisibility(View.GONE);
}
}
}
private void goFullscreen() {
goFullscreen(true);
}
private void goFullscreen(boolean force) {
isFullscreen = true;
setRequestedOrientation(force ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
VideoController videoControls = (VideoController) findViewById(R.id.controller);
if (videoControls != null) {
videoControls.setFullscreen(true);
}
ThumbnailWrapper videoContainer = (ThumbnailWrapper) findViewById(R.id.video_fragment_container);
videoContainer.setMaintainAspectRatio(false);
if (captionFragment != null) {
FragmentTransaction tx = getFragmentManager().beginTransaction();
tx.remove(captionFragment);
tx.commit();
}
findViewById(R.id.detail_bottom_container).setVisibility(View.GONE);
if (isBigScreen) {
findViewById(R.id.detail_right_container).setVisibility(View.GONE);
findViewById(R.id.detail_center_divider).setVisibility(View.GONE);
}
setNavVisibility(videoFragment == null || !videoFragment.isPlaying());
getDecorViewTreeObserver().addOnGlobalLayoutListener(layoutFixer);
}
private void goLandscape() {
if (isBigScreen) {
goLargeLandscape();
} else {
goFullscreen(false);
}
}
private void goLargeLandscape() {
isFullscreen = false;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
VideoController videoControls = (VideoController) findViewById(R.id.controller);
if (videoControls != null) {
videoControls.setFullscreen(false);
}
ThumbnailWrapper videoContainer = (ThumbnailWrapper) findViewById(R.id.video_fragment_container);
videoContainer.setMaintainAspectRatio(true);
FrameLayout headerContainer = (FrameLayout) findViewById(R.id.detail_header_container);
LinearLayout.LayoutParams p = (LinearLayout.LayoutParams) headerContainer.getLayoutParams();
p.weight = 1;
headerContainer.setLayoutParams(p);
headerContainer.setVisibility(View.VISIBLE);
FrameLayout emptyContainer = (FrameLayout) findViewById(R.id.detail_bottom_container);
p = (LinearLayout.LayoutParams) emptyContainer.getLayoutParams();
p.weight = 0;
emptyContainer.setLayoutParams(p);
emptyContainer.setVisibility(View.GONE);
findViewById(R.id.detail_right_container).setVisibility(View.VISIBLE);
createAndAttachCaptionFragment(R.id.detail_right_caption_container);
createAndAttachHeader();
findViewById(R.id.detail_center_divider).setVisibility(View.VISIBLE);
getDecorViewTreeObserver().addOnGlobalLayoutListener(layoutFixer);
}
private void goPortrait() {
isFullscreen = false;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
VideoController videoControls = (VideoController) findViewById(R.id.controller);
if (videoControls != null) {
videoControls.setFullscreen(false);
}
ThumbnailWrapper videoContainer = (ThumbnailWrapper) findViewById(R.id.video_fragment_container);
videoContainer.setMaintainAspectRatio(true);
FrameLayout headerContainer = (FrameLayout) findViewById(R.id.detail_header_container);
LinearLayout.LayoutParams p = (LinearLayout.LayoutParams) headerContainer.getLayoutParams();
p.weight = 0;
headerContainer.setLayoutParams(p);
createAndAttachCaptionFragment(R.id.detail_bottom_container);
FrameLayout captionContainer = (FrameLayout) findViewById(R.id.detail_bottom_container);
captionContainer.setVisibility(View.VISIBLE);
p = (LinearLayout.LayoutParams) captionContainer.getLayoutParams();
p.weight = 1;
captionContainer.setLayoutParams(p);
if (isBigScreen) {
findViewById(R.id.detail_right_container).setVisibility(View.GONE);
findViewById(R.id.detail_center_divider).setVisibility(View.GONE);
}
setNavVisibility(true);
createAndAttachHeader();
getDecorViewTreeObserver().addOnGlobalLayoutListener(layoutFixer);
}
public void setPoints(int points) {
Log.d(LOG_TAG, "setPoints (" + points + ")");
if (pointsView == null) {
return;
}
if (points == POINTS_GONE) {
pointsView.setVisibility(View.GONE);
} else {
pointsView.setText(String.format("%d", points));
pointsView.setVisibility(View.VISIBLE);
}
}
private ViewTreeObserver getDecorViewTreeObserver() {
ViewTreeObserver decorViewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
return decorViewTreeObserver;
}
int previousActionBarHeight = -1;
// Runs after orientation changes to push the portrait views down below the overlaid actionbar. Running this code
// directly in onOrientationChanged caused it to fail the first time we entered portrait orientation.
private ViewTreeObserver.OnGlobalLayoutListener layoutFixer = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (isFullscreen()) {
final View containerView = findViewById(R.id.detail_pane_container);
final FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) containerView.getLayoutParams();
p.setMargins(0, 0, 0, 0);
containerView.setLayoutParams(p);
previousActionBarHeight = 0;
} else {
getActionBar().show();
int actionBarHeight = getActionBar().getHeight();
// Action bar height changes between landscape and portrait on Fire HD (!)
if (actionBarHeight != previousActionBarHeight) {
// First one fails, second one works. (4.2.1)
// p.topMargin = getActionBar().getHeight();
// p.setMargins(0, getActionBar().getHeight(), 0, 0);
final View containerView = findViewById(R.id.detail_pane_container);
final FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) containerView.getLayoutParams();
p.setMargins(0, actionBarHeight, 0, 0);
containerView.setLayoutParams(p);
previousActionBarHeight = actionBarHeight;
}
}
getDecorViewTreeObserver().removeGlobalOnLayoutListener(this);
}
};
private void toggleNavVisibility() {
setNavVisibility(!getNavVisibility());
}
private boolean getNavVisibility() {
return navVis;
}
private void setNavVisibility(boolean visible) {
Log.d(LOG_TAG, "setNavVisibility: " + visible);
// Fire HD:
// View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN : These do nothing.
// View.SYSTEM_UI_FLAG_HIDE_NAVIGATION : This works as expected, but isn't good enough.
// WindowManager.LayoutParams.FLAG_FULLSCREEN : This is great, but
// a) can't keep stable layout (video grows when added, shrinks when removed),
// b) No way to hook the event where the user touches the handle, so that behavior differs from a touch on the rest of the screen.
handler.removeCallbacks(navHider);
navVis = visible;
if (visible) {
// Flags, menu items, and action bar should already be correct in portrait, but it won't hurt.
findViewById(R.id.detail_pane_container).setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getActionBar().show();
if (mainMenu != null) {
for (int i=0; i<mainMenu.size(); ++i) {
mainMenu.getItem(i).setVisible(true);
}
}
setControlsVisible(shouldShowVideoControls);
if (videoFragment != null && videoFragment.isPlaying()) {
handler.postDelayed(navHider, NAV_HIDE_DELAY);
}
} else {
if (isFullscreen()) {
findViewById(R.id.detail_pane_container).setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getActionBar().hide();
// Hide menu items in landscape to prevent the action overflow menu / share menu from
// disappearing on nav hide when the user is trying to interact with them.
// The right solution would be to force the nav to stay open while the menus are open,
// but there doesn't appear to be a way to check (?!). We get NO call to onOptionsMenuClosed
// when that is overridden, so we cannot track it ourselves either. In fact, if the options menu
// is open it consumes the first touch event outside itself to close itself, so we can't even
// do it by listening to touches and assuming they close the menu.
if (mainMenu != null) {
for (int i=0; i<mainMenu.size(); ++i) {
mainMenu.getItem(i).setVisible(false);
}
}
}
setControlsVisible(false);
}
}
private void setControlsVisible(boolean visible) {
VideoController videoControls = (VideoController) findViewById(R.id.controller);
if (videoControls != null) {
if (visible) {
videoControls.show();
} else {
videoControls.hide();
}
}
}
private Runnable navHider = new Runnable() {
@Override
public void run() {
if (!isFinishing()) {
setNavVisibility(false);
}
}
};
}