/*
* Copyright (c) 2013 Allogy Interactive.
*
* 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.allogy.app.media;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import android.app.Activity;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import com.allogy.app.HomeActivity;
import com.allogy.app.R;
import com.allogy.app.adapter.NotesCursorAdapter;
import com.allogy.app.adapter.NotesCursorAdapter.NoteView;
import com.allogy.app.media.AudioPlayerService.AudioPlayerBinder;
import com.allogy.app.provider.Academic;
import com.allogy.app.provider.Notes;
import com.allogy.app.ui.ActionItem;
import com.allogy.app.ui.AnnotatedProgressBar;
import com.allogy.app.ui.QuickAction;
import com.allogy.app.util.Util;
/**
* <p>
* Provides all the necessary UI functionality for playing Audio files.
* </p>
* <p>
* TODO: See if we can merge some of the code from the AudioPlayerActivity and
* the VideoPlayerActivity.
* </p>
*
* @author Diego Nunez
*/
public class AudioPlayerActivity extends Activity {
// TODO: Move any hard coded string references into the string.xml resource
// and reference them from there.
// /
// / CONSTANTS
// /
public static final String LOG_TAG = AudioPlayerActivity.class.getName();
private static final boolean DBG_LOG_ENABLE = false;
public static final String INTENT_EXTRA_LESSON_FILE_ID = "audioplayeractivity.lessonfileid";
// /
// / PROPERTIES
// /
private AnnotatedProgressBar mProgressBar;
private TextView mCurrentAnnote;
private QuickAction mQuickAction = null;
private boolean isAnnotationListDisplayed = false;
private boolean isPlayListDisplayed = false;
private Integer playbackProgress = 0;
private Handler mHandler = new Handler();
private View mEditingAnnotation = null;
/**
* <p>
* The map is used to store local copies of the notes, as such we do not
* have to continuously check to database in order to figure out which note
* to display on audio play back.
* </p>
*/
private Map<Integer, String> mAnnotationProgressNoteMap = new HashMap<Integer, String>();
private int mLessonFileID = Util.OUT_OF_BOUNDS;
private AudioItem mCurrentAudio = null;
// /
// / ACTIVITY EVENTS
// /
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.audio_player_activity_layout);
// retrieve any extras from the intent.
Intent intent = this.getIntent();
mLessonFileID = intent.getIntExtra(
AudioPlayerActivity.INTENT_EXTRA_LESSON_FILE_ID,
Util.OUT_OF_BOUNDS);
if(DBG_LOG_ENABLE) {
Log.i(LOG_TAG, "The Lesson ID is : " + mLessonFileID);
}
// register listener with the activity views.
mProgressBar = (AnnotatedProgressBar) this
.findViewById(R.id.audio_player_apb_progress);
mProgressBar.SetOnSeekListener(mSeekListener);
mCurrentAnnote = (TextView) this
.findViewById(R.id.audio_player_tv_note);
ActionItem ai = new ActionItem();
ai.setTitle("Delete");
ai.setOnClickListener(mActionItemClick);
mQuickAction = new QuickAction(this
.findViewById(R.id.audio_player_list_view));
mQuickAction.setAnimStyle(QuickAction.ANIM_AUTO);
mQuickAction.addActionItem(ai);
mQuickAction.setOnDismissListener(mActionDismissed);
// start the audio player service such that it will remain
// active even though no activity is binded to it.
this.startService(new Intent(this, AudioPlayerService.class));
}
@Override
public void onResume() {
super.onResume();
// bind to the audio service.
this.bindService(new Intent(this, AudioPlayerService.class),
mConnection, Context.BIND_AUTO_CREATE);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public Dialog onCreateDialog(int id, Bundle args) {
Dialog dialog = null;
switch (id) {
default:
return super.onCreateDialog(id);
}
}
@Override
public void onPrepareDialog(int id, Dialog dialog, Bundle args) {
switch (id) {
default:
super.onPrepareDialog(id, dialog, args);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onDestroy() {
super.onDestroy();
this.unbindService(mConnection);
}
// /
// / METHODS
// /
/**
* <p>
* Initialize the progress bar to include all of the notes for a file, and
* save local copies of the notes in order to prevent continuous database
* calls.
* </p>
*
* @param id
* The primary key of a file saved in the database that belongs
* to a lesson.
*/
private void PrepareProgressAnnotations(int id) {
if (!mAnnotationProgressNoteMap.isEmpty()) {
mAnnotationProgressNoteMap.clear();
}
if (mProgressBar.HasAnnotations()) {
mProgressBar.ClearAnnotations();
}
Cursor cursor = Notes.GetNotes(this, id, Notes.Note.TYPE_AUDIO);
if (cursor.moveToFirst()) {
do {
int progress = cursor.getInt(cursor
.getColumnIndexOrThrow(Notes.Note.TIME));
mProgressBar.AddAnnotation(progress);
mAnnotationProgressNoteMap.put(progress, cursor
.getString(cursor
.getColumnIndexOrThrow(Notes.Note.BODY)));
} while (cursor.moveToNext());
}
cursor.close();
}
/**
* Displays either the annotations list or the current play list.
*
* @param type
* True for the annotations list, and false for the play list.
*/
private void DisplayList(boolean type) {
ListView list = (ListView) this
.findViewById(R.id.audio_player_list_view);
TextView img = (TextView) this.findViewById(R.id.audio_player_img_icon);
this.findViewById(R.id.audio_player_btn_save).setVisibility(View.GONE);
this.findViewById(R.id.audio_player_btn_discard).setVisibility(
View.GONE);
this.findViewById(R.id.audio_player_et_note).setVisibility(View.GONE);
int visibility = list.getVisibility();
if (visibility == View.GONE) {
if (type) {
PrepareAnnotationList(list);
isAnnotationListDisplayed = true;
isPlayListDisplayed = false;
} else {
PreparePlayList(list);
isAnnotationListDisplayed = false;
isPlayListDisplayed = true;
}
list.setVisibility(View.VISIBLE);
img.setVisibility(View.GONE);
mCurrentAnnote.setVisibility(View.GONE);
} else {
if (null != mEditingAnnotation) {
PrepareAnnotationList(list);
isAnnotationListDisplayed = true;
isPlayListDisplayed = false;
} else if (isAnnotationListDisplayed
&& isAnnotationListDisplayed == !type) {
PreparePlayList(list);
isAnnotationListDisplayed = false;
isPlayListDisplayed = true;
} else if (isPlayListDisplayed && isPlayListDisplayed == type) {
PrepareAnnotationList(list);
isAnnotationListDisplayed = true;
isPlayListDisplayed = false;
} else {
list.setVisibility(View.GONE);
img.setVisibility(View.VISIBLE);
mCurrentAnnote.setVisibility(View.VISIBLE);
isAnnotationListDisplayed = false;
isPlayListDisplayed = false;
}
}
}
/**
* Sets the adapter and listeners for the annotations <b>ListView</b>.
*
* @param annList
* The <b>ListView</b> which holds the annotations.
*/
private void PrepareAnnotationList(ListView annList) {
annList.setAdapter(new NotesCursorAdapter(this, mCurrentAudio.getId(),
Notes.Note.TYPE_AUDIO));
annList.setOnItemClickListener(mAnnClickListener);
annList.setOnItemLongClickListener(mAnnItemLongClick);
}
/**
* Sets the adapter and listeners for the play list <b>ListView</b>.
*
* @param playList
* The <b>ListView</b> which holds the play list.
*/
private void PreparePlayList(ListView playList) {
playList.setOnItemLongClickListener(null);
playList.setOnItemClickListener(mPlayClickListener);
playList.setAdapter(mAudioPlayerServiceBinder.ShowPlayList(this));
}
/**
* Handles the visibility of all of the components neccesary to create an
* annotation.
*
* @param mode
* If true displays all the necessary UI elements, otherwise
* hides all of the elements.
*/
private void PrepareCreateAnnotation(boolean mode) {
this.findViewById(R.id.audio_player_list_view).setVisibility(View.GONE);
this.findViewById(R.id.audio_player_img_icon).setVisibility(
mode ? View.GONE : View.VISIBLE);
mCurrentAnnote.setVisibility(mode ? View.GONE : View.VISIBLE);
this.findViewById(R.id.audio_player_btn_save).setVisibility(
mode ? View.VISIBLE : View.GONE);
this.findViewById(R.id.audio_player_btn_discard).setVisibility(
mode ? View.VISIBLE : View.GONE);
this.findViewById(R.id.audio_player_et_note).setVisibility(
mode ? View.VISIBLE : View.GONE);
}
/**
* Retrieves the appropriate not to display depending on the playback
* progress.
*
* @param timestamp
* The current playback location.
*/
private void RegisterAnnotationFromTimeStamp(int timestamp) {
// TODO: See if a query to the database for the correct annotation would
// be a better option.
ArrayList<Integer> keys = new ArrayList<Integer>(
mAnnotationProgressNoteMap.keySet());
Collections.sort(keys);
int key = 0;
for (int i = 0, j = 1, len = keys.size(); j <= len; i++, j++) {
key = keys.get(i);
if (j == len) {
if (timestamp >= keys.get(i)) {
mCurrentAnnote.setText(mAnnotationProgressNoteMap.get(key));
}
} else {
if (timestamp >= keys.get(i) && timestamp < keys.get(j)) {
mCurrentAnnote.setText(mAnnotationProgressNoteMap.get(key));
}
}
}
}
/**
* Sets up the current audio to be played.
*
* @param id
* The primary key of a file saved in the database that belongs
* to a lesson.
* @return True if successful, false otherwise.
*/
private boolean RegisterCurrentAudio(int id) {
boolean result = false;
// Reset.
if (!mAudioPlayerServiceBinder.isStopped()) {
mAudioPlayerServiceBinder.Stop();
}
mProgressBar.SetProgress(0);
Cursor cursor = this.getContentResolver().query(
Academic.LessonFiles.CONTENT_URI,
new String[] { Academic.LessonFiles.URI },
String.format("%s = ?", Academic.LessonFiles._ID),
new String[] { Integer.toString(id) }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
mCurrentAudio = new AudioItem(id,
Environment.getExternalStorageDirectory()
+ "/Allogy/Decrypted/"
+ cursor.getString(
cursor.getColumnIndexOrThrow(Academic.LessonFiles.URI))
.replace(".mp3", "").trim());
mCurrentAudio.Tag = mAudioPlayerServiceBinder
.PreparePlayback(mCurrentAudio);
mProgressBar.SetMaxProgress((Integer) mCurrentAudio.Tag);
PrepareProgressAnnotations(id);
result = true;
}
cursor.close();
}
return result;
}
/**
* Performs the navigation to different <b>AudioItem</b>'s withing the play
* list.
*
* @param audioSkip
* The <b>AudioItem</b> to play.
*/
private void RegisterAudioSkip(AudioItem audioSkip) {
if (null == audioSkip) {
Toast
.makeText(this, "The playlist is empty...",
Toast.LENGTH_SHORT).show();
} else {
RegisterCurrentAudio(audioSkip.getId());
PlayCommand();
}
}
/**
* Starts play back.
*/
private void PlayCommand() {
mAudioPlayerServiceBinder.Play();
((ImageButton) AudioPlayerActivity.this.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.pause_button));
}
// /
// / THREADS
// /
/**
* Updates the play back progress, as well as the current note to display.
*/
private Runnable mUpdateRunnable = new Runnable() {
@Override
public void run() {
mProgressBar.SetProgress(playbackProgress);
if (mAudioPlayerServiceBinder.isStopped()) {
((ImageButton) AudioPlayerActivity.this
.findViewById(R.id.audio_player_btn_add_notes))
.setEnabled(true);
mCurrentAnnote.setText("");
((ImageButton) AudioPlayerActivity.this.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.play_button));
} else {
RegisterAnnotationFromTimeStamp(playbackProgress);
}
}
};
// /
// / EVENT LISTENERS
// /
/**
* <p>
* Event listener for button clicks that are not dependent on the
* <b>AudioPlayerService</b>.
* </p>
*
* @param view
* The source of the click event.
*/
public void ButtonsListener(View view) {
switch (view.getId()) {
case R.id.activity_audioplayer_ibtn_home:
Intent i = new Intent();
i.setClass(this, HomeActivity.class);
startActivity(i);
this.finish();
break;
case R.id.audio_player_btn_search:
// TODO: Search...
break;
default:
break;
}
}
/**
* <p>
* Event listener for button clicks that are dependent on the
* <b>AudioPlayerService</b>.
* </p>
*
* @param view
* The source of the click event.
*/
public void ServiceButtonsListener(View view) {
if (!isBound) {
Toast.makeText(this, "Could not connect to the audio service...",
Toast.LENGTH_LONG).show();
return;
}
switch (view.getId()) {
case R.id.audio_player_btn_add_notes:
if (!mAudioPlayerServiceBinder.isStopped()) {
if (!mAudioPlayerServiceBinder.isPaused()) {
mAudioPlayerServiceBinder.Pause();
}
((ImageButton) this
.findViewById(R.id.audio_player_btn_add_notes))
.setEnabled(false);
PrepareCreateAnnotation(true);
} else {
Toast.makeText(this,
"Please play an audio track to add notes.",
Toast.LENGTH_SHORT).show();
}
break;
case R.id.audio_player_btn_list_notes:
DisplayList(true);
break;
case R.id.audio_player_btn_playlist:
DisplayList(false);
break;
case R.id.audio_player_btn_next_track:
RegisterAudioSkip(mAudioPlayerServiceBinder.SkipForward());
break;
case R.id.audio_player_btn_play_track:
if (mAudioPlayerServiceBinder.isStopped()) {
if (!mAudioPlayerServiceBinder.Play()) {
Toast.makeText(this, "Cannot play audio...",
Toast.LENGTH_SHORT).show();
} else {
((ImageButton) this.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.pause_button));
}
} else {
if (mAudioPlayerServiceBinder.Pause()) {
((ImageButton) this.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.play_button));
} else {
((ImageButton) this.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.pause_button));
}
}
break;
case R.id.audio_player_btn_prev_track:
RegisterAudioSkip(mAudioPlayerServiceBinder.SkipBackwards());
break;
case R.id.audio_player_btn_save:
// NOTE:
// R.id.audio_player_cav_btn_discard
// must come after this case.
// TODO: Add saving functionality to the database.
if (null == mEditingAnnotation) {
String tempS = ((EditText) this
.findViewById(R.id.audio_player_et_note)).getText()
.toString();
if (!Util.isNullOrEmpty(tempS)) {
int progress = mAudioPlayerServiceBinder
.GetPlaybackProgress();
// Add to the local copy.
mProgressBar.AddAnnotation(progress);
mAnnotationProgressNoteMap.put(progress, tempS);
// Add to the database.
ContentValues values = new ContentValues();
values.put(Notes.Note.CONTENT_ID, mCurrentAudio.getId());
values.put(Notes.Note.TYPE, Notes.Note.TYPE_AUDIO);
values.put(Notes.Note.BODY, tempS);
values.put(Notes.Note.TIME, progress);
AudioPlayerActivity.this.getContentResolver().insert(
Notes.Note.CONTENT_URI, values);
} else {
Toast.makeText(this, "Note can not be empty!",
Toast.LENGTH_SHORT).show();
break;
}
} else {
NoteView nview = (NoteView) mEditingAnnotation.getTag();
String tempS = nview.body.getText().toString();
if (Util.isNullOrEmpty(tempS)) {
// Update the local copy.
mAnnotationProgressNoteMap.put((Integer) nview.time
.getTag(), tempS);
// Update the database.
ContentValues values = new ContentValues();
values.put(Notes.Note.BODY, tempS);
AudioPlayerActivity.this.getContentResolver().update(
Notes.Note.CONTENT_URI, values,
String.format("%s = ?", Notes.Note._ID),
new String[] { Integer.toString(nview.id) });
} else {
Toast.makeText(this, "Note can not be empty!",
Toast.LENGTH_SHORT).show();
break;
}
}
case R.id.audio_player_btn_discard:
((EditText) this.findViewById(R.id.audio_player_et_note))
.setText("");
if (isAnnotationListDisplayed) {
DisplayList(true);
} else {
PrepareCreateAnnotation(false);
}
if (mAudioPlayerServiceBinder.isPaused()) {
mAudioPlayerServiceBinder.Pause();
}
((ImageButton) this.findViewById(R.id.audio_player_btn_add_notes))
.setEnabled(true);
mEditingAnnotation = null;
break;
default:
// do nothing.
}
}
/**
* Event handler for seek events.
*/
private OnSeekListener mSeekListener = new OnSeekListener() {
@Override
public void onSeeking() {
// not needed.
}
@Override
public void onSeekStarted(int progress) {
// not needed.
}
@Override
public void onSeekFinished(int progress) {
if (isBound) {
mAudioPlayerServiceBinder.SeekTo(progress);
}
}
};
/**
* Event handler for update events.
*/
private OnUpdateListener mUpdateListener = new OnUpdateListener() {
@Override
public void onUpdate(Object arg) {
playbackProgress = (Integer) arg;
// The event is running on a different thread that cannot change the
// UI element on the main thread, so we
// must post an update event using a handler.
mHandler.post(mUpdateRunnable);
}
};
/**
* Event handler for <b>ListView</b> item clicks when the play list is
* showing. We start playing the selected <b>AudioItem</b>.
*/
private OnItemClickListener mPlayClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
if (isBound) {
AudioItem audio = (AudioItem) view.getTag();
if (AudioPlayerActivity.this
.RegisterCurrentAudio(audio.getId())) {
PlayCommand();
}
}
}
};
/**
* Event handler for <b>ListView</b> item clicks when the annotations are
* showing. We switch the clicked item into edit mode.
*/
private OnItemClickListener mAnnClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
if (null == mEditingAnnotation) {
view
.findViewById(
R.id.list_item_audioplayer_annotation_et_note)
.setVisibility(View.GONE);
view.findViewById(
R.id.list_item_audioplayer_annotation_et_note_edit)
.setVisibility(View.VISIBLE);
mEditingAnnotation = view;
AudioPlayerActivity.this.findViewById(
R.id.audio_player_btn_save).setVisibility(View.VISIBLE);
AudioPlayerActivity.this.findViewById(
R.id.audio_player_btn_discard).setVisibility(
View.VISIBLE);
}
}
};
/**
* Event handler for <b>ListView</b> item long clicks when the annotations
* are showing. We pop up the <b>QuickAction</b> View.
*/
private OnItemLongClickListener mAnnItemLongClick = new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
if (null == mEditingAnnotation) {
mEditingAnnotation = view;
mQuickAction.show();
} else {
Toast.makeText(AudioPlayerActivity.this,
"Pending edits must be finilized...",
Toast.LENGTH_SHORT).show();
}
return true;
}
};
/**
* Event handler for <b>QuickAction</b> <b>ActionItem</b> click events. We
* handle the deletion of an annotation.
*/
private OnClickListener mActionItemClick = new OnClickListener() {
@Override
public void onClick(View view) {
if (null != mEditingAnnotation) {
String title = ((TextView) view.findViewById(R.id.title))
.getText().toString();
if (title == "Delete") {
NoteView nview = (NoteView) mEditingAnnotation.getTag();
// remove from the local copy.
int time = (Integer) nview.time.getTag();
mAnnotationProgressNoteMap.remove(time);
mProgressBar.RemoveAnnotation(time);
// remove from the database.
AudioPlayerActivity.this.getContentResolver().delete(
Notes.Note.CONTENT_URI,
String.format("%s = ?", Notes.Note._ID),
new String[] { Integer.toString(nview.id) });
if (isAnnotationListDisplayed) {
DisplayList(true);
} else {
PrepareCreateAnnotation(false);
}
}
}
mQuickAction.dismiss();
}
};
/**
* Event handler for <b>QuickAction</b> dismissed event. We set the editing
* annotation place holder to null.
*/
private PopupWindow.OnDismissListener mActionDismissed = new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
mEditingAnnotation = null;
}
};
// /
// / SERVICE CONNECTION
// /
private AudioPlayerBinder mAudioPlayerServiceBinder = null;
private boolean isBound = false;
/**
* Handles the connection to the <b>AudioPlayerService</b>.
*/
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mAudioPlayerServiceBinder = (AudioPlayerService.AudioPlayerBinder) service;
mAudioPlayerServiceBinder.SetUpdateListener(mUpdateListener);
if (mAudioPlayerServiceBinder.isStopped()) {
if (mLessonFileID != Util.OUT_OF_BOUNDS) {
// load the desired lesson file.
if (AudioPlayerActivity.this
.RegisterCurrentAudio(mLessonFileID)) {
// Automatically begin play back.
PlayCommand();
}
} else if (null != (mCurrentAudio = mAudioPlayerServiceBinder
.GetCurrentAudio())) {
AudioPlayerActivity.this.RegisterCurrentAudio(mCurrentAudio
.getId());
}
} else {
((ImageButton) AudioPlayerActivity.this
.findViewById(R.id.audio_player_btn_play_track))
.setImageDrawable(getResources()
.getDrawable(R.drawable.pause_button));
mCurrentAudio = mAudioPlayerServiceBinder.GetCurrentAudio();
mProgressBar.SetMaxProgress(mAudioPlayerServiceBinder
.GetPlaybackDuration());
mProgressBar.SetProgress(mAudioPlayerServiceBinder
.GetPlaybackProgress());
PrepareProgressAnnotations(mCurrentAudio.getId());
}
isBound = true;
}
@Override
public void onServiceDisconnected(ComponentName className) {
isBound = false;
mAudioPlayerServiceBinder.SetUpdateListener(null);
mAudioPlayerServiceBinder = null;
}
};
}