net.czlee.debatekeeper.debatemanager.DebateManager.java Source code

Java tutorial

Introduction

Here is the source code for net.czlee.debatekeeper.debatemanager.DebateManager.java

Source

/*
 * Copyright (C) 2012 Chuan-Zheng Lee
 *
 * This file is part of the Debatekeeper app, which is licensed under the
 * GNU General Public Licence version 3 (GPLv3).  You can redistribute
 * and/or modify it under the terms of the GPLv3, and you must not use
 * this file except in compliance with the GPLv3.
 *
 * This app 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 Licence for more details.
 *
 * You should have received a copy of the GNU General Public Licence
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.czlee.debatekeeper.debatemanager;

import java.util.ArrayList;

import net.czlee.debatekeeper.AlertManager;
import net.czlee.debatekeeper.DebatingTimerService.GuiUpdateBroadcastSender;
import net.czlee.debatekeeper.PrepTimeBellsManager;
import net.czlee.debatekeeper.R;
import net.czlee.debatekeeper.debateformat.DebateFormat;
import net.czlee.debatekeeper.debateformat.DebatePhaseFormat;
import net.czlee.debatekeeper.debateformat.PeriodInfo;
import net.czlee.debatekeeper.debateformat.PrepTimeSimpleFormat;
import android.app.Service;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.view.PagerAdapter;
import android.util.Log;

/**
 * <p>DebateManager manages a debate by keeping track of speeches and running the speech timers.</p>
 *
 * <p>It is given a {@link DebateFormat}, which cannot then be changed.  If it must be changed, this
 * <code>DebateManager</code> must be destroyed and another one created with the new
 * <code>DebateFormat</code>.</p>
 *
 * <p>DebateManager is also capable of:</p>
 *  <ul>
 *  <li> navigating forwards and backwards between phases
 *  <li> storing times for phases
 *  </ul>
 *
 * <p>DebateManager is <b>not</b> capable of handling the GUI, but it sends a message to the
 * DebatingActivity to update the GUI.</p>
 *
 * <p>The internal mechanics of a single speech are handled by {@link DebatePhaseManager}.</p>
 *
 * @author Chuan-Zheng Lee
 * @since  2012-06-09
 */
public class DebateManager {

    private static final String TAG = "DebateManager";

    private final DebateFormat mDebateFormat;
    private final DebatePhaseManager mPhaseManager;
    private final PoiManager mPoiManager;
    private final Context mContext;

    private final ArrayList<Long> mSpeechTimes;
    private long mPrepTime;

    private boolean mPrepTimeEnabledByUser = true;
    private int mActiveSpeechIndex;
    private DebatePhaseType mActivePhaseType;

    private static final String BUNDLE_SUFFIX_ITEM_TYPE = ".cit";
    private static final String BUNDLE_SUFFIX_INDEX = ".csi";
    private static final String BUNDLE_SUFFIX_SPEECH = ".sm";
    private static final String BUNDLE_SUFFIX_SPEECH_TIMES = ".st";
    private static final String BUNDLE_SUFFIX_PREP_TIME = ".pt";

    private static final int NO_SUCH_PHASE = PagerAdapter.POSITION_NONE;

    /**
     * Constructor.
     * @param context a {@link Context} relating to this DebateManager, probably a {@link Service}.
     * @param df The {@link DebateFormat} used by this DebateManager.
     * @param am The {@link AlertManager} used by this DebateManager.
     */
    public DebateManager(Context context, DebateFormat df, AlertManager am) {
        super();
        this.mContext = context;
        this.mDebateFormat = df;
        this.mPhaseManager = new DebatePhaseManager(am);
        // TODO un-hardcode this '15'
        this.mPoiManager = new PoiManager(am, 15);
        this.mSpeechTimes = new ArrayList<Long>();
        this.mPrepTime = 0;

        this.mSpeechTimes.ensureCapacity(df.numberOfSpeeches());
        for (int i = 0; i < df.numberOfSpeeches(); i++)
            mSpeechTimes.add((long) 0);

        if (hasPrepTime()) {
            this.mActivePhaseType = DebatePhaseType.PREP_TIME;
            this.mActiveSpeechIndex = 0;
            this.mPhaseManager.loadSpeech(mDebateFormat.getPrepFormat(), getActivePhaseName());
        } else {
            this.mActivePhaseType = DebatePhaseType.SPEECH;
            this.mActiveSpeechIndex = 0;
            this.mPhaseManager.loadSpeech(mDebateFormat.getSpeechFormat(mActiveSpeechIndex), getActivePhaseName());
        }

    }

    //******************************************************************************************
    // Public classes
    //******************************************************************************************

    /**
     * Used to uniquely identify speeches and prep timers in a way that is independent of phase
     * index.  The reason we do this is so that we can enable and disable prep time, but still give
     * other classes a means of identifying which speeches before correlate to which speeches after.
     * We can't just do this using the phase index, as the phase index changes when we add or
     * remove prep time.  (Phase indices must always number consecutively from zero.)
     *
     * Users of this class should treat it as a black box.  It can change in any way to reflect
     * extensions of this class to support other debate structures.  The exception to this is
     * the public member <code>specialTag</code>, which is always <code>null</code> for tags returned
     * by this class, but which user classes may like to use for their own purposes.  As of
     * 11 June 2013, this field is used by <code>DebatingActivity.DebateTimerDisplayPagerAdapter</code>
     * to mark tags where no debate is loaded.
     */
    public static class DebatePhaseTag {
        public String specialTag = null;
        private DebateFormat format;
        private DebatePhaseType type;
        private int index;
    }

    public enum DebatePhaseType {

        // Strings are used in the bundle in saveState() and restoreState().
        PREP_TIME("prepTime"), SPEECH("speech");

        private final String key;

        private DebatePhaseType(String key) {
            this.key = key;
        }

        @Override
        public String toString() {
            return key;
        }

        public static DebatePhaseType toEnum(String key) {
            DebatePhaseType[] values = DebatePhaseType.values();
            for (int i = 0; i < values.length; i++)
                if (key.equals(values[i].key))
                    return values[i];
            throw new IllegalArgumentException(String.format("There is no enumerated constant '%s'", key));
        }
    }

    //******************************************************************************************
    // Public methods
    //******************************************************************************************

    /**
     * @return the current period info to be displayed
     */
    public PeriodInfo getActivePhaseCurrentPeriodInfo() {
        return mPhaseManager.getCurrentPeriodInfo();
    }

    /**
     * Returns the current time of the active phase.
     * @return the current time for the active phase
     */
    public long getActivePhaseCurrentTime() {
        return mPhaseManager.getCurrentTime();
    }

    /**
     * @return the {@link DebatePhaseFormat} for the active phase
     */
    public DebatePhaseFormat getActivePhaseFormat() {
        return mPhaseManager.getFormat();
    }

    /**
     * <p>Returns the phase index of the timer that is currently active.</p>
     * <p>The phase indices number consecutively starting from 0.  Note that phase indices don't
     * necessarily correlate with speeches if, for example, prep time is enabled or disabled.</p>
     * @param timerIndex the new timer index
     */
    public int getActivePhaseIndex() {
        return findPhaseIndex(mActivePhaseType, mActiveSpeechIndex);
    }

    /**
     * Returns a human-readable name for the currently active phase of the debate. For prep time
     * this is (currently) always a string "Preparation time". For speeches it is the name of the
     * speech.
     * @return the name of the active phase of the debate
     */
    public String getActivePhaseName() {
        if (mActivePhaseType == DebatePhaseType.PREP_TIME)
            return mContext.getString(R.string.prepTime_title);
        else
            return mDebateFormat.getSpeechName(mActiveSpeechIndex);
    }

    /**
     * @return the next overtime bell, or <code>null</code> if there are no more overtime bells
     */
    public Long getActivePhaseNextOvertimeBellTime() {
        return mPhaseManager.getNextOvertimeBellTime();
    }

    /**
     * @return the current time on the POI timer, or <code>null</code> if the POI timer is not currently running.
     */
    public Long getCurrentPoiTime() {
        if (mPoiManager.isRunning())
            return mPoiManager.getCurrentTime();
        else
            return null;
    }

    /**
     * @return the debate format name
     */
    public String getDebateFormatName() {
        return mDebateFormat.getName();
    }

    /**
     * Returns the number of phases in this debate.  A "phase" is a part of a debate that is
     * governed by a single running timer, e.g. a speech or a period of preparation time.
     * @return the number of phases
     */
    public int getNumberOfPhases() {
        int count = mDebateFormat.numberOfSpeeches();
        if (hasPrepTime())
            count += 1;
        return count;
    }

    /**
     * Returns the current time of the phase identified by the given phase index.
     * @param phaseIndex the phase index (numbered consecutively from 0 including all speeches
     * and prep time periods)
     * @return the current time for that phase
     */
    public long getPhaseCurrentTime(int phaseIndex) {
        validatePhaseIndex(phaseIndex);
        if (phaseIndex == getActivePhaseIndex())
            return getActivePhaseCurrentTime();
        else if (phaseIndex == 0 && hasPrepTime())
            return mPrepTime;
        else if (hasPrepTime())
            return mSpeechTimes.get(phaseIndex - 1);
        else
            return mSpeechTimes.get(phaseIndex);
    }

    /**
     * Returns the {@link DebatePhaseFormat} for the phase specified by the given phase index.
     * @param phaseIndex the position of the timer in the {@link DebateManager}
     * @return the {@link DebatePhaseFormat} for that position
     */
    public DebatePhaseFormat getPhaseFormat(int phaseIndex) {
        validatePhaseIndex(phaseIndex);
        if (phaseIndex == 0 && hasPrepTime())
            return mDebateFormat.getPrepFormat();
        else if (hasPrepTime())
            return mDebateFormat.getSpeechFormat(phaseIndex - 1);
        else
            return mDebateFormat.getSpeechFormat(phaseIndex);
    }

    /**
     * Returns a human-readable name for the phase specified by the given phase index.
     * @param phaseIndex the index of the required phase in the {@link DebateManager}.
     * @return the name of the phase (speech or prep time) at that position.
     */
    public String getPhaseName(int phaseIndex) {
        validatePhaseIndex(phaseIndex);
        if (phaseIndex == 0 && hasPrepTime())
            return mContext.getString(R.string.prepTime_title);
        else if (hasPrepTime())
            return mDebateFormat.getSpeechName(phaseIndex - 1);
        else
            return mDebateFormat.getSpeechName(phaseIndex);
    }

    /**
     * Returns the phase index for the phase referenced by a tag.  If the tag is from a DebateFormat
     * that is different to this one, always returns <code>NO_SUCH_PHASE</code>.
     *
     * <p>Note that while <code>getPhaseIndexForTag(DebatePhaseTag)</code> and
     * <code>getPhaseTagForIndex(int)</code> are inverses of each other, if the result of one is
     * passed into the other after the phases are renumbered (e.g. because prep time becomes enabled
     * or disabled) then the result may not be the original value.</p>
     *
     * @param tag the {@link DebatePhaseTag} for the phase
     * @return the phase index for that tag, or <code>NO_SUCH_PHASE</code> if the phase is not found
     */
    public int getPhaseIndexForTag(DebatePhaseTag tag) {
        if (mDebateFormat != tag.format) {
            Log.i(TAG, String.format("getPhaseIndexForTag - no such phase, tag.format was %s, currently on %s",
                    (tag.format == null) ? "null" : tag.format.getName(), mDebateFormat.getName()));
            return NO_SUCH_PHASE;
        }
        return findPhaseIndex(tag.type, tag.index);
    }

    /**
     * Returns a tag that uniquely identifies a phase of the debate.  The tag will remain the
     * same even if phases are added or removed or re-ordered.  Currently, this happens when
     * prep time is enabled or disabled.
     * @param phaseIndex the phase index for the phase whose tag is to be retrieved
     * @return a {@link DebatePhaseTag} object being the suitable tag
     */
    public DebatePhaseTag getPhaseTagForIndex(int phaseIndex) {
        DebatePhaseTag tag = new DebatePhaseTag();
        if (hasPrepTime()) {
            if (getPhaseFormat(phaseIndex).isPrep()) {
                tag.type = DebatePhaseType.PREP_TIME;
                tag.index = 0;
            } else {
                tag.type = DebatePhaseType.SPEECH;
                tag.index = phaseIndex - 1;
            }
        } else {
            tag.type = DebatePhaseType.SPEECH;
            tag.index = phaseIndex;
        }
        tag.format = mDebateFormat;
        return tag;
    }

    /**
     * Calculates and returns when the next overtime bell
     * @return the next overtime bell after the time given, or <code>null</code> if there are no more overtime bells
     */
    public Long getPhaseNextOvertimeBellTime(int phaseIndex) {
        if (phaseIndex == getActivePhaseIndex())
            return getActivePhaseNextOvertimeBellTime();
        long time = getPhaseCurrentTime(phaseIndex);
        long length = getPhaseFormat(phaseIndex).getLength();
        return mPhaseManager.getNextOvertimeBellTimeAfter(time, length);
    }

    /**
     * @return the current state
     */
    public DebatePhaseManager.DebateTimerState getTimerStatus() {
        return mPhaseManager.getStatus();
    }

    /**
     * Moves to the next phase of the debate (speech or prep time).
     * If already on the last phase, does nothing.
     */
    public void goToNextPhase() {
        if (isInLastPhase())
            return;
        setActivePhaseIndex(getActivePhaseIndex() + 1);
    }

    /**
     * Moves to the previous phase of the debate (speech or prep time).
     * If already on the first phase, does nothing.
     */
    public void goToPreviousPhase() {
        if (isInFirstPhase())
            return;
        setActivePhaseIndex(getActivePhaseIndex() - 1);
    }

    /**
     * @return <code>true</code> if the active phase is the first phase, <code>false</code>
     * otherwise
     */
    public boolean isInFirstPhase() {
        return getActivePhaseIndex() == 0;
    }

    /**
     * @return <code>true</code> if the active phase is the last phase, <code>false</code>
     * otherwise
     */
    public boolean isInLastPhase() {
        return getActivePhaseIndex() == getNumberOfPhases() - 1;
    }

    /**
     * @return <code>true</code> if the POI timer is running, <code>false</code> otherwise.
     */
    public boolean isPoiRunning() {
        return mPoiManager.isRunning();
    }

    /**
     * @return <code>true</code> if GUI elements relating to POIs should be displayed.
     * This is the case if either POIs are currently allowed, or the POI timer is currently
     * running (i.e. the POI in action started before the warning bell).
     */
    public boolean isPoisActive() {
        return mPhaseManager.getCurrentPeriodInfo().isPoisAllowed() || mPoiManager.isRunning();
    }

    /**
     * @return <code>true</code> if the timer is running, <code>false</code> otherwise
     */
    public boolean isRunning() {
        return mPhaseManager.isRunning();
    }

    /**
     * Cleans up, should be called before deleting.
     */
    public void release() {
        stopTimer();
    }

    /**
     * Resets the currently active phase.
     */
    public void resetActivePhase() {
        mPhaseManager.reset();
    }

    /**
     * Restores the state of this <code>DebateManager</code> from a {@link Bundle}.
     * @param key A String to uniquely distinguish this <code>DebateManager</code> from any other
     *        objects that might be stored in the same Bundle.
     * @param bundle The Bundle from which to restore this information.
     */
    public void restoreState(String key, Bundle bundle) {

        // Restore the current item type
        String itemTypeValue = bundle.getString(key + BUNDLE_SUFFIX_ITEM_TYPE);
        if (itemTypeValue == null)
            Log.e(TAG, "restoreState: No item type found");
        else
            try {
                mActivePhaseType = DebatePhaseType.toEnum(itemTypeValue);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "restoreState: Invalid item type: " + itemTypeValue);
            }

        // Restore the current speech
        mActiveSpeechIndex = bundle.getInt(key + BUNDLE_SUFFIX_INDEX, 0);
        loadSpeech();

        // If there are saved speech times, restore them as well
        long[] speechTimes = bundle.getLongArray(key + BUNDLE_SUFFIX_SPEECH_TIMES);
        if (speechTimes != null)
            for (int i = 0; i < speechTimes.length; i++)
                mSpeechTimes.set(i, speechTimes[i]);

        // Restore the prep time
        mPrepTime = bundle.getLong(key + BUNDLE_SUFFIX_PREP_TIME, 0);

        mPhaseManager.restoreState(key + BUNDLE_SUFFIX_SPEECH, bundle);
    }

    /**
     * Saves the state of this <code>DebateManager</code> to a {@link Bundle}.
     * @param key A String to uniquely distinguish this <code>DebateManager</code> from any other
     *        objects that might be stored in the same Bundle.
     * @param bundle The Bundle to which to save this information.
     */
    public void saveState(String key, Bundle bundle) {

        // Take note of which item type we're in
        bundle.putString(key + BUNDLE_SUFFIX_ITEM_TYPE, mActivePhaseType.toString());

        // Take note of which speech we're on
        bundle.putInt(key + BUNDLE_SUFFIX_INDEX, mActiveSpeechIndex);

        // Save the speech times
        long[] speechTimes = new long[mSpeechTimes.size()];
        for (int i = 0; i < mSpeechTimes.size(); i++)
            speechTimes[i] = mSpeechTimes.get(i);
        bundle.putLongArray(key + BUNDLE_SUFFIX_SPEECH_TIMES, speechTimes);

        // Save the prep time
        bundle.putLong(key + BUNDLE_SUFFIX_PREP_TIME, mPrepTime);

        mPhaseManager.saveState(key + BUNDLE_SUFFIX_SPEECH, bundle);
    }

    /**
     * Sets the current time of the active phase.
     * This method will set the time even if the timer is running.
     * @param seconds the new time in seconds
     */
    public void setActivePhaseCurrentTime(long seconds) {
        mPhaseManager.setCurrentTime(seconds);
    }

    /**
     * <p>Instructs the {@link DebateManager} to switch to the phase with the given phase index.
     * Does nothing if the new phase index is the same as the old phase index.</p>
     * <p>The phase indices number consecutively starting from 0.  Note that phase indices don't
     * necessarily correlate with speeches if, for example, whether prep time is enabled changes.</p>
     * @param phaseIndex the new timer index
     */
    public void setActivePhaseIndex(int phaseIndex) {
        if (phaseIndex == getActivePhaseIndex())
            return; // do nothing if no change

        validatePhaseIndex(phaseIndex);
        saveSpeech();
        mPhaseManager.stop();

        if (hasPrepTime()) {
            if (phaseIndex == 0) {
                mActivePhaseType = DebatePhaseType.PREP_TIME;
                mActiveSpeechIndex = 0;
            } else {
                mActivePhaseType = DebatePhaseType.SPEECH;
                mActiveSpeechIndex = phaseIndex - 1;
            }
        } else {
            mActivePhaseType = DebatePhaseType.SPEECH;
            mActiveSpeechIndex = phaseIndex;
        }

        loadSpeech();

    }

    /**
     * Sets a broadcast sender for this speech manager.
     * <code>DebateManager</code> will call <code>sendBroadcast()</code> on the broadcast sender
     * when the timer counts up/down.
     * @param sender the {@link GuiUpdateBroadcastSender}
     */
    public void setBroadcastSender(GuiUpdateBroadcastSender sender) {
        this.mPhaseManager.setBroadcastSender(sender);
        this.mPoiManager.setBroadcastSender(sender);
    }

    /**
     * Sets the overtime bell specifications.
     * @param firstBell The number of seconds after the finish time to ring the first overtime bell
     * @param period The time in between subsequence overtime bells
     */
    public void setOvertimeBells(long firstBell, long period) {
        mPhaseManager.setOvertimeBells(firstBell, period);
    }

    /**
     * Sets the {@link PrepTimeBellsManager} for the prep time format.
     * If the prep time is controlled for the current format, or if there is no prep time, does nothing.
     * @param ptbm
     */
    public void setPrepTimeBellsManager(PrepTimeBellsManager ptbm) {
        if (mDebateFormat.getPrepFormat() != null)
            try {
                ((PrepTimeSimpleFormat) mDebateFormat.getPrepFormat()).setBellsManager(ptbm);
            } catch (ClassCastException e) {
                // Do nothing - this just means the bells manager isn't applicable to this
                // case.
            }
    }

    /**
     * Sets whether prep time is enabled.
     * @param prepTimeEnabled <code>true</code> if the user wants prep time to be enabled, <code>false</code>
     * otherwise
     */
    public void setPrepTimeEnabled(boolean prepTimeEnabled) {
        mPrepTimeEnabledByUser = prepTimeEnabled;

        // Switch out of prep time if necessary, since if you disable prep time it doesn't make
        // any sense to continue to be in prep time
        if (prepTimeEnabled == false) {
            if (mActivePhaseType == DebatePhaseType.PREP_TIME) {
                saveSpeech();
                mPhaseManager.stop();
                mActivePhaseType = DebatePhaseType.SPEECH;
                mActiveSpeechIndex = 0;
                loadSpeech();
            }
        }
    }

    /**
     * Starts the POI timer.
     */
    public void startPoiTimer() {
        mPoiManager.start();
    }

    /**
     * Starts the timer.
     */
    public void startTimer() {
        mPhaseManager.start();
    }

    /**
     * Stops the POI timer.
     */
    public void stopPoiTimer() {
        mPoiManager.stop();
    }

    /**
     * Stops the timer. Also stops the POI timer, since POIs can't be running when the timer is stopped.
     */
    public void stopTimer() {
        mPhaseManager.stop();
        stopPoiTimer();
    }

    //******************************************************************************************
    // Private methods
    //******************************************************************************************

    private boolean hasPrepTime() {
        return mPrepTimeEnabledByUser && mDebateFormat.hasPrepFormat();
    }

    private void loadSpeech() {
        switch (mActivePhaseType) {
        case PREP_TIME:
            mPhaseManager.loadSpeech(mDebateFormat.getPrepFormat(), getActivePhaseName(), mPrepTime);
            break;
        case SPEECH:
            mPhaseManager.loadSpeech(mDebateFormat.getSpeechFormat(mActiveSpeechIndex), getActivePhaseName(),
                    mSpeechTimes.get(mActiveSpeechIndex));
        }
    }

    private void saveSpeech() {
        switch (mActivePhaseType) {
        case PREP_TIME:
            mPrepTime = mPhaseManager.getCurrentTime();
            break;
        case SPEECH:
            mSpeechTimes.set(mActiveSpeechIndex, mPhaseManager.getCurrentTime());
        }
    }

    private void validatePhaseIndex(int phaseIndex) {
        if (phaseIndex >= mDebateFormat.numberOfSpeeches() + 1)
            throw new IndexOutOfBoundsException(
                    String.format("Position %d out of bounds, with prep time", phaseIndex));
    }

    /**
     * Converts a phase type and speech index (which together uniquely identify a phase in a
     * debate) to a phase index.  Note that phase indices for a given phase can change depending
     * on whether prep time is enabled.
     * @param type the {@link DebatePhaseType}
     * @param speechIndex the index of the speech, if applicable (ignored if not applicable)
     * @return the appropriate phase index, or <code>NO_SUCH_PHASE</code> =
     * {@link PagerAdapter}<code>.POSITION_NONE</code> if not applicable
     */
    private int findPhaseIndex(DebatePhaseType type, int speechIndex) {
        if (hasPrepTime()) {
            switch (type) {
            case PREP_TIME:
                return 0;
            case SPEECH:
                return speechIndex + 1;
            }
        } else {
            switch (type) {
            case PREP_TIME:
                return NO_SUCH_PHASE;
            case SPEECH:
                return speechIndex;
            }
        }
        return NO_SUCH_PHASE;
    }

}