/*
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 java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import android.app.DownloadManager;
import android.app.Fragment;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.format.Time;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.VideoView;
import com.concentricsky.android.khan.R;
import com.concentricsky.android.khanacademy.Constants;
import com.concentricsky.android.khanacademy.data.KADataService;
import com.concentricsky.android.khanacademy.data.db.Video;
import com.concentricsky.android.khanacademy.util.Log;
import com.concentricsky.android.khanacademy.util.ObjectCallback;
import com.concentricsky.android.khanacademy.views.VideoController;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.stmt.QueryBuilder;
/**
* This holds a Video model and plays its video.
*
* Offers a callback interface for start/stop events, position updates, and fullscreen requests.
*/
public class VideoFragment extends Fragment
implements VideoController.Callbacks {
public static final String LOG_TAG = VideoFragment.class.getSimpleName();
public interface Callbacks {
public void onVideoStarted();
public void onVideoStopped();
public void onVideoPrepared();
public void onVideoCompleted();
public void onPositionUpdate(int ms);
public void onFullscreenToggleRequested();
}
private Handler handler = new Handler();
private VideoView videoView;
private VideoController controls;
private Video video;
private ProgressBar loadingIndicator;
private View errorView;
private TextView errorText;
private String errorMissingVideo;
private final List<Callbacks> callbacks = new ArrayList<Callbacks>();
private class PositionUpdater implements Runnable {
private boolean running = true;
@Override
public void run() {
if (videoView != null) {
doPositionUpdate(videoView.getCurrentPosition());
}
if (running) {
handler.postDelayed(this, Constants.POSITION_UPDATE_DELAY);
}
}
public void stop() {
running = false;
handler.removeCallbacks(this);
}
public void start() {
running = true;
handler.removeCallbacks(this);
handler.post(this);
}
};
private PositionUpdater positionUpdater = new PositionUpdater();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
errorMissingVideo = getString(R.string.err_video_unavailable);
}
@Override
public void onDestroy() {
unregisterCallbacks();
super.onDestroy();
}
/*
* Errors: (returnCode, errorCode)
*
* https://github.com/android/platform_external_opencore/blob/master/pvmi/pvmf/include/pvmf_return_codes.h
*
* (1,-12) overflow
* (100, 0)
* (1, -2147483648) // read in a so post this might be invalid stream type
* // or Content-Length header > MAX_INT: http://code.google.com/p/android/issues/detail?id=8624
* (-38, 0) // http://lab-programming.blogspot.com/2012/01/how-to-work-around-android-mediaplayer.html
* (1, -1004) // io error, possibly 500 response, see http://stackoverflow.com/a/8244780/931277
* // Got this one when seeking past the downloaded portion of a partially finished download.
*
* Names of some codes between -1000 and -1014: http://android.joao.jp/2011/07/mediaplayer-errors.html
* Interesting - says to close AssetFileDescriptor; may be similar with ParcelFileDescriptor, eh? http://stackoverflow.com/a/11069306/931277
*
* Out of memory with about 20 video detail views stacked up.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle savedInstanceState) {
ViewGroup container = (ViewGroup) inflater.inflate(R.layout.fragment_video, root, false);
videoView = (VideoView) container.findViewById(R.id.videoView);
controls = (VideoController) container.findViewById(R.id.controller);
controls.setVideoView(videoView);
controls.setCallbacks(this);
controls.setFullscreenRequestHandler(new VideoController.FullscreenRequestHandler() {
@Override
public void onFullscreenToggleRequested() {
for (Callbacks c : callbacks) {
c.onFullscreenToggleRequested();
}
}
});
videoView.setOnCompletionListener(controls);
loadingIndicator = new ProgressBar(getActivity());
RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
p.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
container.addView(loadingIndicator, p);
loadingIndicator.setVisibility(View.GONE);
errorView = inflater.inflate(R.layout.missing_video, container, false);
errorText = (TextView) errorView.findViewById(R.id.text_missing_video);
container.addView(errorView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
errorView.setVisibility(View.GONE);
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
int seekTo = getArguments().getInt(Constants.PARAM_VIDEO_POSITION, 0);
boolean playing = getArguments().getBoolean(Constants.PARAM_VIDEO_PLAY_STATE, false);
seekTo(seekTo);
loadingIndicator.setVisibility(View.GONE);
controls.onPrepared(mp);
for (Callbacks c : callbacks) {
c.onVideoPrepared();
}
if (playing) {
controls.play();
}
}
});
positionUpdater.start();
return container;
// Setting the following listener causes a crash on Fire HD. For some reason, orientation changes occasionally (usually within about 20 tries)
// yield the following stack trace, then the device reboots.
/*
* 01-09 17:02:33.537: W/HardwareRenderer(3319): EGL error: EGL_BAD_NATIVE_WINDOW
01-09 17:02:33.576: W/HardwareRenderer(3319): Mountain View, we've had a problem here. Switching back to software rendering.
01-09 17:02:33.584: E/ViewRootImpl(3319): IllegalArgumentException locking surface
01-09 17:02:33.584: E/ViewRootImpl(3319): java.lang.IllegalArgumentException
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.view.Surface.lockCanvasNative(Native Method)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.view.Surface.lockCanvas(Surface.java:76)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.view.ViewRootImpl.draw(ViewRootImpl.java:1959)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1647)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2462)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.os.Handler.dispatchMessage(Handler.java:99)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.os.Looper.loop(Looper.java:137)
01-09 17:02:33.584: E/ViewRootImpl(3319): at android.app.ActivityThread.main(ActivityThread.java:4486)
01-09 17:02:33.584: E/ViewRootImpl(3319): at java.lang.reflect.Method.invokeNative(Native Method)
01-09 17:02:33.584: E/ViewRootImpl(3319): at java.lang.reflect.Method.invoke(Method.java:511)
01-09 17:02:33.584: E/ViewRootImpl(3319): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
01-09 17:02:33.584: E/ViewRootImpl(3319): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
01-09 17:02:33.584: E/ViewRootImpl(3319): at dalvik.system.NativeStart.main(Native Method)
01-09 17:02:34.654: E/InputQueue-JNI(3319): channel '4181fe60 com.concentricsky.android.khanacademy/com.concentricsky.android.khanacademy.app.VideoListActivity (client)' ~ Publisher closed input channel or an error occurred. events=0x8
01-09 17:02:34.662: E/InputQueue-JNI(3319): channel '41841178 com.concentricsky.android.khanacademy/com.concentricsky.android.khanacademy.app.VideoDetailActivity (client)' ~ Publisher closed input channel or an error occurred. events=0x8
01-09 17:02:34.662: E/InputQueue-JNI(3319): channel '417e04b0 com.concentricsky.android.khanacademy/com.concentricsky.android.khanacademy.app.HomeActivity (client)' ~ Publisher closed input channel or an error occurred. events=0x8
*/
// This happens even without the call to `error`, which shows a Toast.
// Looking at the 4.0.3 source on Grepcode, I cannot see how this listener could cause such a problem. I can only guess there is a bug in Amazon's
// VideoView implementation. The one thing that Google's implementation does before calling the provided listener is to set VideoView#mCurrentState
// to STATE_ERROR. It's possible that setting an error listener overwrites the default one in Amazon's implementation, removing some critical piece
// of functionality. Just a guess.
// videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
// @Override
// public boolean onError(MediaPlayer mp, int what, int extra) {
// Log.e(LOG_TAG, String.format("Error: (%d,%d)", what, extra));
// switch (what) {
// case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:
// case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
// case MediaPlayer.MEDIA_ERROR_UNKNOWN:
//// error(errorVideoPlayback);
// }
// return false;
// }
// });
}
@Override
public void onPause() {
super.onPause();
positionUpdater.stop();
}
@Override
public void onResume() {
if (positionUpdater != null) {
positionUpdater.start();
}
super.onResume();
}
@Override
public void onDestroyView() {
Log.d(LOG_TAG, "onDestroyView");
dispose();
super.onDestroyView();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.v(LOG_TAG, ".onActivityCreated");
final String videoId = getArguments().getString(Constants.PARAM_VIDEO_ID);
Log.d(LOG_TAG, " videoId is " + videoId);
((KADataService.Provider) getActivity()).requestDataService(
new ObjectCallback<KADataService>() {
@Override
public void call(KADataService dataService) {
try {
Dao<Video, String> videoDao = dataService.getHelper().getVideoDao();
QueryBuilder<Video, String> q = videoDao.queryBuilder();
q.where().eq("readable_id", videoId);
Video video = videoDao.queryForFirst(q.prepare());
setVideo(video);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
);
}
/**
* Set the current video. This
* @param video
*/
private void setVideo(Video video) {
this.video = video;
String url = null;
DownloadManager dlm = (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query q = new DownloadManager.Query();
q.setFilterById(video.getDlm_id());
q.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
Cursor c = dlm.query(q);
if (c.moveToFirst()) {
String filename = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
if (new File(filename).exists()) {
url = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
}
}
c.close();
if (url == null) {
url = video.getMp4url();
}
if (url == null) {
url = video.getM3u8url();
}
if (url != null) {
Log.d(LOG_TAG, "setVideo: " + url);
loadingIndicator.setVisibility(View.VISIBLE);
videoView.setVideoURI(Uri.parse(url));
} else {
error(errorMissingVideo);
}
}
private void error(String msg) {
if (loadingIndicator != null) {
loadingIndicator.setVisibility(View.GONE);
}
if (videoView != null) {
videoView.stopPlayback();
}
if (errorText != null) {
errorText.setText(msg);
errorView.setVisibility(View.VISIBLE);
}
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
}
public boolean isPlaying() {
if (videoView == null) {
return false;
}
return videoView.isPlaying() && videoView.getCurrentPosition() < videoView.getDuration();
}
public int getVideoPosition() {
if (videoView == null) {
return 0;
}
return videoView.getCurrentPosition();
}
public void stop() {
if (videoView != null) {
videoView.stopPlayback();
}
}
public void play() {
if (controls != null) {
controls.play();
}
}
public void seekTo(int position) {
if (videoView != null) {
videoView.seekTo(position);
if (controls != null) {
controls.updateBar();
}
}
}
private void doPositionUpdate(int ms) {
for (Callbacks c : callbacks) {
c.onPositionUpdate(ms);
}
}
public void registerCallbacks(Callbacks c) {
callbacks.add(c);
}
public void unregisterCallbacks() {
callbacks.clear();
}
public void unregisterCallbacks(Callbacks c) {
callbacks.remove(c);
}
public void onPositionRequested(int ms) {
if (videoView != null) {
int curr = videoView.getCurrentPosition();
if (ms > curr && videoView.canSeekForward() ||
ms < curr && videoView.canSeekBackward()) {
seekTo(ms);
}
// controls.show();
}
}
public void dispose() {
Log.d(LOG_TAG, "dispose");
stop();
if (controls != null) {
controls.setCallbacks(null);
controls.setFullscreenRequestHandler(null);
controls.setVideoView(null);
controls = null;
}
if (videoView != null) {
videoView.setMediaController(null);
videoView.setOnCompletionListener(null);
videoView.setOnPreparedListener(null);
videoView.setOnErrorListener(null);
if (videoView.isPlaying()) {
videoView.stopPlayback();
}
videoView = null;
}
unregisterCallbacks();
}
public float getPercentWatched() {
if (video == null || videoView == null) {
return 0;
}
int posSeconds = getSecondsWatched();
return (float) posSeconds / video.getDuration() * 100;
}
public int getSecondsWatched() {
if (video == null || videoView == null) {
return 0;
}
return videoView.getCurrentPosition() / 1000;
}
public int getClampedSecondsWatchedSince(int since) {
Time now = new Time();
now.setToNow();
int secondsNow = (int) (now.toMillis(true) / 1000);
int maxSeconds = Math.max(secondsNow - since, 0);
return Math.min(maxSeconds, getSecondsWatched());
}
@Override
public void onVideoStarted() {
for (Callbacks c : callbacks) {
c.onVideoStarted();
}
}
@Override
public void onVideoStopped() {
for (Callbacks c : callbacks) {
c.onVideoStopped();
}
}
@Override
public void onVideoCompleted() {
for (Callbacks c : callbacks) {
c.onVideoCompleted();
}
}
}