/* * Copyright (C) 2012 Simon Robinson * * This file is part of Com-Me. * * Com-Me is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Com-Me 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 Lesser General * Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Com-Me. * If not, see <http://www.gnu.org/licenses/>. */ package ac.robinson.mediatablet.activity; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import ac.robinson.mediatablet.MediaTablet; import ac.robinson.mediatablet.MediaViewerActivity; import ac.robinson.mediatablet.R; import ac.robinson.mediautilities.FrameMediaContainer; import ac.robinson.mediautilities.SMILUtilities; import ac.robinson.util.BitmapUtilities; import ac.robinson.util.DebugUtilities; import ac.robinson.util.IOUtilities; import ac.robinson.util.UIUtilities; import ac.robinson.view.AutoResizeTextView; import ac.robinson.view.CustomMediaController; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; import android.media.SoundPool; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.RelativeLayout; import com.larvalabs.svgandroid.SVGParser; public class NarrativeViewerActivity extends MediaViewerActivity { private final int EXTRA_AUDIO_ITEMS = 2; // 3 audio items max, but only 2 for sound pool (other is in MediaPlayer) private SoundPool mSoundPool; private ArrayList<Integer> mFrameSounds; private int mNumExtraSounds; private boolean mMediaPlayerPrepared; private boolean mSoundPoolPrepared; private AssetFileDescriptor mSilenceFileDescriptor = null; private boolean mSilenceFilePlaying; private long mPlaybackStartTime; private long mPlaybackPauseTime; private MediaPlayer mMediaPlayer; private boolean mMediaPlayerError; private boolean mHasPlayed; private boolean mIsLoading; private CustomMediaController mMediaController; private ArrayList<FrameMediaContainer> mNarrativeContentList; private int mNarrativeDuration; private int mPlaybackPosition; private int mInitialPlaybackOffset; private int mNonAudioOffset; private FrameMediaContainer mCurrentFrameContainer; private Bitmap mAudioPictureBitmap = null; @Override protected void initialiseView(Bundle savedInstanceState) { setContentView(R.layout.narrative_viewer); mIsLoading = false; mMediaPlayerError = false; // load previous state on screen rotation mHasPlayed = false; // will begin playing if not playing already; used to stop unplayable narratives mPlaybackPosition = -1; if (savedInstanceState != null) { // mIsPlaying = savedInstanceState.getBoolean(getString(R.string.extra_is_playing)); mPlaybackPosition = savedInstanceState.getInt(getString(R.string.extra_playback_position)); mInitialPlaybackOffset = savedInstanceState.getInt(getString(R.string.extra_playback_offset)); mNonAudioOffset = savedInstanceState.getInt(getString(R.string.extra_playback_non_audio_offset)); } } @Override public void onSaveInstanceState(Bundle savedInstanceState) { // savedInstanceState.putBoolean(getString(R.string.extra_is_playing), mIsPlaying); savedInstanceState.putInt(getString(R.string.extra_playback_position), mPlaybackPosition); savedInstanceState.putInt(getString(R.string.extra_playback_offset), mMediaPlayerController.getCurrentPosition() - mPlaybackPosition); savedInstanceState.putInt(getString(R.string.extra_playback_non_audio_offset), mNonAudioOffset); super.onSaveInstanceState(savedInstanceState); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { if (!mHasPlayed) { preparePlayback(); } else { if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { // don't hide the controller if we're paused showMediaController(CustomMediaController.DEFAULT_VISIBILITY_TIMEOUT); } } } else { showMediaController(-1); // so if we're interacting with an overlay we don't constantly hide/show } } @Override protected void onPause() { super.onPause(); pauseMediaController(); } @Override protected void onDestroy() { releasePlayer(); super.onDestroy(); } @Override public boolean onOptionsItemSelected(MenuItem item) { pauseMediaController(); return super.onOptionsItemSelected(item); } private void preparePlayback() { if (mNarrativeContentList != null && mNarrativeContentList.size() > 0 && mMediaPlayer != null && mSoundPool != null && mMediaController != null && mPlaybackPosition >= 0) { return; // no need to re-initialise } // TODO: lazily load mNarrativeContentList = SMILUtilities.getSMILFrameList(getCurrentMediaFile(), 1, false, 0, false); if (mNarrativeContentList == null || mNarrativeContentList.size() <= 0) { UIUtilities.showToast(NarrativeViewerActivity.this, R.string.error_loading_narrative_player); finish(); return; } String startFrameId = mNarrativeContentList.get(0).mFrameId; // first launch boolean updatePosition = mPlaybackPosition < 0; if (updatePosition) { mInitialPlaybackOffset = 0; mNonAudioOffset = 0; } mNarrativeDuration = 0; for (FrameMediaContainer container : mNarrativeContentList) { if (updatePosition && startFrameId.equals(container.mFrameId)) { updatePosition = false; mPlaybackPosition = mNarrativeDuration; } mNarrativeDuration += container.mFrameMaxDuration; } if (mPlaybackPosition < 0) { mPlaybackPosition = 0; mInitialPlaybackOffset = 0; mNonAudioOffset = 0; } mCurrentFrameContainer = getMediaContainer(mPlaybackPosition, true); releasePlayer(); mMediaPlayer = new MediaPlayer(); mSoundPool = new SoundPool(EXTRA_AUDIO_ITEMS, AudioManager.STREAM_MUSIC, 100); mFrameSounds = new ArrayList<Integer>(); mMediaController = new CustomMediaController(this); RelativeLayout parentLayout = (RelativeLayout) findViewById(R.id.narrative_playback_container); RelativeLayout.LayoutParams controllerLayout = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); controllerLayout.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); controllerLayout.setMargins(0, 0, 0, getResources().getDimensionPixelSize(R.dimen.button_padding)); parentLayout.addView(mMediaController, controllerLayout); mMediaController.setAnchorView(findViewById(R.id.image_playback)); showMediaController(CustomMediaController.DEFAULT_VISIBILITY_TIMEOUT); // (can use 0 for permanent visibility) mHasPlayed = true; prepareMediaItems(mCurrentFrameContainer); } public void handleButtonClicks(View currentButton) { pauseMediaController(); super.handleButtonClicks(currentButton); } public void handleNarrativeClicks(View currentButton) { showMediaController(CustomMediaController.DEFAULT_VISIBILITY_TIMEOUT); } private void showMediaController(int timeout) { if (mMediaController != null) { if (!mMediaController.isShowing() || timeout <= 0) { mMediaController.show(timeout); } else { mMediaController.refreshShowTimeout(); } } } private void makeMediaItemsVisible(boolean mediaControllerIsShowing) { // make sure the text view is visible above the playback bar Resources res = getResources(); int mediaControllerHeight = res.getDimensionPixelSize(R.dimen.media_controller_height); if (mCurrentFrameContainer != null && mCurrentFrameContainer.mImagePath != null) { AutoResizeTextView textView = (AutoResizeTextView) findViewById(R.id.text_playback); RelativeLayout.LayoutParams textLayout = (RelativeLayout.LayoutParams) textView.getLayoutParams(); int textPadding = res.getDimensionPixelSize(R.dimen.playback_text_padding); textLayout.setMargins(0, 0, 0, (mediaControllerIsShowing ? mediaControllerHeight : textPadding)); textView.setLayoutParams(textLayout); } } private void pauseMediaController() { mMediaPlayerController.pause(); showMediaController(-1); // to keep on showing until done here UIUtilities.releaseKeepScreenOn(getWindow()); } private FrameMediaContainer getMediaContainer(int narrativePlaybackPosition, boolean updatePlaybackPosition) { mIsLoading = true; int currentPosition = 0; for (FrameMediaContainer container : mNarrativeContentList) { int newPosition = currentPosition + container.mFrameMaxDuration; if (narrativePlaybackPosition >= currentPosition && narrativePlaybackPosition < newPosition) { if (updatePlaybackPosition) { mPlaybackPosition = currentPosition; } return container; } currentPosition = newPosition; } return null; } private void prepareMediaItems(FrameMediaContainer container) { // load the audio for the media player Resources res = getResources(); mSoundPoolPrepared = false; mMediaPlayerPrepared = false; mMediaPlayerError = false; mNonAudioOffset = 0; unloadSoundPool(); mSoundPool.setOnLoadCompleteListener(mSoundPoolLoadListener); mNumExtraSounds = 0; String currentAudioItem = null; boolean soundPoolAllowed = !DebugUtilities.hasSoundPoolBug(); for (int i = 0, n = container.mAudioDurations.size(); i < n; i++) { if (container.mAudioDurations.get(i).intValue() == container.mFrameMaxDuration) { currentAudioItem = container.mAudioPaths.get(i); } else { // playing *anything* in SoundPool at the same time as MediaPlayer crashes on Galaxy Tab if (soundPoolAllowed) { mSoundPool.load(container.mAudioPaths.get(i), 1); mNumExtraSounds += 1; } } } if (mNumExtraSounds == 0) { mSoundPoolPrepared = true; } FileInputStream playerInputStream = null; mSilenceFilePlaying = false; boolean dataLoaded = false; int dataLoadingErrorCount = 0; while (!dataLoaded && dataLoadingErrorCount <= 2) { try { mMediaPlayer.reset(); if (currentAudioItem == null || (!(new File(currentAudioItem).exists()))) { mSilenceFilePlaying = true; if (mSilenceFileDescriptor == null) { mSilenceFileDescriptor = res.openRawResourceFd(R.raw.silence_100ms); } mMediaPlayer.setDataSource(mSilenceFileDescriptor.getFileDescriptor(), mSilenceFileDescriptor.getStartOffset(), mSilenceFileDescriptor.getDeclaredLength()); } else { // can't play from data directory (they're private; permissions don't work), must use an input // stream - original was: mMediaPlayer.setDataSource(currentAudioItem); playerInputStream = new FileInputStream(new File(currentAudioItem)); mMediaPlayer.setDataSource(playerInputStream.getFD()); } dataLoaded = true; } catch (Throwable t) { // sometimes setDataSource fails for mysterious reasons - loop to open it, rather than failing dataLoaded = false; dataLoadingErrorCount += 1; } finally { IOUtilities.closeStream(playerInputStream); } } try { if (dataLoaded) { mMediaPlayer.setLooping(false); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setOnPreparedListener(mMediaPlayerPreparedListener); // mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener); // done later - better pausing mMediaPlayer.setOnErrorListener(mMediaPlayerErrorListener); mMediaPlayer.prepareAsync(); } else { throw new IllegalStateException(); } } catch (Throwable t) { UIUtilities.showToast(NarrativeViewerActivity.this, R.string.error_loading_narrative_player); finish(); return; } // load the image ImageView photoDisplay = (ImageView) findViewById(R.id.image_playback); if (container.mImagePath != null && new File(container.mImagePath).exists()) { Bitmap scaledBitmap = BitmapUtilities.loadAndCreateScaledBitmap(container.mImagePath, photoDisplay.getWidth(), photoDisplay.getHeight(), BitmapUtilities.ScalingLogic.FIT, true); photoDisplay.setImageBitmap(scaledBitmap); photoDisplay.setScaleType(ScaleType.CENTER_INSIDE); } else if (TextUtils.isEmpty(container.mTextContent)) { // no text and no image: audio icon if (mAudioPictureBitmap == null) { mAudioPictureBitmap = SVGParser.getSVGFromResource(res, R.raw.ic_audio_playback).getBitmap( photoDisplay.getWidth(), photoDisplay.getHeight()); } photoDisplay.setImageBitmap(mAudioPictureBitmap); photoDisplay.setScaleType(ScaleType.FIT_CENTER); } else { photoDisplay.setImageDrawable(null); } // load the text AutoResizeTextView textView = (AutoResizeTextView) findViewById(R.id.text_playback); if (!TextUtils.isEmpty(container.mTextContent)) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.playback_text_size)); textView.setText(container.mTextContent); RelativeLayout.LayoutParams textLayout = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); textLayout.addRule(RelativeLayout.CENTER_HORIZONTAL); int textViewHeight = res.getDimensionPixelSize(R.dimen.media_controller_height); int textViewPadding = res.getDimensionPixelSize(R.dimen.playback_text_padding); if (container.mImagePath != null) { textView.setMaxHeight(res.getDimensionPixelSize(R.dimen.playback_maximum_text_height_with_image)); textLayout.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); textLayout.setMargins(0, 0, 0, (mMediaController.isShowing() ? textViewHeight : textViewPadding)); textView.setBackgroundResource(R.drawable.rounded_playback_text); textView.setTextColor(res.getColor(R.color.icon_text_with_image)); } else { textView.setMaxHeight(photoDisplay.getHeight()); // no way to clear, so set to parent height textLayout.addRule(RelativeLayout.CENTER_VERTICAL); textLayout.setMargins(0, 0, 0, textViewPadding); textView.setBackgroundColor(res.getColor(android.R.color.transparent)); textView.setTextColor(res.getColor(R.color.icon_text_no_image)); } textView.setLayoutParams(textLayout); textView.setVisibility(View.VISIBLE); } else { textView.setVisibility(View.GONE); } } private void unloadSoundPool() { for (Integer soundId : mFrameSounds) { mSoundPool.stop(soundId); mSoundPool.unload(soundId); } mFrameSounds.clear(); } private void releasePlayer() { UIUtilities.releaseKeepScreenOn(getWindow()); // release controller first, so we don't play to a null player if (mMediaController != null) { mMediaController.hide(); ((RelativeLayout) findViewById(R.id.narrative_playback_container)).removeView(mMediaController); mMediaController.setMediaPlayer(null); mMediaController = null; } if (mMediaPlayer != null) { try { mMediaPlayer.stop(); } catch (IllegalStateException e) { } mMediaPlayer.release(); mMediaPlayer = null; } if (mSoundPool != null) { unloadSoundPool(); mSoundPool.release(); mSoundPool = null; } } private CustomMediaController.MediaPlayerControl mMediaPlayerController = new CustomMediaController.MediaPlayerControl() { @Override public void start() { mPlaybackPauseTime = -1; if (mPlaybackPosition < 0) { // so we return to the start when playing from the end mPlaybackPosition = 0; mInitialPlaybackOffset = 0; mNonAudioOffset = 0; mCurrentFrameContainer = getMediaContainer(mPlaybackPosition, true); prepareMediaItems(mCurrentFrameContainer); } else { if (mMediaPlayer != null && mSoundPool != null) { mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener); mPlaybackStartTime = System.currentTimeMillis() - mMediaPlayer.getCurrentPosition(); mMediaPlayer.start(); mSoundPool.autoResume(); // TODO: check this works showMediaController(CustomMediaController.DEFAULT_VISIBILITY_TIMEOUT); } else { UIUtilities.showToast(NarrativeViewerActivity.this, R.string.error_loading_narrative_player); finish(); return; } } UIUtilities.acquireKeepScreenOn(getWindow()); } @Override public void pause() { mIsLoading = false; if (mPlaybackPauseTime < 0) { // save the time paused, but don't overwrite if we call pause multiple times mPlaybackPauseTime = System.currentTimeMillis(); } if (mMediaPlayer != null) { mMediaPlayer.setOnCompletionListener(null); // make sure we don't continue accidentally mMediaPlayer.pause(); } if (mSoundPool != null) { mSoundPool.autoPause(); // TODO: check this works } showMediaController(-1); // to keep on showing until done here UIUtilities.releaseKeepScreenOn(getWindow()); } @Override public int getDuration() { return mNarrativeDuration; } @Override public int getCurrentPosition() { if (mPlaybackPosition < 0) { return mNarrativeDuration; } else { int rootPlaybackPosition = mPlaybackPosition + mNonAudioOffset; if (mSilenceFilePlaying) { // must calculate the actual time at the point of pausing, rather than the current time if (mPlaybackPauseTime > 0) { rootPlaybackPosition += (int) (mPlaybackPauseTime - mPlaybackStartTime); } else { rootPlaybackPosition += (int) (System.currentTimeMillis() - mPlaybackStartTime); } } else { rootPlaybackPosition += (mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0); } return rootPlaybackPosition; } } @Override public void seekTo(int pos) { // TODO: seek others (is it even possible with soundpool?) int actualPos = pos - mPlaybackPosition; if (mPlaybackPosition < 0) { // so we allow seeking from the end mPlaybackPosition = mNarrativeDuration - mCurrentFrameContainer.mFrameMaxDuration; } mPlaybackPauseTime = -1; // we'll be playing after this call if (actualPos >= 0 && actualPos < mCurrentFrameContainer.mFrameMaxDuration) { if (mIsLoading || (actualPos < mMediaPlayer.getDuration() && mCurrentFrameContainer.mAudioPaths.size() > 0)) { if (!mIsLoading) { if (mMediaController.isDragging()) { mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener); } mPlaybackStartTime = System.currentTimeMillis() - actualPos; mMediaPlayer.seekTo(actualPos); if (!mMediaPlayer.isPlaying()) { // we started from the end mMediaPlayer.start(); UIUtilities.acquireKeepScreenOn(getWindow()); } } else { // still loading - come here so we don't reload the same item again } } else { // for image- or text-only frames mNonAudioOffset = actualPos; if (mMediaController.isDragging()) { mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener); } mPlaybackStartTime = System.currentTimeMillis(); mMediaPlayer.seekTo(0); mMediaPlayer.start(); mMediaController.setProgress(); } } else if (pos >= 0 && pos < mNarrativeDuration) { FrameMediaContainer newContainer = getMediaContainer(pos, true); if (newContainer != mCurrentFrameContainer) { mCurrentFrameContainer = newContainer; prepareMediaItems(mCurrentFrameContainer); mInitialPlaybackOffset = pos - mPlaybackPosition; } else { mIsLoading = false; } } } @Override public boolean isPlaying() { return mMediaPlayer == null ? false : mMediaPlayer.isPlaying(); } @Override public boolean isLoading() { return mIsLoading; } @Override public int getBufferPercentage() { return 0; } @Override public boolean canPause() { return true; } @Override public boolean canSeekBackward() { return true; } @Override public boolean canSeekForward() { return true; } @Override public void onControllerVisibilityChange(boolean visible) { makeMediaItemsVisible(visible); findViewById(R.id.panel_media_viewer).setVisibility(visible ? View.VISIBLE : View.GONE); } }; private void startPlayers() { // so that we don't start playing after pause if we were loading if (mIsLoading) { for (Integer soundId : mFrameSounds) { mSoundPool.play(soundId, 1, 1, 1, 0, 1f); // volume is % of *current*, rather than maximum // TODO: seek to mInitialPlaybackOffset } mMediaPlayer.setOnCompletionListener(mMediaPlayerCompletionListener); mPlaybackStartTime = System.currentTimeMillis() - mInitialPlaybackOffset; mMediaPlayer.seekTo(mInitialPlaybackOffset); mMediaPlayer.start(); mIsLoading = false; mMediaController.setMediaPlayer(mMediaPlayerController); UIUtilities.acquireKeepScreenOn(getWindow()); } } private SoundPool.OnLoadCompleteListener mSoundPoolLoadListener = new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { mFrameSounds.add(sampleId); if (mFrameSounds.size() >= mNumExtraSounds) { mSoundPoolPrepared = true; } if (mSoundPoolPrepared && mMediaPlayerPrepared) { startPlayers(); } } }; private OnPreparedListener mMediaPlayerPreparedListener = new OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mMediaPlayerPrepared = true; if (mSoundPoolPrepared) { startPlayers(); } } }; private OnCompletionListener mMediaPlayerCompletionListener = new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { if (mMediaPlayerError) { // releasePlayer(); // don't do this, as it means the player will be null; instead we resume from errors mCurrentFrameContainer = getMediaContainer(mPlaybackPosition, false); prepareMediaItems(mCurrentFrameContainer); mMediaPlayerError = false; return; } mInitialPlaybackOffset = 0; int currentPosition = mMediaPlayerController.getCurrentPosition() + (mMediaPlayer.getDuration() - mMediaPlayer.getCurrentPosition()) + 1; if (currentPosition < mNarrativeDuration) { mMediaPlayerController.seekTo(currentPosition); } else if (!mMediaController.isDragging()) { // move to just before the end (accounting for mNarrativeDuration errors) mMediaPlayerController.seekTo(currentPosition - 2); pauseMediaController(); // will also show the controller if applicable mPlaybackPosition = -1; // so we start from the beginning } } }; private OnErrorListener mMediaPlayerErrorListener = new OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { mMediaPlayerError = true; // UIUtilities.showToast(NarrativeViewerActivity.this, R.string.error_loading_narrative_player); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Playback error � what: " + what + ", extra: " + extra); return false; // not handled -> onCompletionListener will be called } }; }