package org.commcare.android.framework;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.commcare.android.database.user.models.ACase;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.tasks.templates.CommCareTask;
import org.commcare.android.tasks.templates.CommCareTaskConnector;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.dalvik.activities.CommCareHomeActivity;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.dialogs.CustomProgressDialog;
import org.commcare.dalvik.dialogs.DialogController;
import org.commcare.util.SessionFrame;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.NoLocalizedTextException;
import org.odk.collect.android.views.media.AudioButton;
import org.odk.collect.android.views.media.AudioController;
import org.odk.collect.android.views.media.MediaState;
import org.odk.collect.android.views.media.MediaEntity;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
/**
* Base class for CommCareActivities to simplify
* common localization and workflow tasks
*
* @author ctsims
*
*/
public abstract class CommCareActivity<R> extends FragmentActivity implements CommCareTaskConnector<R>,
AudioController, DialogController {
protected final static int DIALOG_PROGRESS = 32;
protected final static String DIALOG_TEXT = "cca_dialog_text";
public final static String KEY_DIALOG_FRAG = "dialog_fragment";
StateFragment stateHolder;
private boolean firstRun = true;
//Fields for implementation of AudioController
private MediaEntity currentEntity;
private AudioButton currentButton;
private MediaState stateBeforePause;
//fields for implementing task transitions for CommCareTaskConnector
boolean inTaskTransition;
boolean shouldDismissDialog = true;
/*
* (non-Javadoc)
* @see android.support.v4.app.FragmentActivity#onCreate(android.os.Bundle)
*/
@Override
@TargetApi(14)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = this.getSupportFragmentManager();
stateHolder = (StateFragment) fm.findFragmentByTag("state");
// If the state holder is null, create a new one for this activity
if (stateHolder == null) {
stateHolder = new StateFragment();
fm.beginTransaction().add(stateHolder, "state").commit();
} else {
if(stateHolder.getPreviousState() != null){
firstRun = stateHolder.getPreviousState().isFirstRun();
loadPreviousAudio(stateHolder.getPreviousState());
} else{
firstRun = true;
}
}
if(this.getClass().isAnnotationPresent(ManagedUi.class)) {
this.setContentView(this.getClass().getAnnotation(ManagedUi.class).value());
loadFields();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getActionBar().setDisplayShowCustomEnabled(true);
//Add breadcrumb bar
BreadcrumbBarFragment bar = (BreadcrumbBarFragment) fm.findFragmentByTag("breadcrumbs");
// If the state holder is null, create a new one for this activity
if (bar == null) {
bar = new BreadcrumbBarFragment();
fm.beginTransaction().add(bar, "breadcrumbs").commit();
}
}
}
private void loadPreviousAudio(AudioController oldController) {
MediaEntity oldEntity = oldController.getCurrMedia();
if (oldEntity != null) {
this.currentEntity = oldEntity;
oldController.removeCurrentMediaEntity();
}
}
private void playPreviousAudio() {
if (currentEntity == null) return;
switch (currentEntity.getState()) {
case PausedForRenewal:
playCurrentMediaEntity();
break;
case Paused:
break;
case Playing:
case Ready:
System.out.println("WARNING: state in loadPreviousAudio is invalid");
}
}
/*
* Method to override in classes that need some functions called only once at the start
* of the life cycle. Called by the CommCareActivity onResume() method; so, after the onCreate()
* method of all classes, but before the onResume() of the overriding activity. State maintained in
* stateFragment Fragment and firstRun boolean.
*/
public void fireOnceOnStart(){
// override when needed
}
/*
* (non-Javadoc)
* @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
try {
CommCareApplication._().getCurrentSession().clearAllState();
} catch(SessionUnavailableException sue) {
// probably won't go anywhere with this
}
// app icon in action bar clicked; go home
Intent intent = new Intent(this, CommCareHomeActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void loadFields() {
CommCareActivity oldActivity = stateHolder.getPreviousState();
Class c = this.getClass();
for(Field f : c.getDeclaredFields()) {
if(f.isAnnotationPresent(UiElement.class)) {
UiElement element = f.getAnnotation(UiElement.class);
try{
f.setAccessible(true);
try {
View v = this.findViewById(element.value());
f.set(this, v);
if(oldActivity != null) {
View oldView = (View)f.get(oldActivity);
if(oldView != null) {
if(v instanceof TextView) {
((TextView)v).setText(((TextView)oldView).getText());
}
v.setVisibility(oldView.getVisibility());
v.setEnabled(oldView.isEnabled());
continue;
}
}
if(element.locale() != "") {
if(v instanceof TextView) {
((TextView)v).setText(Localization.get(element.locale()));
} else {
throw new RuntimeException("Can't set the text for a " + v.getClass().getName() + " View!");
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
throw new RuntimeException("Bad Object type for field " + f.getName());
} catch (IllegalAccessException e) {
throw new RuntimeException("Couldn't access the activity field for some reason");
}
} finally {
f.setAccessible(false);
}
}
}
}
protected CommCareActivity getDestroyedActivityState() {
return stateHolder.getPreviousState();
}
protected boolean isTopNavEnabled() {
return false;
}
boolean visible = false;
/* (non-Javadoc)
* @see android.app.Activity#onResume()
*/
@Override
@TargetApi(11)
protected void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
//If we're in honeycomb this is taken care of by the fragment
} else {
this.setTitle(getTitle(this, getActivityTitle()));
}
visible = true;
playPreviousAudio();
//set that this activity has run
if(isFirstRun()){
fireOnceOnStart();
setActivityHasRun();
}
}
/* (non-Javadoc)
* @see android.app.Activity#onPause()
*/
@Override
protected void onPause() {
super.onPause();
visible = false;
if (currentEntity != null) saveEntityStateAndClear();
}
protected boolean isInVisibleState() {
return visible;
}
/* (non-Javadoc)
* @see android.app.Activity#onDestroy()
*/
@Override
protected void onDestroy() {
super.onDestroy();
if (currentEntity != null) attemptSetStateToPauseForRenewal();
}
/* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#connectTask(org.commcare.android.tasks.templates.CommCareTask)
*/
@Override
public <A, B, C> void connectTask(CommCareTask<A, B, C, R> task) {
//If stateHolder is null here, it's because it is restoring itself, it doesn't need
//this step
wakelock();
stateHolder.connectTask(task);
//If we've left an old dialog showing during the task transition and it was from the same task
//as the one that is starting, don't dismiss it
CustomProgressDialog currDialog = getCurrentDialog();
if (currDialog != null && currDialog.getTaskId() == task.getTaskId()) {
shouldDismissDialog = false;
}
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#getReceiver()
*/
@Override
public R getReceiver() {
return (R)this;
}
/* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#startBlockingForTask()
*
* Override these to control the UI for your task
*/
@Override
public void startBlockingForTask(int id) {
//attempt to dismiss the dialog from the last task before showing this one
attemptDismissDialog();
//ONLY if shouldDismissDialog = true, i.e. if we chose to dismiss the last dialog during transition, show a new one
if (id >= 0 && shouldDismissDialog) {
this.showProgressDialog(id);
}
}
/* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#stopBlockingForTask()
*/
@Override
public void stopBlockingForTask(int id) {
if (id >= 0) {
if (inTaskTransition) {
shouldDismissDialog = true;
}
else {
dismissProgressDialog();
}
}
unlock();
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#startTaskTransition()
*/
@Override
public void startTaskTransition() {
inTaskTransition = true;
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#stopTaskTransition()
*/
@Override
public void stopTaskTransition() {
inTaskTransition = false;
attemptDismissDialog();
//reset shouldDismissDialog to true after this transition cycle is over
shouldDismissDialog = true;
}
//if shouldDismiss flag has not been set to false in the course of a task transition,
//then dismiss the dialog
public void attemptDismissDialog() {
if (shouldDismissDialog) {
dismissProgressDialog();
}
}
/**
* Handle an error in task execution.
*
* @param e
*/
protected void taskError(Exception e) {
//TODO: For forms with good error reporting, integrate that
Toast.makeText(this, Localization.get("activity.task.error.generic", new String[] {e.getMessage()}), Toast.LENGTH_LONG).show();
Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, e.getMessage());
}
/**
* Display exception details as a pop-up to the user.
*
* @param e Exception to handle
*/
protected void displayException(Exception e) {
String mErrorMessage = e.getMessage();
AlertDialog mAlertDialog = new AlertDialog.Builder(this).create();
mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
mAlertDialog.setTitle(Localization.get("notification.case.predicate.title"));
mAlertDialog.setMessage(Localization.get("notification.case.predicate.action", new String[] {mErrorMessage}));
DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {
/*
* (non-Javadoc)
* @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int)
*/
@Override
public void onClick(DialogInterface dialog, int i) {
switch (i) {
case DialogInterface.BUTTON1:
finish();
break;
}
}
};
mAlertDialog.setCancelable(false);
mAlertDialog.setButton(Localization.get("dialog.ok"), errorListener);
mAlertDialog.show();
}
/* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTaskConnector#taskCancelled(int)
*/
@Override
public void taskCancelled(int id) {
}
/**
*
*/
public void cancelCurrentTask() {
stateHolder.cancelTask();
}
/*
* (non-Javadoc)
* @see android.support.v4.app.FragmentActivity#onStop()
*/
@Override
public void onStop() {
super.onStop();
}
private void wakelock() {
int lockLevel = getWakeLockingLevel();
if(lockLevel == -1) { return;}
stateHolder.wakelock(lockLevel);
}
private void unlock() {
stateHolder.unlock();
}
/**
* @return The WakeLock flags that should be used for this activity's tasks. -1
* if this activity should not acquire/use the wakelock for tasks
*/
protected int getWakeLockingLevel() {
return -1;
}
//Graphical stuff below, needs to get modularized
public void TransplantStyle(TextView target, int resource) {
//get styles from here
TextView tv = (TextView)View.inflate(this, resource, null);
int[] padding = {target.getPaddingLeft(), target.getPaddingTop(), target.getPaddingRight(),target.getPaddingBottom() };
target.setTextColor(tv.getTextColors().getDefaultColor());
target.setTypeface(tv.getTypeface());
target.setBackgroundDrawable(tv.getBackground());
target.setPadding(padding[0], padding[1], padding[2], padding[3]);
}
/**
* The right-hand side of the title associated with this activity.
*
* This will update dynamically as the activity loads/updates, but if
* it will ever have a value it must return a blank string when one
* isn't available.
*
* @return
*/
public String getActivityTitle() {
return null;
}
public static String getTopLevelTitleName(Context c) {
String topLevel = null;
try {
topLevel = Localization.get("app.display.name");
return topLevel;
} catch(NoLocalizedTextException nlte) {
//nothing, app display name is optional for now.
}
return c.getString(org.commcare.dalvik.R.string.title_bar_name);
}
public static String getTitle(Context c, String local) {
String topLevel = getTopLevelTitleName(c);
String[] stepTitles = new String[0];
try {
stepTitles = CommCareApplication._().getCurrentSession().getHeaderTitles();
//See if we can insert any case hacks
int i = 0;
for(String[] step : CommCareApplication._().getCurrentSession().getFrame().getSteps()){
try {
if(SessionFrame.STATE_DATUM_VAL.equals(step[0])) {
//Haaack
if(step[1] != null && step[1].contains("case_id")) {
ACase foundCase = CommCareApplication._().getUserStorage(ACase.STORAGE_KEY, ACase.class).getRecordForValue(ACase.INDEX_CASE_ID, step[2]);
stepTitles[i] = Localization.get("title.datum.wrapper", new String[] { foundCase.getName()});
}
}
} catch(Exception e) {
//TODO: Your error handling is bad and you should feel bad
}
++i;
}
} catch(SessionUnavailableException sue) {
}
String returnValue = topLevel;
for(String title : stepTitles) {
if(title != null) {
returnValue += " > " + title;
}
}
if(local != null) {
returnValue += " > " + local;
}
return returnValue;
}
public void setActivityHasRun(){
this.firstRun = false;
}
public boolean isFirstRun(){
return this.firstRun;
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#getCurrMedia()
*
* All methods for implementation of AudioController
*/
@Override
public MediaEntity getCurrMedia() {
return currentEntity;
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#refreshCurrentAudioButton(org.odk.collect.android.views.media.AudioButton)
*/
@Override
public void refreshCurrentAudioButton(AudioButton clicked) {
if (currentButton != null && currentButton != clicked) {
currentButton.setStateToReady();
}
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#setCurrent(org.odk.collect.android.views.media.MediaEntity, org.odk.collect.android.views.media.AudioButton)
*/
@Override
public void setCurrent(MediaEntity e, AudioButton b) {
refreshCurrentAudioButton(b);
setCurrent(e);
setCurrentAudioButton(b);
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#setCurrent(org.odk.collect.android.views.media.MediaEntity)
*/
@Override
public void setCurrent(MediaEntity e) {
releaseCurrentMediaEntity();
currentEntity = e;
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#setCurrentAudioButton(org.odk.collect.android.views.media.AudioButton)
*/
@Override
public void setCurrentAudioButton(AudioButton b) {
currentButton = b;
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#releaseCurrentMediaEntity()
*/
@Override
public void releaseCurrentMediaEntity() {
if (currentEntity != null) {
MediaPlayer mp = currentEntity.getPlayer();
mp.reset();
mp.release();
}
currentEntity = null;
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#playCurrentMediaEntity()
*/
@Override
public void playCurrentMediaEntity() {
if (currentEntity != null) {
MediaPlayer mp = currentEntity.getPlayer();
mp.start();
currentEntity.setState(MediaState.Playing);
}
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#pauseCurrentMediaEntity()
*/
@Override
public void pauseCurrentMediaEntity() {
if (currentEntity != null && currentEntity.getState().equals(MediaState.Playing)) {
MediaPlayer mp = currentEntity.getPlayer();
mp.pause();
currentEntity.setState(MediaState.Paused);
}
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#getMediaEntityId()
*/
@Override
public Object getMediaEntityId() {
return currentEntity.getId();
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#attemptSetStateToPauseForRenewal()
*/
@Override
public void attemptSetStateToPauseForRenewal() {
if (stateBeforePause != null && stateBeforePause.equals(MediaState.Playing)) {
currentEntity.setState(MediaState.PausedForRenewal);
}
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#saveEntityStateAndClear()
*/
@Override
public void saveEntityStateAndClear() {
stateBeforePause = currentEntity.getState();
pauseCurrentMediaEntity();
refreshCurrentAudioButton(null);
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#setMediaEntityState(org.odk.collect.android.views.media.MediaState)
*/
@Override
public void setMediaEntityState(MediaState state) {
currentEntity.setState(state);
}
/*
* (non-Javadoc)
* @see org.odk.collect.android.views.media.AudioController#removeCurrentMediaEntity()
*/
@Override
public void removeCurrentMediaEntity() {
currentEntity = null;
}
/** All methods for implementation of DialogController **/
/*
* (non-Javadoc)
* @see org.commcare.dalvik.dialogs.DialogController#updateProgress(java.lang.String, int)
*/
@Override
public void updateProgress(String updateText, int taskId) {
CustomProgressDialog mProgressDialog = getCurrentDialog();
if (mProgressDialog != null) {
if (mProgressDialog.getTaskId() == taskId) {
mProgressDialog.updateMessage(updateText);
}
else {
Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
"Attempting to update a progress dialog whose taskId does not match the"
+ "task for which the update message was intended.");
}
}
}
/*
* (non-Javadoc)
* @see org.commcare.dalvik.dialogs.DialogController#showProgressDialog(int)
*/
@Override
public void showProgressDialog(int taskId) {
CustomProgressDialog dialog = generateProgressDialog(taskId);
if (dialog != null) {
dialog.show(getSupportFragmentManager(), KEY_DIALOG_FRAG);
}
}
/*
* (non-Javadoc)
* @see org.commcare.dalvik.dialogs.DialogController#getCurrentDialog()
*/
@Override
public CustomProgressDialog getCurrentDialog() {
return (CustomProgressDialog) getSupportFragmentManager().
findFragmentByTag(KEY_DIALOG_FRAG);
}
/*
* (non-Javadoc)
* @see org.commcare.dalvik.dialogs.DialogController#dismissProgressDialog()
*/
@Override
public void dismissProgressDialog() {
CustomProgressDialog mProgressDialog = getCurrentDialog();
if (mProgressDialog != null && mProgressDialog.isAdded()) {
mProgressDialog.dismissAllowingStateLoss();
}
}
/*
* (non-Javadoc)
* @see org.commcare.dalvik.dialogs.DialogController#generateProgressDialog(int)
*/
@Override
public CustomProgressDialog generateProgressDialog(int taskId) {
//dummy method for compilation, implementation handled in those subclasses that need it
return null;
}
}