net.czlee.debatekeeper.DebatingActivity.java Source code

Java tutorial

Introduction

Here is the source code for net.czlee.debatekeeper.DebatingActivity.java

Source

/*
 * Copyright (C) 2012 Phillip Cao, 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;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.Toast;

import net.czlee.debatekeeper.AlertManager.FlashScreenListener;
import net.czlee.debatekeeper.AlertManager.FlashScreenMode;
import net.czlee.debatekeeper.debateformat.BellInfo;
import net.czlee.debatekeeper.debateformat.DebateFormat;
import net.czlee.debatekeeper.debateformat.DebateFormatBuilderFromXml;
import net.czlee.debatekeeper.debateformat.DebateFormatBuilderFromXmlForSchema1;
import net.czlee.debatekeeper.debateformat.DebateFormatBuilderFromXmlForSchema2;
import net.czlee.debatekeeper.debateformat.DebatePhaseFormat;
import net.czlee.debatekeeper.debateformat.PeriodInfo;
import net.czlee.debatekeeper.debateformat.PrepTimeFormat;
import net.czlee.debatekeeper.debateformat.SpeechFormat;
import net.czlee.debatekeeper.debateformat.XmlUtilities;
import net.czlee.debatekeeper.debatemanager.DebateManager;
import net.czlee.debatekeeper.debatemanager.DebateManager.DebatePhaseTag;

import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * This is the main activity for the Debatekeeper application.  It is the launcher activity,
 * and the activity in which the user spends the most time.
 *
 * @author Phillip Cao
 * @author Chuan-Zheng Lee
 * @since  2012-04-05
 *
 */
public class DebatingActivity extends FragmentActivity {

    private static final String TAG = "DebatingActivity";

    private View mDebateTimerDisplay;
    private boolean mIsEditingTime = false;
    private final Semaphore mFlashScreenSemaphore = new Semaphore(1, true);
    private static final int COLOUR_TRANSPARENT = 0;

    private EnableableViewPager mViewPager;
    private boolean mChangingPages;

    private Button mLeftControlButton;
    private Button mLeftCentreControlButton;
    private Button mCentreControlButton;
    private Button mRightControlButton;
    private Button mPlayBellButton;

    private final ControlButtonSpec CONTROL_BUTTON_START_TIMER = new ControlButtonSpec(
            R.string.mainScreen_controlButton_startTimer_text, new ControlButtonStartTimerOnClickListener());
    private final ControlButtonSpec CONTROL_BUTTON_STOP_TIMER = new ControlButtonSpec(
            R.string.mainScreen_controlButton_stopTimer_text, new ControlButtonStopTimerOnClickListener());
    private final ControlButtonSpec CONTROL_BUTTON_CHOOSE_STYLE = new ControlButtonSpec(
            R.string.mainScreen_controlButton_chooseStyle_text, new ControlButtonChooseStyleOnClickListener());
    private final ControlButtonSpec CONTROL_BUTTON_RESET_TIMER = new ControlButtonSpec(
            R.string.mainScreen_controlButton_resetTimer_text,
            new ControlButtonResetActiveDebatePhaseOnClickListener());
    private final ControlButtonSpec CONTROL_BUTTON_RESUME_TIMER = new ControlButtonSpec(
            R.string.mainScreen_controlButton_resumeTimer_text, new ControlButtonStartTimerOnClickListener());
    private final ControlButtonSpec CONTROL_BUTTON_NEXT_PHASE = new ControlButtonSpec(
            R.string.mainScreen_controlButton_nextPhase_text, new ControlButtonNextDebatePhaseOnClickListener());

    private DebateManager mDebateManager;
    private Bundle mLastStateBundle;

    private String mFormatXmlFileName = null;
    private CountDirection mCountDirection = CountDirection.COUNT_UP;
    private CountDirection mPrepTimeCountDirection = CountDirection.COUNT_DOWN;
    private BackgroundColourArea mBackgroundColourArea = BackgroundColourArea.WHOLE_SCREEN;
    private boolean mPoiTimerEnabled = true;
    private boolean mSpeechKeepScreenOn;
    private boolean mPrepTimeKeepScreenOn;

    private boolean mDialogBlocking = false;
    private boolean mDialogWaiting = false;
    private DialogFragment mDialogFragmentInWaiting = null;
    private String mDialogTagInWaiting = null;

    private static final String BUNDLE_KEY_DEBATE_MANAGER = "dm";
    private static final String BUNDLE_KEY_XML_FILE_NAME = "xmlfn";
    private static final String PREFERENCE_XML_FILE_NAME = "xmlfn";
    private static final String LAST_CHANGELOG_VERSION_SHOWN = "lastChangeLog";
    private static final String DIALOG_ARGUMENT_FATAL_MESSAGE = "fm";
    private static final String DIALOG_ARGUMENT_XML_ERROR_LOG = "xel";
    private static final String DIALOG_ARGUMENT_SCHEMA_USED = "used";
    private static final String DIALOG_ARGUMENT_SCHEMA_SUPPORTED = "supp";
    private static final String DIALOG_ARGUMENT_FILE_NAME = "fn";
    private static final String DIALOG_TAG_SCHEMA_TOO_NEW = "toonew";
    private static final String DIALOG_TAG_ERRORS_WITH_XML = "errors";
    private static final String DIALOG_TAG_FATAL_PROBLEM = "fatal";
    private static final String DIALOG_TAG_CHANGELOG = "changelog";

    private static final int CHOOSE_STYLE_REQUEST = 0;

    private DebatingTimerService.DebatingTimerServiceBinder mBinder;
    private final BroadcastReceiver mGuiUpdateBroadcastReceiver = new GuiUpdateBroadcastReceiver();
    private final ServiceConnection mConnection = new DebatingTimerServiceConnection();

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

    public static class DialogChangelogFragment extends DialogFragment {

        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final Activity activity = getActivity();
            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
            View content = activity.getLayoutInflater().inflate(R.layout.changelog_dialog, null);
            final CheckBox doNotShowAgain = (CheckBox) content.findViewById(R.id.changelogDialog_dontShow);
            final Resources res = getResources();

            builder.setTitle(res.getString(R.string.changelogDialog_title, BuildConfig.VERSION_NAME))
                    .setView(content).setCancelable(true).setPositiveButton(
                            res.getString(R.string.changelogDialog_ok), new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    // Take note of "do not show again" setting
                                    if (doNotShowAgain.isChecked()) {
                                        SharedPreferences prefs = activity.getPreferences(MODE_PRIVATE);
                                        Editor editor = prefs.edit();
                                        int thisChangelogVersionCode = res
                                                .getInteger(R.integer.changelogDialog_versionCode);
                                        editor.putInt(LAST_CHANGELOG_VERSION_SHOWN, thisChangelogVersionCode);
                                        editor.apply();
                                    }
                                    dialog.dismiss();
                                }
                            })
                    .setOnCancelListener(new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                            dialog.dismiss();
                        }
                    });

            return builder.create();
        }

    }

    public static class DialogErrorsWithXmlFileFragment extends DialogFragment {

        static DialogErrorsWithXmlFileFragment newInstance(ArrayList<String> errorLog, String filename) {
            DialogErrorsWithXmlFileFragment fragment = new DialogErrorsWithXmlFileFragment();
            Bundle args = new Bundle();
            args.putStringArrayList(DIALOG_ARGUMENT_XML_ERROR_LOG, errorLog);
            args.putString(DIALOG_ARGUMENT_FILE_NAME, filename);
            fragment.setArguments(args);
            return fragment;
        }

        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

            StringBuilder errorMessage = new StringBuilder(
                    getString(R.string.errorsinXmlFileDialog_message_prefix));

            Bundle args = getArguments();

            ArrayList<String> errorLog = args.getStringArrayList(DIALOG_ARGUMENT_XML_ERROR_LOG);

            if (errorLog != null) {
                for (String error : errorLog) {
                    errorMessage.append("\n");
                    errorMessage.append(error);
                }
            }

            errorMessage
                    .append(getString(R.string.dialogs_fileName_suffix, args.getString(DIALOG_ARGUMENT_FILE_NAME)));

            builder.setTitle(R.string.errorsinXmlFileDialog_title).setMessage(errorMessage).setCancelable(true)
                    .setPositiveButton(R.string.errorsinXmlFileDialog_button,
                            new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    dialog.dismiss();
                                }
                            });

            return builder.create();
        }

    }

    public static class DialogFatalProblemWithXmlFileFragment extends DialogFragment {

        static DialogFatalProblemWithXmlFileFragment newInstance(String message, String filename) {
            DialogFatalProblemWithXmlFileFragment fragment = new DialogFatalProblemWithXmlFileFragment();
            Bundle args = new Bundle();
            args.putString(DIALOG_ARGUMENT_FATAL_MESSAGE, message);
            args.putString(DIALOG_ARGUMENT_FILE_NAME, filename);
            fragment.setArguments(args);
            return fragment;
        }

        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final FragmentActivity activity = getActivity();
            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
            Bundle args = getArguments();

            StringBuilder errorMessage = new StringBuilder(args.getString(DIALOG_ARGUMENT_FATAL_MESSAGE));
            errorMessage.append(getString(R.string.fatalProblemWithXmlFileDialog_message_suffix));
            errorMessage
                    .append(getString(R.string.dialogs_fileName_suffix, args.getString(DIALOG_ARGUMENT_FILE_NAME)));

            builder.setTitle(R.string.fatalProblemWithXmlFileDialog_title).setMessage(errorMessage)
                    .setCancelable(true).setPositiveButton(R.string.fatalProblemWithXmlFileDialog_button,
                            new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    Intent intent = new Intent(activity, FormatChooserActivity.class);

                                    // We want to start this from the Activity, not from this Fragment,
                                    // as the Fragment won't be active when it comes back.  See:
                                    // http://stackoverflow.com/questions/10564474/wrong-requestcode-in-onactivityresult
                                    activity.startActivityForResult(intent, CHOOSE_STYLE_REQUEST);
                                }
                            })
                    .setOnCancelListener(new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                            activity.finish();
                        }
                    });

            return builder.create();
        }

    }

    public static class DialogSchemaTooNewFragment extends DialogFragment {

        static DialogSchemaTooNewFragment newInstance(String schemaUsed, String schemaSupported, String filename) {
            DialogSchemaTooNewFragment fragment = new DialogSchemaTooNewFragment();
            Bundle args = new Bundle();
            args.putString(DIALOG_ARGUMENT_SCHEMA_USED, schemaUsed);
            args.putString(DIALOG_ARGUMENT_SCHEMA_SUPPORTED, schemaSupported);
            args.putString(DIALOG_ARGUMENT_FILE_NAME, filename);
            fragment.setArguments(args);
            return fragment;
        }

        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final DebatingActivity activity = (DebatingActivity) getActivity();
            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
            Bundle args = getArguments();

            String schemaUsed = args.getString(DIALOG_ARGUMENT_SCHEMA_USED);
            String schemaSupported = args.getString(DIALOG_ARGUMENT_SCHEMA_SUPPORTED);

            String appVersion;
            try {
                appVersion = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionName;
            } catch (NameNotFoundException e) {
                appVersion = "unknown";
            }

            StringBuilder message = new StringBuilder(
                    getString(R.string.schemaTooNewDialog_message, schemaUsed, schemaSupported, appVersion));

            message.append(getString(R.string.dialogs_fileName_suffix, args.getString(DIALOG_ARGUMENT_FILE_NAME)));

            builder.setTitle(R.string.schemaTooNewDialog_title).setMessage(message).setCancelable(false)
                    .setPositiveButton(R.string.schemaTooNewDialog_button_upgrade,
                            new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    // Open Google Play to upgrade
                                    Uri uri = Uri.parse(getString(R.string.app_marketUri));
                                    Intent intent = new Intent(Intent.ACTION_VIEW);
                                    intent.setData(uri);
                                    startActivity(intent);
                                }
                            })
                    .setNegativeButton(R.string.schemaTooNewDialog_button_ignore,
                            new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    // To ignore, just dismiss the dialog and return to whatever was happening before
                                    dialog.dismiss();
                                }
                            });

            return builder.create();
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
            ((DebatingActivity) getActivity()).showQueuedDialog();
        }
    }

    //******************************************************************************************
    // Private classes
    //******************************************************************************************

    private enum BackgroundColourArea {
        // These must match the values string array in the preference.xml file.
        // (We can pull strings from the resource automatically,
        // but we can't assign them to enums automatically.)
        DISABLED("disabled"), TOP_BAR_ONLY("topBarOnly"), WHOLE_SCREEN("wholeScreen");

        private final String key;

        BackgroundColourArea(String key) {
            this.key = key;
        }

        public static BackgroundColourArea toEnum(String key) {
            for (BackgroundColourArea value : BackgroundColourArea.values())
                if (key.equals(value.key))
                    return value;
            throw new IllegalArgumentException(String.format("There is no enumerated constant '%s'", key));
        }
    }

    private class ControlButtonSpec {
        int textResId;
        View.OnClickListener onClickListener;

        private ControlButtonSpec(int textResId, View.OnClickListener onClickListener) {
            this.textResId = textResId;
            this.onClickListener = onClickListener;
        }
    }

    private class ControlButtonChooseStyleOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(DebatingActivity.this, FormatChooserActivity.class);
            startActivityForResult(intent, CHOOSE_STYLE_REQUEST);
        }
    }

    private class ControlButtonNextDebatePhaseOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            goToNextSpeech();
            updateGui();
        }
    }

    private class ControlButtonResetActiveDebatePhaseOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            mDebateManager.resetActivePhase();
            updateGui();
        }
    }

    private class ControlButtonStartTimerOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            mDebateManager.startTimer();
            updateGui();
            updateKeepScreenOn();
        }
    }

    private class ControlButtonStopTimerOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            mDebateManager.stopTimer();
            updateGui();
            updateKeepScreenOn();
        }
    }

    private enum CountDirection {

        // These must match the values string array in the preference.xml file.
        // (We can pull strings from the resource automatically,
        // but we can't assign them to enums automatically.)
        COUNT_UP("alwaysUp"), COUNT_DOWN("alwaysDown");

        private final String key;

        CountDirection(String key) {
            this.key = key;
        }

        public static CountDirection toEnum(String key) {
            for (CountDirection value : CountDirection.values())
                if (key.equals(value.key))
                    return value;
            throw new IllegalArgumentException(String.format("There is no enumerated constant '%s'", key));
        }
    }

    private class CurrentTimeOnLongClickListener implements OnLongClickListener {

        @Override
        public boolean onLongClick(View v) {
            editCurrentTimeStart();
            return true;
        }

    }

    private class DebateTimerDisplayOnClickListener implements OnClickListener {

        @Override
        public void onClick(View v) {
            editCurrentTimeFinish(true);
        }

    }

    private class DebateTimerDisplayOnPageChangeListener extends SimpleOnPageChangeListener {

        @Override
        public void onPageSelected(int position) {
            // Log.d(TAG, "onPageSelected for position " + position);

            // Enable the lock that prevents updateGui() from running while pages are changing.
            // This is necessary to prevent updateGui() from updating the wrong view after this
            // method is run (and the active phase index changed) and before
            // DebateTimerDisplayPagerAdapter#setPrimaryItem() is called (and the view pointer
            // updated).
            mChangingPages = true;

            if (mDebateManager != null)
                mDebateManager.setActivePhaseIndex(position);
            updateControls();
        }

    }

    /**
     * Implementation of {@link PagerAdapter} that pages through the various speeches of a debate
     * managed by a {@link DebateManager}.
     *
     * @author Chuan-Zheng Lee
     * @since 2013-06-10
     *
     */
    private class DebateTimerDisplayPagerAdapter extends PagerAdapter {

        private static final String TAG = "DebateTmrDispPagAdapt";

        private final HashMap<DebatePhaseTag, View> mViewsMap = new HashMap<>();
        private static final String NO_DEBATE_LOADED = "no_debate_loaded";

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            DebatePhaseTag dpt = (DebatePhaseTag) object;
            View view = mViewsMap.get(dpt);
            if (view == null) {
                Log.e(TAG, "Nothing found to destroy at position " + position + " - " + object.toString());
                return;
            }
            container.removeView(view);
            mViewsMap.remove(dpt);
        }

        @Override
        public int getCount() {
            if (mDebateManager == null)
                return 1;
            else
                return mDebateManager.getNumberOfPhases();
        }

        @Override
        public int getItemPosition(Object object) {

            // If it was the "no debate loaded" screen and there is now a debate loaded,
            // then the View no longer exists.  Likewise if there is no debate loaded and
            // there was anything but the "no debate loaded" screen.
            DebatePhaseTag tag = (DebatePhaseTag) object;
            if ((mDebateManager == null) != (NO_DEBATE_LOADED.equals(tag.specialTag)))
                return POSITION_NONE;

            // If it was "no debate loaded" and there is still no debate loaded, it's unchanged.
            if (mDebateManager == null && NO_DEBATE_LOADED.equals(tag.specialTag))
                return POSITION_UNCHANGED;

            // That covers all situations in which mDebateManager could be null. Just to be safe:
            assert mDebateManager != null;

            // If there's no messy debate format changing or loading, delegate this function to the
            // DebateManager.
            return mDebateManager.getPhaseIndexForTag((DebatePhaseTag) object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {

            if (mDebateManager == null) {
                // Load the "no debate loaded" screen.
                Log.i(TAG, "No debate loaded");
                View v = View.inflate(DebatingActivity.this, R.layout.no_debate_loaded, null);
                container.addView(v);
                DebatePhaseTag tag = new DebatePhaseTag();
                tag.specialTag = NO_DEBATE_LOADED;
                mViewsMap.put(tag, v);
                return tag;
            }

            // The View for the position in question is the inflated debate_timer_display for
            // the relevant timer (prep time or speech).
            View v = View.inflate(DebatingActivity.this, R.layout.debate_timer_display, null);

            // OnTouchListeners
            v.setOnClickListener(new DebateTimerDisplayOnClickListener());
            v.findViewById(R.id.debateTimer_currentTime)
                    .setOnLongClickListener(new CurrentTimeOnLongClickListener());

            // Set the time picker to 24-hour time
            TimePicker currentTimePicker = (TimePicker) v.findViewById(R.id.debateTimer_currentTimePicker);
            currentTimePicker.setIs24HourView(true);

            // Set the POI timer OnClickListener
            Button poiTimerButton = (Button) v.findViewById(R.id.debateTimer_poiTimerButton);
            poiTimerButton.setOnClickListener(new PoiButtonOnClickListener());

            // Update the debate timer display
            long time = mDebateManager.getPhaseCurrentTime(position);
            DebatePhaseFormat dpf = mDebateManager.getPhaseFormat(position);
            PeriodInfo pi = dpf.getPeriodInfoForTime(time);

            updateDebateTimerDisplay(v, dpf, pi, mDebateManager.getPhaseName(position), time,
                    mDebateManager.getPhaseNextOvertimeBellTime(position));

            container.addView(v);

            // Retrieve a tag and take note of it.
            DebatePhaseTag tag = mDebateManager.getPhaseTagForIndex(position);
            mViewsMap.put(tag, v);

            return tag;

        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            DebatePhaseTag dpt = (DebatePhaseTag) object;
            return mViewsMap.containsKey(dpt) && (mViewsMap.get(dpt) == view);
        }

        @Override
        public void setPrimaryItem(ViewGroup container, int position, Object object) {

            // Log.d(TAG, "setPrimaryItem for position " + position);
            View original = mDebateTimerDisplay;

            // Note: There is no guarantee that mDebateTimerDisplay will in fact be a debate
            // timer display - it is just whatever view is currently being displayed.  Therefore,
            // other methods should check that mDebateTimerDisplay is in fact a debate timer
            // display (by comparing its ID to R.id.debateTimer_root) before working on it.
            DebatePhaseTag dpt = (DebatePhaseTag) object;
            mDebateTimerDisplay = mViewsMap.get(dpt);

            // Disable the lock that prevents updateGui() from running while the pages are
            // changing.
            mChangingPages = false;

            // This method seems to be called multiple times on each update.
            // To save unnecessary work (i.e. for performance), only run (the relatively-intensive)
            // updateGui if mDebateTimerDisplay has actually changed.
            if (original != mDebateTimerDisplay)
                updateGui();
        }

        /**
         * Refreshes all the background colours known to this {@link PagerAdapter}.
         * This should be called when a background colour user preference is changed, in a way
         * that requires all of the background colours in all {@link View}s known to be refreshed.
         * Before calling this method, <code>DebatingActivity.resetBackgroundColoursToTransparent()</code>
         * should be called to reset all of the other background colours to transparent.
         */
        public void refreshBackgroundColours() {
            if (mDebateManager == null)
                return;
            for (Entry<DebatePhaseTag, View> entry : mViewsMap.entrySet()) {
                int phaseIndex = mDebateManager.getPhaseIndexForTag(entry.getKey());
                DebatePhaseFormat dpf = mDebateManager.getPhaseFormat(phaseIndex);
                long time = mDebateManager.getPhaseCurrentTime(phaseIndex);
                PeriodInfo pi = dpf.getPeriodInfoForTime(time);
                int backgroundColour = getBackgroundColorFromPeriodInfo(dpf, pi);
                boolean overtime = time > dpf.getLength();
                int timeTextColour = getResources()
                        .getColor((overtime) ? R.color.overtimeTextColour : android.R.color.primary_text_dark);
                updateDebateTimerDisplayColours(entry.getValue(), timeTextColour, backgroundColour);
            }
        }

    }

    private class DebatingTimerFlashScreenListener implements FlashScreenListener {

        @Override
        public boolean begin() {
            boolean result;
            try {
                result = mFlashScreenSemaphore.tryAcquire(2, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                return false; // Don't bother with the flash screen any more
            }
            return result;
        }

        @Override
        public void done() {
            mFlashScreenSemaphore.release();
        }

        @Override
        public void flashScreenOff() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // Restore the original colours
                    // It takes a bit of brain-work to figure out what they should be.  We actually
                    // do this brain-work because the correct colours should be considered volatile
                    // - this timer can happen at any time so there is no guarantee they haven't
                    // changed since the last time we checked.
                    int textColour, backgroundColour;
                    Resources resources = getResources();
                    if (mDebateManager != null) {
                        DebatePhaseFormat dpf = mDebateManager.getActivePhaseFormat();
                        boolean overtime = mDebateManager.getActivePhaseCurrentTime() > dpf.getLength();
                        textColour = resources.getColor(
                                (overtime) ? R.color.overtimeTextColour : android.R.color.primary_text_dark);
                        backgroundColour = getBackgroundColorFromPeriodInfo(dpf,
                                mDebateManager.getActivePhaseCurrentPeriodInfo());
                    } else {
                        textColour = resources.getColor(android.R.color.primary_text_dark);
                        backgroundColour = COLOUR_TRANSPARENT;
                    }

                    updateDebateTimerDisplayColours(mDebateTimerDisplay, textColour, backgroundColour);

                    // Set the background colour of the root view to be black again.
                    findViewById(R.id.mainScreen_rootView)
                            .setBackgroundColor(resources.getColor(android.R.color.black));
                }
            });
        }

        @Override
        public void flashScreenOn(final int colour) {

            runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    // We need to figure out how to colour the text.
                    // Basically we want to colour the text to whatever the background colour is now.
                    // So the whole screen is coloured, it'll be the current background colour for
                    // the current period.  If not, it'll be black (make sure we don't make the
                    // text transparent though!).
                    int invertedTextColour;
                    if (mBackgroundColourArea == BackgroundColourArea.WHOLE_SCREEN && mDebateManager != null)
                        invertedTextColour = getBackgroundColorFromPeriodInfo(mDebateManager.getActivePhaseFormat(),
                                mDebateManager.getActivePhaseCurrentPeriodInfo());
                    else
                        invertedTextColour = getResources().getColor(android.R.color.black);

                    // So we invert the text colour and set all background colours to transparent.
                    // Everything will be restored by flashScreenOff().
                    updateDebateTimerDisplayColours(mDebateTimerDisplay, invertedTextColour, COLOUR_TRANSPARENT);
                    ;

                    // Having completed preparations, set the background colour of the root view to
                    // flash the screen.
                    findViewById(R.id.mainScreen_rootView).setBackgroundColor(colour);
                }
            });
        }
    }

    /**
     * Defines call-backs for service binding, passed to bindService()
     */
    private class DebatingTimerServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            mBinder = (DebatingTimerService.DebatingTimerServiceBinder) service;
            initialiseDebate();
            restoreBinder();
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            mDebateManager = null;
            mViewPager.getAdapter().notifyDataSetChanged();
        }
    }

    private class FatalXmlError extends Exception {

        private static final long serialVersionUID = -1774973645180296278L;

        public FatalXmlError(String detailMessage) {
            super(detailMessage);
        }

        public FatalXmlError(String detailMessage, Throwable throwable) {
            super(detailMessage, throwable);
        }
    }

    private final class GuiUpdateBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            updateGui();
        }
    }

    private class PlayBellButtonOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            mBinder.getAlertManager().playSingleBell();
        }
    }

    private class PoiButtonOnClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            if (mDebateManager != null) {
                if (mDebateManager.isPoiRunning())
                    mDebateManager.stopPoiTimer();
                else
                    mDebateManager.startPoiTimer();
            }
        }
    }

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

    @Override
    public void onBackPressed() {

        // If no debate is loaded, exit.
        if (mDebateManager == null)
            super.onBackPressed();

        // If we're in editing mode, exit editing mode
        else if (mIsEditingTime)
            editCurrentTimeFinish(false);

        // If the timer is stopped AND it's not the first speaker, go back one speaker.
        // Note: We do not just leave this check to goToPreviousSpeaker(), because we want to do
        // other things if it's not in a state in which it could go to the previous speaker.
        else if (!mDebateManager.isInFirstPhase() && !mDebateManager.isRunning())
            goToPreviousSpeech();

        // Otherwise, behave normally (i.e. exit).
        // Note that if the timer is running, the service will remain present in the
        // background, so this doesn't stop a running timer.
        else
            super.onBackPressed();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.debating_activity_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        editCurrentTimeFinish(false);
        switch (item.getItemId()) {
        case R.id.mainScreen_menuItem_chooseFormat:
            Intent getStyleIntent = new Intent(this, FormatChooserActivity.class);
            getStyleIntent.putExtra(FormatChooserActivity.EXTRA_XML_FILE_NAME, mFormatXmlFileName);
            startActivityForResult(getStyleIntent, CHOOSE_STYLE_REQUEST);
            return true;
        case R.id.mainScreen_menuItem_resetDebate:
            if (mDebateManager == null)
                return true;
            resetDebate();
            updateGui();
            return true;
        case R.id.mainScreen_menuItem_settings:
            startActivity(new Intent(this, GlobalSettingsActivity.class));
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem resetDebateItem = menu.findItem(R.id.mainScreen_menuItem_resetDebate);
        resetDebateItem.setEnabled(mDebateManager != null);
        return super.onPrepareOptionsMenu(menu);
    }

    //******************************************************************************************
    // Protected methods
    //******************************************************************************************

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == CHOOSE_STYLE_REQUEST) {
            if (resultCode == RESULT_OK) {
                String filename = data.getStringExtra(FormatChooserActivity.EXTRA_XML_FILE_NAME);
                if (filename != null) {
                    Log.v(TAG, "Got file name " + filename);
                    setXmlFileName(filename);
                    resetDebateWithoutToast();
                }
            } else if (resultCode == FormatChooserActivity.RESULT_ERROR) {
                Log.w(TAG, "Got error from FormatChooserActivity");
                setXmlFileName(null);
                if (mBinder != null)
                    mBinder.releaseDebateManager();
                mDebateManager = null;
                updateGui();

            }
            // Do nothing if cancelled
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_debate);

        mLeftControlButton = (Button) findViewById(R.id.mainScreen_leftControlButton);
        mLeftCentreControlButton = (Button) findViewById(R.id.mainScreen_leftCentreControlButton);
        mCentreControlButton = (Button) findViewById(R.id.mainScreen_centreControlButton);
        mRightControlButton = (Button) findViewById(R.id.mainScreen_rightControlButton);
        mPlayBellButton = (Button) findViewById(R.id.mainScreen_playBellButton);

        //
        // ViewPager
        mViewPager = (EnableableViewPager) findViewById(R.id.mainScreen_debateTimerViewPager);
        mViewPager.setAdapter(new DebateTimerDisplayPagerAdapter());
        mViewPager.addOnPageChangeListener(new DebateTimerDisplayOnPageChangeListener());
        mViewPager.setPageMargin(1);
        mViewPager.setPageMarginDrawable(R.drawable.divider);

        //
        // OnClickListeners
        mPlayBellButton.setOnClickListener(new PlayBellButtonOnClickListener());

        mLastStateBundle = savedInstanceState; // This could be null

        //
        // Find the style file name
        String filename = loadXmlFileName();

        // If there doesn't appear to be an existing style selected, then start
        // the Activity to select the style immediately, and don't bother with the
        // rest.
        if (filename == null) {
            Intent getStyleIntent = new Intent(DebatingActivity.this, FormatChooserActivity.class);
            startActivityForResult(getStyleIntent, CHOOSE_STYLE_REQUEST);
        }

        //
        // Start the timer service
        Intent intent = new Intent(this, DebatingTimerService.class);
        startService(intent);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

        //
        // If there's been an update, show the changelog.
        SharedPreferences prefs = getPreferences(MODE_PRIVATE);
        Resources res = getResources();
        int thisChangelogVersion = res.getInteger(R.integer.changelogDialog_versionCode);
        int lastChangelogVersionShown = prefs.getInt(LAST_CHANGELOG_VERSION_SHOWN, 0);
        if (lastChangelogVersionShown < thisChangelogVersion) {
            if (isFirstInstall()) {
                // Don't show on the dialog on first install, but take note of the version.
                Editor editor = prefs.edit();
                editor.putInt(LAST_CHANGELOG_VERSION_SHOWN, thisChangelogVersion);
                editor.apply();
            } else {
                // The dialog will update the preference to the new version code.
                showDialog(new DialogChangelogFragment(), DIALOG_TAG_CHANGELOG);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unbindService(mConnection);

        boolean keepRunning = false;
        if (mDebateManager != null) {
            if (mDebateManager.isRunning()) {
                keepRunning = true;
            }
        }
        if (!keepRunning) {
            Intent intent = new Intent(this, DebatingTimerService.class);
            stopService(intent);
            Log.i(TAG, "Timer is not running, stopped service");
        } else {
            Log.i(TAG, "Timer is running, keeping service alive");
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle bundle) {
        bundle.putString(BUNDLE_KEY_XML_FILE_NAME, mFormatXmlFileName);
        if (mDebateManager != null)
            mDebateManager.saveState(BUNDLE_KEY_DEBATE_MANAGER, bundle);
    }

    @Override
    protected void onStart() {
        super.onStart();
        restoreBinder();
        LocalBroadcastManager.getInstance(this).registerReceiver(mGuiUpdateBroadcastReceiver,
                new IntentFilter(DebatingTimerService.UPDATE_GUI_BROADCAST_ACTION));

        updateGui();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBinder != null) {
            AlertManager am = mBinder.getAlertManager();
            if (am != null) {
                am.activityStop();
            }
        }
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mGuiUpdateBroadcastReceiver);
    }

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

    /**
     * Gets the preferences from the shared preferences file and applies them.
     */
    private void applyPreferences() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        boolean silentMode, vibrateMode, overtimeBellsEnabled;
        boolean poiBuzzerEnabled, poiVibrateEnabled, prepTimerEnabled;
        int firstOvertimeBell, overtimeBellPeriod;
        String userCountDirectionValue, userPrepTimeCountDirectionValue, poiFlashScreenModeValue,
                backgroundColourAreaValue;
        FlashScreenMode flashScreenMode, poiFlashScreenMode;

        Resources res = getResources();

        final String TAG = "applyPreferences";

        try {

            // The boolean preferences
            silentMode = prefs.getBoolean(res.getString(R.string.pref_silentMode_key),
                    res.getBoolean(R.bool.prefDefault_silentMode));
            vibrateMode = prefs.getBoolean(res.getString(R.string.pref_vibrateMode_key),
                    res.getBoolean(R.bool.prefDefault_vibrateMode));
            overtimeBellsEnabled = prefs.getBoolean(res.getString(R.string.pref_overtimeBellsEnable_key),
                    res.getBoolean(R.bool.prefDefault_overtimeBellsEnable));

            mSpeechKeepScreenOn = prefs.getBoolean(res.getString(R.string.pref_keepScreenOn_key),
                    res.getBoolean(R.bool.prefDefault_keepScreenOn));
            mPrepTimeKeepScreenOn = prefs.getBoolean(res.getString(R.string.pref_prepTimer_keepScreenOn_key),
                    res.getBoolean(R.bool.prefDefault_prepTimer_keepScreenOn));

            mPoiTimerEnabled = prefs.getBoolean(res.getString(R.string.pref_poiTimer_enable_key),
                    res.getBoolean(R.bool.prefDefault_poiTimer_enable));
            poiBuzzerEnabled = prefs.getBoolean(res.getString(R.string.pref_poiTimer_buzzerEnable_key),
                    res.getBoolean(R.bool.prefDefault_poiTimer_buzzerEnable));
            poiVibrateEnabled = prefs.getBoolean(res.getString(R.string.pref_poiTimer_vibrateEnable_key),
                    res.getBoolean(R.bool.prefDefault_poiTimer_vibrateEnable));

            prepTimerEnabled = prefs.getBoolean(res.getString(R.string.pref_prepTimer_enable_key),
                    res.getBoolean(R.bool.prefDefault_prepTimer_enable));

            // Overtime bell integers
            if (overtimeBellsEnabled) {
                firstOvertimeBell = prefs.getInt(res.getString(R.string.pref_firstOvertimeBell_key),
                        res.getInteger(R.integer.prefDefault_firstOvertimeBell));
                overtimeBellPeriod = prefs.getInt(res.getString(R.string.pref_overtimeBellPeriod_key),
                        res.getInteger(R.integer.prefDefault_overtimeBellPeriod));
            } else {
                firstOvertimeBell = 0;
                overtimeBellPeriod = 0;
            }

            // List preference: POI flash screen mode
            poiFlashScreenModeValue = prefs.getString(res.getString(R.string.pref_poiTimer_flashScreenMode_key),
                    res.getString(R.string.prefDefault_poiTimer_flashScreenMode));
            poiFlashScreenMode = FlashScreenMode.toEnum(poiFlashScreenModeValue);

            // List preference: Count direction
            //  - Backwards compatibility measure
            // This changed in version 0.9, to remove the generallyUp and generallyDown options.
            // Therefore, if we find either of those, we need to replace it with alwaysUp or
            // alwaysDown, respectively.
            userCountDirectionValue = prefs.getString(res.getString(R.string.pref_countDirection_key),
                    res.getString(R.string.prefDefault_countDirection));
            if (userCountDirectionValue.equals("generallyUp") || userCountDirectionValue.equals("generallyDown")) {
                // Replace the preference with alwaysUp or alwaysDown, respectively.
                SharedPreferences.Editor editor = prefs.edit();
                String newValue = (userCountDirectionValue.equals("generallyUp")) ? "alwaysUp" : "alwaysDown";
                editor.putString(res.getString(R.string.pref_countDirection_key), newValue);
                editor.apply();
                Log.i(TAG, "countDirection: replaced " + userCountDirectionValue + " with " + newValue);
                userCountDirectionValue = newValue;
            }
            mCountDirection = CountDirection.toEnum(userCountDirectionValue);

            // List preference: Count direction for prep time
            userPrepTimeCountDirectionValue = prefs.getString(
                    res.getString(R.string.pref_prepTimer_countDirection_key),
                    res.getString(R.string.prefDefault_prepTimer_countDirection));
            mPrepTimeCountDirection = CountDirection.toEnum(userPrepTimeCountDirectionValue);

            // List preference: Background colour area
            BackgroundColourArea oldBackgroundColourArea = mBackgroundColourArea;
            backgroundColourAreaValue = prefs.getString(res.getString(R.string.pref_backgroundColourArea_key),
                    res.getString(R.string.prefDefault_backgroundColourArea));
            mBackgroundColourArea = BackgroundColourArea.toEnum(backgroundColourAreaValue);
            if (oldBackgroundColourArea != mBackgroundColourArea) {
                Log.v(TAG, "background colour preference changed - refreshing");
                resetBackgroundColoursToTransparent();
                ((DebateTimerDisplayPagerAdapter) mViewPager.getAdapter()).refreshBackgroundColours();
            }

            // List preference: Flash screen mode
            //  - Backwards compatibility measure
            // This changed from a boolean to a list preference in version 0.6, so there is
            // backwards compatibility to take care of.  Backwards compatibility applies if
            // (a) the list preference is NOT present AND (b) the boolean preference IS present.
            // In this case, retrieve the boolean preference, delete it and write the corresponding
            // list preference.  In all other cases, just take the list preference (using the
            // normal default mechanism if it isn't present, i.e. neither are present).

            if (!prefs.contains(res.getString(R.string.pref_flashScreenMode_key))
                    && prefs.contains(res.getString(R.string.pref_flashScreenBool_key))) {
                // Boolean preference.
                // First, get the string and convert it to an enum.
                boolean flashScreenModeBool = prefs.getBoolean(res.getString(R.string.pref_flashScreenBool_key),
                        false);
                flashScreenMode = (flashScreenModeBool) ? FlashScreenMode.SOLID_FLASH : FlashScreenMode.OFF;

                // Then, convert that enum to the list preference value (a string) and write that
                // back to the preferences.  Also, remove the old boolean preference.
                String flashStringModePrefValue = flashScreenMode.toPrefValue();
                SharedPreferences.Editor editor = prefs.edit();
                editor.putString(res.getString(R.string.pref_flashScreenMode_key), flashStringModePrefValue);
                editor.remove(res.getString(R.string.pref_flashScreenBool_key));
                editor.apply();
                Log.i(TAG, "flashScreenMode: replaced boolean preference with list preference: "
                        + flashStringModePrefValue);

            } else {
                // List preference.
                // Get the string and convert it to an enum.
                String flashScreenModeValue;
                flashScreenModeValue = prefs.getString(res.getString(R.string.pref_flashScreenMode_key),
                        res.getString(R.string.prefDefault_flashScreenMode));
                flashScreenMode = FlashScreenMode.toEnum(flashScreenModeValue);
            }

        } catch (ClassCastException e) {
            Log.e(TAG, "caught ClassCastException!");
            return;
        }

        if (mDebateManager != null) {
            mDebateManager.setOvertimeBells(firstOvertimeBell, overtimeBellPeriod);
            mDebateManager.setPrepTimeEnabled(prepTimerEnabled);
            applyPrepTimeBells();

            // This is necessary if the debate structure has changed, i.e. if prep time has been
            // enabled or disabled.
            mViewPager.getAdapter().notifyDataSetChanged();

        } else {
            Log.v(TAG, "Couldn't restore overtime bells, mDebateManager doesn't yet exist");
        }

        if (mBinder != null) {
            AlertManager am = mBinder.getAlertManager();

            // Volume control stream is linked to silent mode
            am.setSilentMode(silentMode);
            setVolumeControlStream((silentMode) ? AudioManager.STREAM_RING : AudioManager.STREAM_MUSIC);

            am.setVibrateMode(vibrateMode);
            am.setFlashScreenMode(flashScreenMode);

            am.setPoiBuzzerEnabled(poiBuzzerEnabled);
            am.setPoiVibrateEnabled(poiVibrateEnabled);
            am.setPoiFlashScreenMode(poiFlashScreenMode);

            this.updateKeepScreenOn();

            Log.v(TAG, "successfully applied");
        } else {
            Log.v(TAG, "Couldn't restore AlertManager preferences; mBinder doesn't yet exist");
        }

    }

    private void applyPrepTimeBells() {
        PrepTimeBellsManager ptbm = new PrepTimeBellsManager(this);
        SharedPreferences prefs = getSharedPreferences(PrepTimeBellsManager.PREP_TIME_BELLS_PREFERENCES_NAME,
                MODE_PRIVATE);
        ptbm.loadFromPreferences(prefs);
        mDebateManager.setPrepTimeBellsManager(ptbm);
    }

    /**
     * Builds a <code>DebateFormat</code> from a specified XML file. Shows a <code>Dialog</code> if
     * the debate format builder logged non-fatal errors.
     * @param filename the file name of the XML file
     * @return the built <code>DebateFormat</code>
     * @throws FatalXmlError if there was any problem, which could include:
     * <ul><li>A problem opening or reading the file</li>
     * <li>A problem parsing the XML file</li>
     * <li>That there were no speeches in this debate format</li>
     * </ul>
     * The message of the exception will be human-readable and can be displayed in a dialogue box.
     */
    private DebateFormat buildDebateFromXml(String filename) throws FatalXmlError {

        DebateFormatBuilderFromXml dfbfx;

        InputStream is;
        DebateFormat df;
        FormatXmlFilesManager filesManager = new FormatXmlFilesManager(this);

        try {
            is = filesManager.open(filename);
        } catch (IOException e) {
            throw new FatalXmlError(getString(R.string.fatalProblemWithXmlFileDialog_message_cannotFind), e);
        }

        dfbfx = new DebateFormatBuilderFromXmlForSchema2(this);

        // First try schema 2.0
        try {
            df = dfbfx.buildDebateFromXml(is);
        } catch (IOException e) {
            throw new FatalXmlError(getString(R.string.fatalProblemWithXmlFileDialog_message_cannotRead), e);
        } catch (SAXException e) {
            throw new FatalXmlError(
                    getString(R.string.fatalProblemWithXmlFileDialog_message_badXml, e.getMessage()), e);
        }

        // If the schema wasn't supported, try schema 1.0 to see if it works
        if (!dfbfx.isSchemaSupported()) {

            DebateFormat df1;
            DebateFormatBuilderFromXml dfbfx1 = new DebateFormatBuilderFromXmlForSchema1(this);

            try {
                is.close();
                is = filesManager.open(filename);
            } catch (IOException e) {
                throw new FatalXmlError(getString(R.string.fatalProblemWithXmlFileDialog_message_cannotFind), e);
            }

            try {
                df1 = dfbfx1.buildDebateFromXml(is);
            } catch (IOException e) {
                throw new FatalXmlError(getString(R.string.fatalProblemWithXmlFileDialog_message_cannotRead), e);
            } catch (SAXException e) {
                throw new FatalXmlError(
                        getString(R.string.fatalProblemWithXmlFileDialog_message_badXml, e.getMessage()), e);
            }

            // If it's looking good, replace.
            // (Otherwise, pretend this schema 1.0 attempt never happened.)
            if (dfbfx1.isSchemaSupported()) {
                df = df1;
                dfbfx = dfbfx1;
            }
        }

        // If the schema still isn't supported (even after possibly having been replaced by
        // schema 1.0), prompt the user to upgrade the app.
        if (dfbfx.isSchemaTooNew()) {
            DialogFragment fragment = DialogSchemaTooNewFragment.newInstance(dfbfx.getSchemaVersion(),
                    dfbfx.getSupportedSchemaVersion(), filename);
            showBlockingDialog(fragment, DIALOG_TAG_SCHEMA_TOO_NEW);
        }

        if (df.numberOfSpeeches() == 0)
            throw new FatalXmlError(getString(R.string.fatalProblemWithXmlFileDialog_message_noSpeeches));

        if (dfbfx.hasErrors()) {
            DialogFragment fragment = DialogErrorsWithXmlFileFragment.newInstance(dfbfx.getErrorLog(),
                    mFormatXmlFileName);
            queueDialog(fragment, DIALOG_TAG_ERRORS_WITH_XML);
        }

        return df;
    }

    /**
     * Finishes editing the current time and restores the GUI to its prior state.
     * @param save true if the edited time should become the new current time, false if it should
     * be discarded.
     */
    private void editCurrentTimeFinish(boolean save) {

        TimePicker currentTimePicker = (TimePicker) mDebateTimerDisplay
                .findViewById(R.id.debateTimer_currentTimePicker);

        if (currentTimePicker == null) {
            Log.e(TAG, "editCurrentTimeFinish: currentTimePicker was null");
            return;
        }

        currentTimePicker.clearFocus();

        // Hide the keyboard
        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(currentTimePicker.getWindowToken(), 0);

        if (save && mDebateManager != null && mIsEditingTime) {
            // We're using this in hours and minutes, not minutes and seconds
            int minutes = currentTimePicker.getCurrentHour();
            int seconds = currentTimePicker.getCurrentMinute();
            long newTime = minutes * 60 + seconds;
            // Invert the time if in count-down mode
            newTime = subtractFromSpeechLengthIfCountingDown(newTime);
            mDebateManager.setActivePhaseCurrentTime(newTime);
        }

        mIsEditingTime = false;

        updateGui();

    }

    /**
     * Displays the time picker to edit the current time.
     * Does nothing if there is no debate loaded or if the timer is running.
     */
    private void editCurrentTimeStart() {

        // Check that things are in a valid state to enter edit time mode
        // If they aren't, return straight away
        if (mDebateManager == null)
            return;
        if (mDebateManager.isRunning())
            return;

        // Only if things were in a valid state do we enter edit time mode
        mIsEditingTime = true;

        TimePicker currentTimePicker = (TimePicker) mDebateTimerDisplay
                .findViewById(R.id.debateTimer_currentTimePicker);

        if (currentTimePicker == null) {
            Log.e(TAG, "editCurrentTimeFinish: currentTimePicker was null");
            return;
        }

        long currentTime = mDebateManager.getActivePhaseCurrentTime();

        // Invert the time if in count-down mode
        currentTime = subtractFromSpeechLengthIfCountingDown(currentTime);

        // Limit to the allowable time range
        if (currentTime < 0) {
            currentTime = 0;
            Toast.makeText(this, R.string.mainScreen_toast_editTextDiscardChangesInfo_limitedBelow,
                    Toast.LENGTH_LONG).show();
        }
        if (currentTime >= 24 * 60) {
            currentTime = 24 * 60 - 1;
            Toast.makeText(this, R.string.mainScreen_toast_editTextDiscardChangesInfo_limitedAbove,
                    Toast.LENGTH_LONG).show();
        }

        // We're using this in hours and minutes, not minutes and seconds
        currentTimePicker.setCurrentHour((int) (currentTime / 60));
        currentTimePicker.setCurrentMinute((int) (currentTime % 60));

        updateGui();

        // If we had to limit the time, display a helpful/apologetic message informing the user
        // of how to discard their changes, since they can't recover the time.

    }

    /**
     * Returns the count direction that should currently be used.
     * This method used to assemble the speech format and user count directions to find the
     * count direction to use.  In version 0.9, the speech format count direction was made
     * obsolete, so the only thing it has to take into account now is the user count direction.
     * However, because of the addition of a separate prep time count direction, there is still
     * some brain-work to do.
     * @return CountDirection.COUNT_UP or CountDirection.COUNT_DOWN
     */
    private CountDirection getCountDirection(DebatePhaseFormat dpf) {
        if (dpf.isPrep())
            return mPrepTimeCountDirection;
        else
            return mCountDirection;
    }

    /**
     * @param dpf the {@link DebatePhaseFormat}
     * @param pi the current {@link PeriodInfo}
     * @return the appropriate background colour
     */
    private int getBackgroundColorFromPeriodInfo(DebatePhaseFormat dpf, PeriodInfo pi) {
        Integer backgroundColour = pi.getBackgroundColor();

        if (backgroundColour == null)
            backgroundColour = getResources()
                    .getColor((dpf.isPrep()) ? R.color.prepTimeBackgroundColour : android.R.color.background_dark);

        return backgroundColour;
    }

    /**
     * Goes to the next speech.
     * Does nothing if there is no debate loaded, if the current speech is the last speech, if
     * the timer is running, or if the current time is being edited.
     */
    private void goToNextSpeech() {

        if (mDebateManager == null)
            return;
        if (mDebateManager.isRunning())
            return;
        if (mDebateManager.isInLastPhase())
            return;
        if (mIsEditingTime)
            return;

        mDebateManager.goToNextPhase();
        mViewPager.setCurrentItem(mDebateManager.getActivePhaseIndex());

        updateGui();
    }

    /**
     * Goes to the previous speech.
     * Does nothing if there is no debate loaded, if the current speech is the first speech, if
     * the timer is running, or if the current time is being edited.
     */
    private void goToPreviousSpeech() {

        if (mDebateManager == null)
            return;
        if (mDebateManager.isRunning())
            return;
        if (mDebateManager.isInFirstPhase())
            return;
        if (mIsEditingTime)
            return;

        mDebateManager.goToPreviousPhase();
        mViewPager.setCurrentItem(mDebateManager.getActivePhaseIndex());

        updateGui();
    }

    private void initialiseDebate() {
        if (mFormatXmlFileName == null) {
            Log.w(TAG, "Tried to initialise debate with null file");
            return;
        }

        mDebateManager = mBinder.getDebateManager();
        if (mDebateManager == null) {

            DebateFormat df;
            try {
                df = buildDebateFromXml(mFormatXmlFileName);
            } catch (FatalXmlError e) {
                DialogFragment fragment = DialogFatalProblemWithXmlFileFragment.newInstance(e.getMessage(),
                        mFormatXmlFileName);
                queueDialog(fragment, DIALOG_TAG_FATAL_PROBLEM);

                // We still need to notify of a data set change when there ends up being no
                // debate format
                mViewPager.getAdapter().notifyDataSetChanged();
                return;
            }

            mDebateManager = mBinder.createDebateManager(df);

            // We only restore the state if there wasn't an existing debate, i.e. if the service
            // wasn't already running, and if the debate format stored in the saved instance state
            // matches the debate format we're using now.  Also, only do this once (so set it to
            // null once restored).
            if (mLastStateBundle != null) {
                String xmlFileName = mLastStateBundle.getString(BUNDLE_KEY_XML_FILE_NAME);
                if (xmlFileName != null && xmlFileName.equals(mFormatXmlFileName))
                    mDebateManager.restoreState(BUNDLE_KEY_DEBATE_MANAGER, mLastStateBundle);
                mLastStateBundle = null;
            }
        }

        mViewPager.getAdapter().notifyDataSetChanged();
        mViewPager.setCurrentItem(mDebateManager.getActivePhaseIndex(), false);
        applyPreferences();
        updateGui();
    }

    /**
     * Returns whether or not this is the user's first time opening the app.
     * @return true if it is the first time, false otherwise.
     */
    private boolean isFirstInstall() {
        PackageInfo info;
        try {
            info = getPackageManager().getPackageInfo(getPackageName(), 0);
        } catch (NameNotFoundException e) {
            Log.e(TAG, "isFirstInstall: Can't find package info, assuming it is the first install");
            return true;
        }

        Log.v(TAG, String.format("isFirstInstall: %d vs %d", info.firstInstallTime, info.lastUpdateTime));
        return info.firstInstallTime == info.lastUpdateTime;
    }

    private String loadXmlFileName() {
        SharedPreferences sp = getPreferences(MODE_PRIVATE);
        String filename = sp.getString(PREFERENCE_XML_FILE_NAME, null);
        mFormatXmlFileName = filename;
        return filename;
    }

    /**
     * Queues a dialog to be shown after a currently-shown dialog, or immediately if there is
     * no currently-shown dialog.  This does not happen automatically - dialogs must know whether
     * they are potentially blocking or waiting, and set themselves up accordingly.  Dialogs
     * that could block must set <code>mDialogBlocking</code> to true when they are shown, and call
     * <code>showQueuedDialog()</code> when they are dismissed.
     * Dialogs that could be queued must call <code>queueDialog()</code> instead of <code>showDialog()</code>.
     * Only one dialog may be queued at a time.  If more than one dialog is queued, only the last
     * one is kept in the queue; all others are discarded.
     * @param fragment the {@link DialogFragment} that would be passed to showDialog()
     * @param tag the tag that would be passed to showDialog()
     */
    private void queueDialog(DialogFragment fragment, String tag) {
        if (!mDialogBlocking) {
            showDialog(fragment, tag);
            return;
        }

        mDialogWaiting = true;
        mDialogFragmentInWaiting = fragment;
        mDialogTagInWaiting = tag;
    }

    /**
     * Resets the background colour to the default.
     * This should be called whenever the background colour preference is changed, as <code>updateGui()</code>
     * doesn't automatically do this (for efficiency). You should call <code>updateGui()</code> as immediately as
     * practicable after calling this.
     */
    private void resetBackgroundColoursToTransparent() {
        for (int i = 0; i < mViewPager.getChildCount(); i++) {
            View v = mViewPager.getChildAt(i);
            if (v.getId() == R.id.debateTimer_root) {
                v.setBackgroundColor(COLOUR_TRANSPARENT);
                View speechNameText = v.findViewById(R.id.debateTimer_speechNameText);
                View periodDescriptionText = v.findViewById(R.id.debateTimer_periodDescriptionText);
                speechNameText.setBackgroundColor(COLOUR_TRANSPARENT);
                periodDescriptionText.setBackgroundColor(COLOUR_TRANSPARENT);
            }
        }
    }

    private void resetDebate() {
        resetDebateWithoutToast();
        Toast.makeText(this, R.string.mainScreen_toast_resetDebate, Toast.LENGTH_SHORT).show();
    }

    private void resetDebateWithoutToast() {
        if (mBinder == null)
            return;
        mBinder.releaseDebateManager();
        initialiseDebate();
    }

    private void restoreBinder() {
        if (mBinder != null) {
            AlertManager am = mBinder.getAlertManager();
            if (am != null) {
                am.setFlashScreenListener(new DebatingTimerFlashScreenListener());
                am.activityStart();
            }
        }

        // Always apply preferences after restoring the binder, as some of the preferences
        // apply to the binder.
        applyPreferences();
    }

    /**
     *  Sets up a single button
     */
    private void setButton(Button button, ControlButtonSpec spec) {
        if (spec == null)
            button.setVisibility(View.GONE);
        else {
            button.setText(spec.textResId);
            button.setOnClickListener(spec.onClickListener);
            button.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Sets up the buttons according to the {@link ControlButtonSpec}s specified.  If the centre
     * button is blank and the left and right are not, a "left-centre" button is used in place
     * of the left button; <i>i.e.</i> the left button has "double weight" of sorts.
     * @param left the {@link ControlButtonSpec} for the left button
     * @param centre the {@link ControlButtonSpec} for the centre button
     * @param right the {@link ControlButtonSpec} for the right button
     */
    private void setButtons(ControlButtonSpec left, ControlButtonSpec centre, ControlButtonSpec right) {

        if (left != null && centre == null && right != null) {
            setButton(mLeftCentreControlButton, left);
            setButton(mLeftControlButton, null);
            setButton(mCentreControlButton, null);
        } else {
            setButton(mLeftCentreControlButton, null);
            setButton(mLeftControlButton, left);
            setButton(mCentreControlButton, centre);
        }

        setButton(mRightControlButton, right);
    }

    /**
     * Enables or disables all of the control buttons (except for the "Bell" button).  If
     * <code>mDebateManager</code> is <code>null</code>, this does nothing.
     * @param enable <code>true</code> to enable, <code>false</code> to disable
     */
    private void setButtonsEnable(boolean enable) {
        if (mDebateManager == null)
            return;
        mLeftControlButton.setEnabled(enable);
        mLeftCentreControlButton.setEnabled(enable);
        mCentreControlButton.setEnabled(enable);
        // Disable the [Next Speaker] button if there are no more speakers
        mRightControlButton.setEnabled(enable && !mDebateManager.isInLastPhase());
    }

    private void setXmlFileName(String filename) {
        mFormatXmlFileName = filename;
        SharedPreferences sp = getPreferences(MODE_PRIVATE);
        Editor editor = sp.edit();
        if (filename != null)
            editor.putString(PREFERENCE_XML_FILE_NAME, filename);
        else
            editor.remove(PREFERENCE_XML_FILE_NAME);
        editor.apply();
    }

    private void showBlockingDialog(DialogFragment fragment, String tag) {
        mDialogBlocking = true;
        showDialog(fragment, tag);
    }

    /**
     * Shows the dialog given, allowing state loss as a workaround for a bug in Android
     * (possibly issue #7132432)
     *
     * <p>For more information see:</p>
     * <ul>
     * <li>https://github.com/android/platform_frameworks_support/commit/4ccc001f3f883190ac8d900c4f69d71fda94690e</li>
     * <li>http://stackoverflow.com/questions/12105064/actions-in-onactivityresult-and-error-can-not-perform-this-action-after-onsavei</li>
     * <li>http://stackoverflow.com/questions/14262312/java-lang-illegalstateexception-can-not-perform-this-action-after-onsaveinstanc</li>
     * </ul>
     *
     * @param fragment a {@link DialogFragment} instance
     * @param tag the tag for the fragment
     */
    private void showDialog(DialogFragment fragment, String tag) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(fragment, tag);
        ft.commitAllowingStateLoss();
    }

    /**
     * Shows the currently-queued dialog if there is one; does nothing otherwise.  Dialogs that
     * could block other dialogs must call this method on dismissal.
     */
    private void showQueuedDialog() {
        if (mDialogWaiting) {
            showDialog(mDialogFragmentInWaiting, mDialogTagInWaiting);
            mDialogBlocking = false;
            mDialogWaiting = false;
            mDialogFragmentInWaiting = null;
            mDialogTagInWaiting = null;
        }
    }

    /**
     * Returns the number of seconds that would be displayed, taking into account the count
     * direction.  If the overall count direction is <code>COUNT_DOWN</code> and there is a speech
     * format ready, it returns (speechLength - time).  Otherwise, it just returns time.
     * @param time the time that is wished to be formatted (in seconds)
     * @return the time that would be displayed (as an integer, number of seconds)
     */
    private long subtractFromSpeechLengthIfCountingDown(long time) {
        if (mDebateManager != null)
            return subtractFromSpeechLengthIfCountingDown(time, mDebateManager.getActivePhaseFormat());
        return time;
    }

    private long subtractFromSpeechLengthIfCountingDown(long time, DebatePhaseFormat sf) {
        if (getCountDirection(sf) == CountDirection.COUNT_DOWN)
            return sf.getLength() - time;
        return time;
    }

    /**
     *  Updates the buttons according to the current status of the debate
     *  The buttons are allocated as follows:
     *  When at start:          [Start] [Next]
     *  When running:           [Stop]
     *  When stopped by user:   [Resume] [Restart] [Next]
     *  When stopped by alarm:  [Resume]
     *  The [Bell] button always is on the right of any of the above three buttons.
     */
    private void updateControls() {
        if (mDebateTimerDisplay == null)
            return;

        if (mDebateManager != null && mDebateTimerDisplay.getId() == R.id.debateTimer_root) {

            View currentTimeText = mDebateTimerDisplay.findViewById(R.id.debateTimer_currentTime);
            View currentTimePicker = mDebateTimerDisplay.findViewById(R.id.debateTimer_currentTimePicker);

            // If it's the last speaker, don't show a "next speaker" button.
            // Show a "restart debate" button instead.
            switch (mDebateManager.getTimerStatus()) {
            case NOT_STARTED:
                setButtons(CONTROL_BUTTON_START_TIMER, null, CONTROL_BUTTON_NEXT_PHASE);
                break;
            case RUNNING:
                setButtons(CONTROL_BUTTON_STOP_TIMER, null, null);
                break;
            case STOPPED_BY_BELL:
                setButtons(CONTROL_BUTTON_RESUME_TIMER, null, null);
                break;
            case STOPPED_BY_USER:
                setButtons(CONTROL_BUTTON_RESUME_TIMER, CONTROL_BUTTON_RESET_TIMER, CONTROL_BUTTON_NEXT_PHASE);
                break;
            }

            currentTimeText.setVisibility((mIsEditingTime) ? View.GONE : View.VISIBLE);
            currentTimePicker.setVisibility((mIsEditingTime) ? View.VISIBLE : View.GONE);

            setButtonsEnable(!mIsEditingTime);
            currentTimeText.setLongClickable(!mDebateManager.isRunning());
            mViewPager.setPagingEnabled(!mIsEditingTime && !mDebateManager.isRunning());

        } else {
            // If no debate is loaded, show only one control button, which leads the user to
            // choose a style. (Keep the play bell button enabled.)
            setButtons(CONTROL_BUTTON_CHOOSE_STYLE, null, null);
            mLeftControlButton.setEnabled(true);
            mCentreControlButton.setEnabled(false);
            mRightControlButton.setEnabled(false);

            // This seems counter-intuitive, but we enable paging if there is no debate loaded,
            // as there is only one page anyway, and this way the "scrolled to the limit"
            // indicators appear on the screen.
            mViewPager.setPagingEnabled(true);
        }

        // Show or hide the [Bell] button
        updatePlayBellButton();
    }

    /**
     * Updates the debate timer display with the current active debate phase information.
     */
    private void updateDebateTimerDisplay() {
        if (mDebateManager == null) {
            Log.w("updateDebateTmrDisplay", "mDebateManager was null");
            mViewPager.getAdapter().notifyDataSetChanged();
            return;
        }

        updateDebateTimerDisplay(mDebateTimerDisplay, mDebateManager.getActivePhaseFormat(),
                mDebateManager.getActivePhaseCurrentPeriodInfo(), mDebateManager.getActivePhaseName(),
                mDebateManager.getActivePhaseCurrentTime(), mDebateManager.getActivePhaseNextOvertimeBellTime());
    }

    /**
     * Updates a debate timer display with relevant information.
     * @param debateTimerDisplay a {@link View} which should normally be the <code>RelativeLayout</code> in debate_timer_display.xml.
     * @param dpf the {@link DebatePhaseFormat} to be displayed
     * @param pi the {@link PeriodInfo} to be displayed, should be the current one
     * @param phaseName the name of the debate phase
     * @param time the current time in the debate phase
     * @param nextOvertimeBellTime the next overtime bell in the debate phase
     */
    private void updateDebateTimerDisplay(View debateTimerDisplay, DebatePhaseFormat dpf, PeriodInfo pi,
            String phaseName, long time, Long nextOvertimeBellTime) {

        // Make sure it makes sense to run this method now

        if (debateTimerDisplay == null) {
            Log.w("updateDebateTmrDisplay", "debateTimerDisplay was null");
            return;
        }
        if (debateTimerDisplay.getId() != R.id.debateTimer_root) {
            Log.w("updateDebateTmrDisplay", "debateTimerDisplay was not the debate timer display");
            return;
        }

        // If it passed all those checks, populate the timer display

        TextView periodDescriptionText = (TextView) debateTimerDisplay
                .findViewById(R.id.debateTimer_periodDescriptionText);
        TextView speechNameText = (TextView) debateTimerDisplay.findViewById(R.id.debateTimer_speechNameText);
        TextView currentTimeText = (TextView) debateTimerDisplay.findViewById(R.id.debateTimer_currentTime);
        TextView infoLineText = (TextView) debateTimerDisplay.findViewById(R.id.debateTimer_informationLine);

        // The information at the top of the screen
        speechNameText.setText(phaseName);
        periodDescriptionText.setText(pi.getDescription());

        // Take count direction into account for display
        long timeToShow = subtractFromSpeechLengthIfCountingDown(time, dpf);

        currentTimeText.setText(secsToTextSigned(timeToShow));

        boolean overtime = time > dpf.getLength();

        // Colours
        int currentTimeTextColor = getResources()
                .getColor((overtime) ? R.color.overtimeTextColour : android.R.color.primary_text_dark);
        int backgroundColour = getBackgroundColorFromPeriodInfo(dpf, pi);

        // If we're updating the current display (as opposed to an inactive debate phase), then
        // don't update colours if there is a flash screen in progress.
        boolean displayIsActive = debateTimerDisplay == mDebateTimerDisplay;
        boolean semaphoreAcquired = displayIsActive && mFlashScreenSemaphore.tryAcquire();

        // If not current display, or we got the semaphore, we're good to go.  If not, don't bother.
        if (!displayIsActive || semaphoreAcquired) {
            updateDebateTimerDisplayColours(debateTimerDisplay, currentTimeTextColor, backgroundColour);
            if (semaphoreAcquired)
                mFlashScreenSemaphore.release();
        }

        // Construct the line that goes at the bottom
        StringBuilder infoLine = new StringBuilder();

        // First, length...
        long length = dpf.getLength();
        String lengthStr;
        if (length % 60 == 0)
            lengthStr = getResources().getQuantityString(R.plurals.mainScreen_timeInMinutes, (int) (length / 60),
                    length / 60);
        else
            lengthStr = XmlUtilities.secsToText(length);

        int finalTimeTextUnformattedResid = (dpf.isPrep()) ? R.string.mainScreen_prepTimeLength
                : R.string.mainScreen_speechLength;
        infoLine.append(String.format(this.getString(finalTimeTextUnformattedResid), lengthStr));

        if (dpf.isPrep()) {
            PrepTimeFormat ptf = (PrepTimeFormat) dpf;
            if (ptf.isControlled())
                infoLine.append(getString(R.string.mainScreen_prepTimeControlledIndicator));
        }

        // ...then, if applicable, bells
        ArrayList<BellInfo> currentSpeechBells = dpf.getBellsSorted();
        Iterator<BellInfo> currentSpeechBellsIter = currentSpeechBells.iterator();

        if (overtime) {
            // show next overtime bell (don't bother with list of bells anymore)
            if (nextOvertimeBellTime == null)
                infoLine.append(getString(R.string.mainScreen_bellsList_noOvertimeBells));
            else {
                long timeToDisplay = subtractFromSpeechLengthIfCountingDown(nextOvertimeBellTime, dpf);
                infoLine.append(
                        getString(R.string.mainScreen_bellsList_nextOvertimeBell, secsToTextSigned(timeToDisplay)));
            }

        } else if (currentSpeechBellsIter.hasNext()) {
            // Convert the list of bells into a string.
            StringBuilder bellsStr = new StringBuilder();

            while (currentSpeechBellsIter.hasNext()) {
                BellInfo bi = currentSpeechBellsIter.next();
                long bellTime = subtractFromSpeechLengthIfCountingDown(bi.getBellTime(), dpf);
                bellsStr.append(XmlUtilities.secsToText(bellTime));
                if (bi.isPauseOnBell())
                    bellsStr.append(getString(R.string.mainScreen_pauseOnBellIndicator));
                if (bi.isSilent())
                    bellsStr.append(getString(R.string.mainScreen_silentBellIndicator));
                if (currentSpeechBellsIter.hasNext())
                    bellsStr.append(", ");
            }

            infoLine.append(getResources().getQuantityString(R.plurals.mainScreen_bellsList_normal,
                    currentSpeechBells.size(), bellsStr));

        } else {
            infoLine.append(getString(R.string.mainScreen_bellsList_noBells));
        }

        infoLineText.setText(infoLine.toString());

        // Update the POI timer button
        updatePoiTimerButton(debateTimerDisplay, dpf);

    }

    /**
     * @param view a {@link View} which should often be the <code>RelativeLayout</code> in debate_timer_display.xml,
     * except in cases where no debate is loaded or something like that.
     * @param timeTextColour the text colour to use for the current time
     * @param backgroundColour the colour to use for the background
     */
    private void updateDebateTimerDisplayColours(View view, int timeTextColour, int backgroundColour) {

        boolean viewIsDebateTimerDisplay = view.getId() == R.id.debateTimer_root;

        switch (mBackgroundColourArea) {
        case TOP_BAR_ONLY:
            if (viewIsDebateTimerDisplay) {
                // These would only be expected to exist if the view given is the debate timer display
                view.findViewById(R.id.debateTimer_speechNameText).setBackgroundColor(backgroundColour);
                view.findViewById(R.id.debateTimer_periodDescriptionText).setBackgroundColor(backgroundColour);
            }
            break;
        case WHOLE_SCREEN:
            view.setBackgroundColor(backgroundColour);
            break;
        case DISABLED:
            // Do nothing
        }

        // This would only be expected to exist if the view given is the debate timer display
        if (viewIsDebateTimerDisplay)
            ((TextView) view.findViewById(R.id.debateTimer_currentTime)).setTextColor(timeTextColour);
    }

    /**
     * Updates the GUI (in the general case).
     */
    private void updateGui() {
        if (mChangingPages) {
            Log.d(TAG, "Changing pages, don't do updateGui");
            return;
        }

        // Log.d(TAG, "updateGui");

        updateDebateTimerDisplay();
        updateControls();

        if (mDebateManager != null) {
            this.setTitle(
                    getString(R.string.activityName_Debating_withFormat, mDebateManager.getDebateFormatName()));
        } else {
            setTitle(R.string.activityName_Debating_withoutFormat);
        }

    }

    /**
     * Update the "keep screen on" flag according to
     * <ul>
     *    <li>whether it is prep time or a speech, and</li>
     *    <li>whether the timer is currently running.</li>
     * </ul>
     * This method should be called whenever
     * <ul>
     *     <li>the user preference is applied, and</li>
     *     <li>the timer starts or stops, or might start or stop.</li>
     * </ul>
     */
    private void updateKeepScreenOn() {
        boolean relevantKeepScreenOn;

        if (mDebateManager != null && mDebateManager.getActivePhaseFormat().isPrep())
            relevantKeepScreenOn = mPrepTimeKeepScreenOn;
        else
            relevantKeepScreenOn = mSpeechKeepScreenOn;

        if (relevantKeepScreenOn && mDebateManager != null && mDebateManager.isRunning())
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        else
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    private void updatePlayBellButton() {
        if (mBinder != null)
            mPlayBellButton.setVisibility((mBinder.getAlertManager().isSilentMode()) ? View.GONE : View.VISIBLE);
    }

    /**
     * @param debateTimerDisplay the {@link View} to be updated
     * @param dpf the {@link DebatePhaseFormat} relevant for this <code>debateTimerDisplay</code>
     */
    private void updatePoiTimerButton(View debateTimerDisplay, DebatePhaseFormat dpf) {
        Button poiButton = (Button) debateTimerDisplay.findViewById(R.id.debateTimer_poiTimerButton);

        // Determine whether or not we display the POI timer button
        // Display only when user has POI timer enabled, and a debate is loaded and the current
        // speech has POIs in it.
        boolean displayPoiTimerButton = false;
        if (mPoiTimerEnabled)
            if (dpf.getClass() == SpeechFormat.class)
                if (((SpeechFormat) dpf).hasPoisAllowedSomewhere())
                    displayPoiTimerButton = true;

        // If it's appropriate to display the button, do so
        if (displayPoiTimerButton) {
            poiButton.setVisibility(View.VISIBLE);

            // Determine whether POIs are active
            boolean poisActive = false;
            if (mDebateManager != null)
                if (mDebateManager.isPoisActive())
                    poisActive = true;

            // If POIs are currently active, enable the button
            if (poisActive) {
                poiButton.setEnabled(mDebateManager.isRunning());

                Long poiTime = mDebateManager.getCurrentPoiTime();
                if (poiTime == null)
                    poiButton.setText(R.string.mainScreen_poiTimer_buttonText);
                else
                    poiButton.setText(poiTime.toString());

                // Otherwise, disable it
            } else {
                poiButton.setText(R.string.mainScreen_poiTimer_buttonText);
                poiButton.setEnabled(false);
            }

            // Otherwise, hide the button
        } else {
            poiButton.setVisibility(View.GONE);
        }
    }

    /**
     * Converts a number of seconds to a String in the format 00:00, or +00:00 if the time
     * given is negative.  (Note: A <i>plus</i> sign is used for <i>negative</i> numbers; this
     * indicates overtime.)
     * @param time a time in seconds
     * @return the String
     */
    private static String secsToTextSigned(long time) {
        if (time >= 0)
            return XmlUtilities.secsToText(time);
        else
            return "+" + XmlUtilities.secsToText(-time);
    }
}