com.forrestguice.suntimeswidget.SuntimesActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.forrestguice.suntimeswidget.SuntimesActivity.java

Source

/**
Copyright (C) 2014 Forrest Guice
This file is part of SuntimesWidget.
    
SuntimesWidget is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
SuntimesWidget 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 License for more details.
    
You should have received a copy of the GNU General Public License
along with SuntimesWidget.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.forrestguice.suntimeswidget;

import android.annotation.TargetApi;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;

import android.os.Parcelable;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;

import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;

import com.forrestguice.suntimeswidget.calculator.SuntimesRiseSetDataset;
import com.forrestguice.suntimeswidget.getfix.GetFixHelper;
import com.forrestguice.suntimeswidget.getfix.GetFixUI;
import com.forrestguice.suntimeswidget.notes.NoteChangedListener;
import com.forrestguice.suntimeswidget.notes.NoteData;
import com.forrestguice.suntimeswidget.notes.SuntimesNotes;
import com.forrestguice.suntimeswidget.notes.SuntimesNotes3;
import com.forrestguice.suntimeswidget.settings.AppSettings;
import com.forrestguice.suntimeswidget.settings.SolarEvents;
import com.forrestguice.suntimeswidget.settings.WidgetSettings;
import com.forrestguice.suntimeswidget.settings.WidgetTimezones;

import java.lang.reflect.Method;
import java.text.DateFormat;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

public class SuntimesActivity extends AppCompatActivity {
    public static final String KEY_UI_CARDISTOMORROW = "cardIsTomorrow";
    public static final String KEY_UI_USERSWAPPEDCARD = "userSwappedCard";

    public static final String WARNINGID_DATE = "Date";
    public static final String WARNINGID_TIMEZONE = "Timezone";

    private static final String DIALOGTAG_TIMEZONE = "timezone";
    private static final String DIALOGTAG_ALARM = "alarm";
    private static final String DIALOGTAG_ABOUT = "about";
    private static final String DIALOGTAG_HELP = "help";
    private static final String DIALOGTAG_LOCATION = "location";
    private static final String DIALOGTAG_DATE = "dateselect";
    private static final String DIALOGTAG_LIGHTMAP = "lightmap";

    protected static final SuntimesUtils utils = new SuntimesUtils();

    private ActionBar actionBar;
    private Menu actionBarMenu;

    private GetFixHelper getFixHelper;

    private WidgetSettings.Location location;
    protected SuntimesNotes notes;
    protected SuntimesRiseSetDataset dataset;

    private int color_textTimeDelta;

    // clock views
    private TextView txt_time;
    private TextView txt_time_suffix;
    private TextView txt_timezone;

    // note views
    private ProgressBar note_progress;
    private ViewFlipper note_flipper;
    private Animation anim_note_inPrev;
    private Animation anim_note_inNext;
    private Animation anim_note_outNext;
    private Animation anim_note_outPrev;

    private ImageView ic_time1_note;
    private TextView txt_time1_note1;
    private TextView txt_time1_note2;
    private TextView txt_time1_note3;

    private ImageView ic_time2_note;
    private TextView txt_time2_note1;
    private TextView txt_time2_note2;
    private TextView txt_time2_note3;

    // time card views
    private ViewFlipper card_flipper;
    private Animation anim_card_inPrev;
    private Animation anim_card_inNext;
    private Animation anim_card_outNext;
    private Animation anim_card_outPrev;

    private ImageButton btn_flipperNext_today;
    private ImageButton btn_flipperPrev_today;
    private ImageButton btn_flipperNext_tomorrow;
    private ImageButton btn_flipperPrev_tomorrow;

    private TextView txt_date;
    private TextView txt_sunrise_actual;
    private TextView txt_sunrise_civil;
    private TextView txt_sunrise_nautical;
    private TextView txt_sunrise_astro;
    private TextView txt_sunset_actual;
    private TextView txt_sunset_civil;
    private TextView txt_sunset_nautical;
    private TextView txt_sunset_astro;
    private TextView txt_solarnoon;

    private LinearLayout layout_daylength;
    private TextView txt_daylength;
    private TextView txt_lightlength;

    private TextView txt_date2;
    private TextView txt_sunrise2_actual;
    private TextView txt_sunrise2_civil;
    private TextView txt_sunrise2_nautical;
    private TextView txt_sunrise2_astro;
    private TextView txt_sunset2_actual;
    private TextView txt_sunset2_civil;
    private TextView txt_sunset2_nautical;
    private TextView txt_sunset2_astro;
    private TextView txt_solarnoon2;

    private LinearLayout layout_daylength2;
    private TextView txt_daylength2;
    private TextView txt_lightlength2;

    private LightMapView lightmap;
    private View lightmapLayout;

    private boolean isRtl = false;
    private boolean userSwappedCard = false;
    private HashMap<SolarEvents.SolarEventField, TextView> timeFields;

    private boolean showWarnings = false;
    private SuntimesWarning timezoneWarning;
    private SuntimesWarning dateWarning;
    private List<SuntimesWarning> warnings;

    public SuntimesActivity() {
        super();
    }

    /**
     * OnCreate: the Activity initially created
     * @param savedState a Bundle containing previously saved application state
     */
    @Override
    public void onCreate(Bundle savedState) {
        Context context = SuntimesActivity.this;
        calculateData(context);

        setTheme(AppSettings.loadTheme(this, dataset));
        GetFixUI.themeIcons(this);

        super.onCreate(savedState);
        setResult(RESULT_CANCELED);

        initLocale(this); // must follow super.onCreate or locale is reverted
        setContentView(R.layout.layout_main);
        initViews(context);

        initWarnings(context, savedState);

        initGetFix();
        getFixHelper.loadSettings(savedState);
        notes.resetNoteIndex();

        Intent intent = getIntent();
        Uri data = intent.getData();
        if (data != null) {
            intent.setData(null);
            configLocation(data);
        }
    }

    private void initLocale(Context context) {
        AppSettings.initLocale(this);
        isRtl = AppSettings.isLocaleRtl(this);

        WidgetSettings.initDefaults(context); // locale specific defaults

        SuntimesUtils.initDisplayStrings(context); // locale specific strings
        AppSettings.initDisplayStrings(context);
        WidgetSettings.initDisplayStrings(context);

        initAnimations(context); // locale specific animations
        initColors(context); // locale specific colors
    }

    /**
     * OnStart: the Activity becomes visible
     */
    @Override
    public void onStart() {
        super.onStart();
        calculateData(SuntimesActivity.this);
        updateViews(SuntimesActivity.this);
    }

    /**
     * OnResume: the user is now interacting w/ the Activity (running state)
     */
    @Override
    public void onResume() {
        super.onResume();
        updateActionBar(this);
        getFixHelper.onResume();

        // restore open dialogs
        FragmentManager fragments = getSupportFragmentManager();
        TimeZoneDialog timezoneDialog = (TimeZoneDialog) fragments.findFragmentByTag(DIALOGTAG_TIMEZONE);
        if (timezoneDialog != null) {
            timezoneDialog.setOnAcceptedListener(onConfigTimeZone);
            timezoneDialog.setOnCanceledListener(onCancelTimeZone);
            //Log.d("DEBUG", "TimeZoneDialog listeners restored.");
        }

        AlarmDialog alarmDialog = (AlarmDialog) fragments.findFragmentByTag(DIALOGTAG_ALARM);
        if (alarmDialog != null) {
            alarmDialog.setData(dataset);
            alarmDialog.setOnAcceptedListener(alarmDialog.scheduleAlarmClickListener);
            //Log.d("DEBUG", "AlarmDialog listeners restored.");
        }

        LocationConfigDialog locationDialog = (LocationConfigDialog) fragments
                .findFragmentByTag(DIALOGTAG_LOCATION);
        if (locationDialog != null) {
            locationDialog.setOnAcceptedListener(onConfigLocation(locationDialog));
            //Log.d("DEBUG", "LocationDialog listeners restored.");
        }

        TimeDateDialog dateDialog = (TimeDateDialog) fragments.findFragmentByTag(DIALOGTAG_DATE);
        if (dateDialog != null) {
            dateDialog.setOnAcceptedListener(onConfigDate);
            dateDialog.setOnCanceledListener(onCancelDate);
            //Log.d("DEBUG", "TimeDateDialog listeners restored.");
        }

        LightMapDialog lightMapDialog = (LightMapDialog) fragments.findFragmentByTag(DIALOGTAG_LIGHTMAP);
        if (lightMapDialog != null) {
            lightMapDialog.setData(dataset);
            lightMapDialog.updateViews(dataset);
            //Log.d("DEBUG", "LightMapDialog updated on restore.");
        }
    }

    /**
     * OnPause: the user about to interact w/ another Activity
     */
    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        saveWarnings(outState);
        outState.putBoolean(KEY_UI_USERSWAPPEDCARD, userSwappedCard);
        outState.putBoolean(KEY_UI_CARDISTOMORROW, (card_flipper.getDisplayedChild() != 0));
    }

    @Override
    public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        restoreWarnings(savedInstanceState);
        setUserSwappedCard(savedInstanceState.getBoolean(KEY_UI_USERSWAPPEDCARD, false), "onRestoreInstanceState");
        boolean cardIsTomorrow = savedInstanceState.getBoolean(KEY_UI_CARDISTOMORROW, false);
        card_flipper.setDisplayedChild((cardIsTomorrow ? 1 : 0));
    }

    /**
     * OnStop: the Activity no longer visible
     */
    @Override
    public void onStop() {
        stopTimeTask();
        getFixHelper.cancelGetFix();
        super.onStop();
    }

    /**
     * OnDestroy: the activity destroyed
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    /**
     * @param requestCode the request code that was passed to requestPermissions
     * @param permissions the requested permissions
     * @param grantResults either PERMISSION_GRANTED or PERMISSION_DENIED for each of the requested permissions
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
            @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        FragmentManager fragments = getSupportFragmentManager();
        LocationConfigDialog locationDialog = (LocationConfigDialog) fragments
                .findFragmentByTag(DIALOGTAG_LOCATION);
        if (locationDialog != null) {
            locationDialog.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }

        WidgetSettings.LocationMode locationMode = WidgetSettings.loadLocationModePref(this, 0);
        if (locationMode == WidgetSettings.LocationMode.CURRENT_LOCATION) {
            getFixHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    /**
     * initialize ui/views
     * @param context a context used to access resources
     */
    protected void initViews(Context context) {
        initActionBar(context);
        initClockViews(context);
        initNoteViews(context);
        initCardViews(context);
        initLightMap(context);
    }

    /**
     * initialize warning snackbar
     */
    private void initWarnings(Context context, Bundle savedState) {
        timezoneWarning = new SuntimesWarning(WARNINGID_TIMEZONE);
        dateWarning = new SuntimesWarning(WARNINGID_DATE);

        warnings = new ArrayList<SuntimesWarning>();
        warnings.add(timezoneWarning);
        warnings.add(dateWarning);

        restoreWarnings(savedState);
    }

    /**
     * initialize the actionbar
     */
    private void initActionBar(Context context) {
        Toolbar menuBar = (Toolbar) findViewById(R.id.app_menubar);
        setSupportActionBar(menuBar);
        actionBar = getSupportActionBar();
    }

    private void initLightMap(Context context) {
        lightmap = (LightMapView) findViewById(R.id.info_time_lightmap);
        lightmapLayout = findViewById(R.id.info_time_lightmap_layout);

        lightmapLayout.setClickable(true);
        lightmapLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                showLightMapDialog();
            }
        });
        lightmapLayout.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                showLightMapDialog();
                return true;
            }
        });
    }

    /**
     * initialize gps helper
     */
    private void initGetFix() {
        getFixHelper = new GetFixHelper(this, new GetFixUI() {
            private MenuItem refreshItem = null;

            @Override
            public void enableUI(boolean value) {
                if (refreshItem != null) {
                    refreshItem.setEnabled(value);
                }
            }

            @Override
            public void updateUI(Location... locations) {
                WidgetSettings.Location location = new WidgetSettings.Location(
                        getString(R.string.gps_lastfix_title_found), locations[0]);
                actionBar.setSubtitle(location.toString());
            }

            @Override
            public void showProgress(boolean showProgress) {
                note_progress.setVisibility((showProgress ? View.VISIBLE : View.GONE));
            }

            @Override
            public void onStart() {
                invalidateData(SuntimesActivity.this);

                refreshItem = actionBarMenu.findItem(R.id.action_location_refresh);
                if (refreshItem != null) {
                    actionBar.setTitle(getString(R.string.gps_lastfix_title_searching));
                    actionBar.setSubtitle("");
                    refreshItem.setIcon(GetFixUI.ICON_GPS_SEARCHING);
                }
            }

            @Override
            public void onResult(Location result, boolean wasCancelled) {
                if (refreshItem != null) {
                    refreshItem.setIcon((result != null) ? ICON_GPS_FOUND
                            : (getFixHelper.isLocationEnabled() ? ICON_GPS_FOUND : ICON_GPS_DISABLED));

                    if (result != null) {
                        WidgetSettings.Location location = new WidgetSettings.Location(
                                getString(R.string.gps_lastfix_title_found), result);
                        WidgetSettings.saveLocationPref(SuntimesActivity.this, 0, location);

                    } else {
                        String msg = (wasCancelled ? getString(R.string.gps_lastfix_toast_cancelled)
                                : getString(R.string.gps_lastfix_toast_notfound));
                        Toast errorMsg = Toast.makeText(SuntimesActivity.this, msg, Toast.LENGTH_LONG);
                        errorMsg.show();
                    }
                    SuntimesActivity.this.calculateData(SuntimesActivity.this);
                    SuntimesActivity.this.updateViews(SuntimesActivity.this);
                }
            }
        });
    }

    /**
     * update actionbar items; shouldn't be called until after the menu is inflated.
     */
    private void updateActionBar(Context context) {
        if (actionBarMenu != null) {
            MenuItem refreshItem = actionBarMenu.findItem(R.id.action_location_refresh);
            if (refreshItem != null) {
                WidgetSettings.LocationMode mode = WidgetSettings.loadLocationModePref(context, 0);
                if (mode != WidgetSettings.LocationMode.CURRENT_LOCATION) {
                    refreshItem.setVisible(false);

                } else {
                    refreshItem.setIcon((getFixHelper.isLocationEnabled() ? GetFixUI.ICON_GPS_FOUND
                            : GetFixUI.ICON_GPS_DISABLED));
                    refreshItem.setVisible(true);
                }
            }
        }
    }

    /**
     * initialize the note flipper and associated views
     * @param context a context used to access resources
     */
    private void initNoteViews(Context context) {
        note_progress = (ProgressBar) findViewById(R.id.info_note_progress);
        if (note_progress != null) {
            note_progress.setVisibility(View.GONE);
        }

        note_flipper = (ViewFlipper) findViewById(R.id.info_note_flipper);
        if (note_flipper != null) {
            note_flipper.setOnTouchListener(noteTouchListener);
            note_flipper.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    /* DO NOTHING HERE (but we still need this listener) */ }
            });

        } else {
            Log.w("initNoteViews", "Failed to set touchListener; note_flipper is null!");
        }

        LinearLayout note1 = (LinearLayout) findViewById(R.id.info_time_note1);
        if (note1 != null) {
            txt_time1_note1 = (TextView) note1.findViewById(R.id.text_timenote1);
            txt_time1_note2 = (TextView) note1.findViewById(R.id.text_timenote2);
            txt_time1_note3 = (TextView) note1.findViewById(R.id.text_timenote3);
            ic_time1_note = (ImageView) note1.findViewById(R.id.icon_timenote);
            ic_time1_note.setVisibility(View.INVISIBLE);

        } else {
            Log.w("initNoteViews", "Failed to init note layout1; was null!");
        }

        LinearLayout note2 = (LinearLayout) findViewById(R.id.info_time_note2);
        if (note2 != null) {
            txt_time2_note1 = (TextView) note2.findViewById(R.id.text_timenote1);
            txt_time2_note2 = (TextView) note2.findViewById(R.id.text_timenote2);
            txt_time2_note3 = (TextView) note2.findViewById(R.id.text_timenote3);
            ic_time2_note = (ImageView) note2.findViewById(R.id.icon_timenote);
            ic_time2_note.setVisibility(View.INVISIBLE);

        } else {
            Log.w("initNoteViews", "Failed to init note layout2; was null!");
        }
    }

    /**
     * initialize the card flipper and associated views
     * @param context a context used to access resources
     */
    private void initCardViews(Context context) {
        timeFields = new HashMap<SolarEvents.SolarEventField, TextView>();
        card_flipper = (ViewFlipper) findViewById(R.id.info_time_flipper);
        if (card_flipper != null) {
            card_flipper.setOnTouchListener(timeCardTouchListener);
        } else {
            Log.w("initCardViews", "Failed to set touchListener; card_flipper was null!");
        }

        // Today's times
        LinearLayout viewToday = (LinearLayout) findViewById(R.id.info_time_all_today);
        if (viewToday != null) {
            txt_date = (TextView) viewToday.findViewById(R.id.text_date);
            txt_date.setOnClickListener(dateTapClickListener(false));

            txt_sunrise_actual = (TextView) viewToday.findViewById(R.id.text_time_sunrise_actual);
            txt_sunset_actual = (TextView) viewToday.findViewById(R.id.text_time_sunset_actual);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.SUNRISE, false), txt_sunrise_actual);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.SUNSET, false), txt_sunset_actual);

            txt_sunrise_civil = (TextView) viewToday.findViewById(R.id.text_time_sunrise_civil);
            txt_sunset_civil = (TextView) viewToday.findViewById(R.id.text_time_sunset_civil);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_CIVIL, false), txt_sunrise_civil);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_CIVIL, false), txt_sunset_civil);

            txt_sunrise_nautical = (TextView) viewToday.findViewById(R.id.text_time_sunrise_nautical);
            txt_sunset_nautical = (TextView) viewToday.findViewById(R.id.text_time_sunset_nautical);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_NAUTICAL, false),
                    txt_sunrise_nautical);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_NAUTICAL, false),
                    txt_sunset_nautical);

            txt_sunrise_astro = (TextView) viewToday.findViewById(R.id.text_time_sunrise_astro);
            txt_sunset_astro = (TextView) viewToday.findViewById(R.id.text_time_sunset_astro);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_ASTRONOMICAL, false),
                    txt_sunrise_astro);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_ASTRONOMICAL, false),
                    txt_sunset_astro);

            txt_solarnoon = (TextView) viewToday.findViewById(R.id.text_time_noon);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.NOON, false), txt_solarnoon);

            layout_daylength = (LinearLayout) viewToday.findViewById(R.id.layout_daylength);
            txt_daylength = (TextView) viewToday.findViewById(R.id.text_daylength);
            txt_lightlength = (TextView) viewToday.findViewById(R.id.text_lightlength);

            btn_flipperNext_today = (ImageButton) viewToday.findViewById(R.id.info_time_nextbtn);
            btn_flipperNext_today.setOnClickListener(onNextCardClick);
            btn_flipperNext_today.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
                        btn_flipperNext_today.setColorFilter(
                                ContextCompat.getColor(SuntimesActivity.this, R.color.btn_tint_pressed));
                    } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                        btn_flipperNext_today.setColorFilter(null);
                    }
                    return false;
                }
            });

            btn_flipperPrev_today = (ImageButton) viewToday.findViewById(R.id.info_time_prevbtn);
            btn_flipperPrev_today.setOnClickListener(onPrevCardClick);
            btn_flipperPrev_today.setVisibility(View.GONE);

        } else {
            Log.w("initCardViews", "Failed to init card layout1; was null!");
        }

        // Tomorrow's times
        LinearLayout viewTomorrow = (LinearLayout) findViewById(R.id.info_time_all_tomorrow);
        if (viewTomorrow != null) {
            txt_date2 = (TextView) viewTomorrow.findViewById(R.id.text_date);
            txt_date2.setOnClickListener(dateTapClickListener(true));

            txt_sunrise2_actual = (TextView) viewTomorrow.findViewById(R.id.text_time_sunrise_actual);
            txt_sunset2_actual = (TextView) viewTomorrow.findViewById(R.id.text_time_sunset_actual);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.SUNRISE, true), txt_sunrise2_actual);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.SUNSET, true), txt_sunset2_actual);

            txt_sunrise2_civil = (TextView) viewTomorrow.findViewById(R.id.text_time_sunrise_civil);
            txt_sunset2_civil = (TextView) viewTomorrow.findViewById(R.id.text_time_sunset_civil);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_CIVIL, true), txt_sunrise2_civil);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_CIVIL, true), txt_sunset2_civil);

            txt_sunrise2_nautical = (TextView) viewTomorrow.findViewById(R.id.text_time_sunrise_nautical);
            txt_sunset2_nautical = (TextView) viewTomorrow.findViewById(R.id.text_time_sunset_nautical);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_NAUTICAL, true),
                    txt_sunrise2_nautical);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_NAUTICAL, true),
                    txt_sunset2_nautical);

            txt_sunrise2_astro = (TextView) viewTomorrow.findViewById(R.id.text_time_sunrise_astro);
            txt_sunset2_astro = (TextView) viewTomorrow.findViewById(R.id.text_time_sunset_astro);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.MORNING_ASTRONOMICAL, true),
                    txt_sunrise2_astro);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.EVENING_ASTRONOMICAL, true),
                    txt_sunset2_astro);

            txt_solarnoon2 = (TextView) viewTomorrow.findViewById(R.id.text_time_noon);
            timeFields.put(new SolarEvents.SolarEventField(SolarEvents.NOON, true), txt_solarnoon2);

            layout_daylength2 = (LinearLayout) viewTomorrow.findViewById(R.id.layout_daylength);
            txt_daylength2 = (TextView) viewTomorrow.findViewById(R.id.text_daylength);
            txt_lightlength2 = (TextView) viewTomorrow.findViewById(R.id.text_lightlength);

            btn_flipperNext_tomorrow = (ImageButton) viewTomorrow.findViewById(R.id.info_time_nextbtn);
            btn_flipperNext_tomorrow.setOnClickListener(onNextCardClick);
            btn_flipperNext_tomorrow.setVisibility(View.GONE);

            btn_flipperPrev_tomorrow = (ImageButton) viewTomorrow.findViewById(R.id.info_time_prevbtn);
            btn_flipperPrev_tomorrow.setOnClickListener(onPrevCardClick);
            btn_flipperPrev_tomorrow.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
                        btn_flipperPrev_tomorrow.setColorFilter(
                                ContextCompat.getColor(SuntimesActivity.this, R.color.btn_tint_pressed));
                    } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                        btn_flipperPrev_tomorrow.setColorFilter(null);
                    }
                    return false;
                }
            });

            initTimeFields();

        } else {
            Log.w("initCardViews", "Failed to init card layout2; was null!");
        }

        stretchTableRule();
    }

    /**
     * initialize the clock ui
     * @param context a context used to access resources
     */
    private void initClockViews(Context context) {
        LinearLayout clockLayout = (LinearLayout) findViewById(R.id.layout_clock);
        if (clockLayout != null) {
            clockLayout.setOnClickListener(onClockClick);
        }

        txt_time = (TextView) findViewById(R.id.text_time);
        txt_time_suffix = (TextView) findViewById(R.id.text_time_suffix);
        txt_timezone = (TextView) findViewById(R.id.text_timezone);
    }

    /**
     * initialize view animations
     * @param context a context used to access resources
     */
    private void initAnimations(Context context) {
        anim_note_inPrev = AnimationUtils.loadAnimation(this, R.anim.fade_in);
        anim_note_inNext = AnimationUtils.loadAnimation(this, R.anim.fade_in);
        anim_note_outPrev = AnimationUtils.loadAnimation(this, R.anim.fade_out);
        anim_note_outNext = AnimationUtils.loadAnimation(this, R.anim.fade_out);
        //anim_note_outPrev = AnimationUtils.loadAnimation(this, R.anim.slide_out_right);
        //anim_note_outNext = AnimationUtils.loadAnimation(this, R.anim.slide_out_left);

        anim_card_inPrev = AnimationUtils.loadAnimation(this, R.anim.fade_in);
        anim_card_inNext = AnimationUtils.loadAnimation(this, R.anim.fade_in);

        anim_card_outPrev = AnimationUtils.loadAnimation(this,
                (isRtl ? R.anim.slide_out_left : R.anim.slide_out_right));
        anim_card_outNext = AnimationUtils.loadAnimation(this,
                (isRtl ? R.anim.slide_out_right : R.anim.slide_out_left));
    }

    /**
     * @param context a context used to access resources
     */
    private void initColors(Context context) {
        int[] colorAttrs = { android.R.attr.textColorPrimary };
        TypedArray typedArray = context.obtainStyledAttributes(colorAttrs);
        int def = Color.WHITE;

        color_textTimeDelta = ContextCompat.getColor(context, typedArray.getResourceId(0, def));

        typedArray.recycle();
    }

    /**
     * Initialize note object and onChanged listener.
     */
    private void initNotes() {
        notes = new SuntimesNotes3();
        notes.init(this, dataset);
        notes.setOnChangedListener(new NoteChangedListener() {
            @Override
            public void onNoteChanged(NoteData note, int transition) {
                updateNoteUI(note, transition);
            }
        });
    }

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

    @Override
    protected boolean onPrepareOptionsPanel(View view, Menu menu) {
        forceActionBarIcons(menu);
        return super.onPrepareOptionsPanel(view, menu);
    }

    /**
     * from http://stackoverflow.com/questions/18374183/how-to-show-icons-in-overflow-menu-in-actionbar
     */
    private void forceActionBarIcons(Menu menu) {
        if (menu != null) {
            if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
                try {
                    Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
                    m.setAccessible(true);
                    m.invoke(menu, true);

                } catch (Exception e) {
                    Log.e("SuntimesActivity", "failed to set show overflow icons", e);
                }
            }
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_settings:
            showSettings();
            return true;

        case R.id.action_about:
            showAbout();
            return true;

        case R.id.action_help:
            showHelp();
            return true;

        case R.id.action_location_add:
            configLocation();
            return true;

        case R.id.action_location_refresh:
            refreshLocation();
            return false;

        case R.id.action_location_show:
            showMap();
            return true;

        case R.id.action_timezone:
            configTimeZone();
            return true;

        case R.id.action_date:
            configDate();
            return true;

        case R.id.action_alarm:
            scheduleAlarm();
            return true;

        default:
            return super.onOptionsItemSelected(item);
        }
    }

    /**
     * Select a date other than today.
     */
    private void configDate() {
        final TimeDateDialog datePicker = new TimeDateDialog();
        datePicker.setOnAcceptedListener(onConfigDate);
        datePicker.setOnCanceledListener(onCancelDate);
        datePicker.show(getSupportFragmentManager(), DIALOGTAG_DATE);
    }

    DialogInterface.OnClickListener onConfigDate = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            dateWarning.reset();
            calculateData(SuntimesActivity.this);
            updateViews(SuntimesActivity.this);
        }
    };
    DialogInterface.OnClickListener onCancelDate = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            showWarnings();
        }
    };

    /**
     * Refresh location (current location mode).
     */
    protected void refreshLocation() {
        getFixHelper.getFix();
    }

    /**
     * Configure location.
     */
    protected void configLocation() {
        configLocation(null);
    }

    protected void configLocation(Uri data) {
        final LocationConfigDialog locationDialog = new LocationConfigDialog();
        locationDialog.setData(data);
        locationDialog.setHideTitle(true);
        locationDialog.setOnAcceptedListener(onConfigLocation(locationDialog));

        getFixHelper.cancelGetFix();
        locationDialog.show(getSupportFragmentManager(), DIALOGTAG_LOCATION);
    }

    protected DialogInterface.OnClickListener onConfigLocation(final LocationConfigDialog dialog) {
        return new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                calculateData(SuntimesActivity.this);
                updateActionBar(SuntimesActivity.this);
                updateViews(SuntimesActivity.this);

                WidgetSettings.LocationMode locationMode = dialog.getDialogContent().getLocationMode();
                if (locationMode == WidgetSettings.LocationMode.CURRENT_LOCATION) {
                    getFixHelper.getFix();
                }
            }
        };
    }

    /**
     * Configure time zone.
     */
    protected void configTimeZone() {
        TimeZoneDialog timezoneDialog = new TimeZoneDialog();
        timezoneDialog.setOnAcceptedListener(onConfigTimeZone);
        timezoneDialog.setOnCanceledListener(onCancelTimeZone);
        timezoneDialog.show(getSupportFragmentManager(), DIALOGTAG_TIMEZONE);
    }

    DialogInterface.OnClickListener onConfigTimeZone = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            timezoneWarning.reset();
            calculateData(SuntimesActivity.this);
            updateViews(SuntimesActivity.this);
        }
    };
    DialogInterface.OnClickListener onCancelTimeZone = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            showWarnings();
        }
    };

    /**
     * Show the location on a map.
     * Intent filtering code based off answer by "gumberculese";
     * http://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name
     */
    protected void showMap() {
        Intent mapIntent = new Intent(Intent.ACTION_VIEW);
        mapIntent.setData(location.getUri());
        //if (mapIntent.resolveActivity(getPackageManager()) != null)
        //{
        //    startActivity(mapIntent);
        //}

        String myPackage = "com.forrestguice.suntimeswidget";
        List<ResolveInfo> info = getPackageManager().queryIntentActivities(mapIntent, 0);
        List<Intent> geoIntents = new ArrayList<Intent>();

        if (!info.isEmpty()) {
            for (ResolveInfo resolveInfo : info) {
                String packageName = resolveInfo.activityInfo.packageName;
                if (!TextUtils.equals(packageName, myPackage)) {
                    Intent geoIntent = new Intent(Intent.ACTION_VIEW);
                    geoIntent.setPackage(packageName);
                    geoIntent.setData(location.getUri());
                    geoIntents.add(geoIntent);
                }
            }
        }

        if (geoIntents.size() > 0) {
            Intent chooserIntent = Intent.createChooser(geoIntents.remove(0),
                    getString(R.string.configAction_mapLocation_chooser));
            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
                    geoIntents.toArray(new Parcelable[geoIntents.size()]));
            startActivity(chooserIntent);

        } else {
            Toast noAppError = Toast.makeText(this, getString(R.string.configAction_mapLocation_noapp),
                    Toast.LENGTH_LONG);
            noAppError.show();
        }
    }

    /**
     * Show the help dialog.
     */
    protected void showHelp() {
        String topic1 = getString(R.string.help_general_timeMode);
        String topic2 = getString(R.string.help_general_daylength);
        String helpText = getString(R.string.help_general, topic1, topic2);

        HelpDialog helpDialog = new HelpDialog();
        helpDialog.setContent(helpText);
        helpDialog.show(getSupportFragmentManager(), DIALOGTAG_HELP);
    }

    /**
     * Show the about dialog.
     */
    protected void showAbout() {
        AboutDialog aboutDialog = new AboutDialog();
        aboutDialog.show(getSupportFragmentManager(), DIALOGTAG_ABOUT);
    }

    /**
     * Show application settings.
     */
    protected void showSettings() {
        Intent settingsIntent = new Intent(this, SuntimesSettingsActivity.class);
        startActivity(settingsIntent);
    }

    /**
     * Show the alarm dialog.
     */
    protected void scheduleAlarm() {
        scheduleAlarm(null);
    }

    protected void scheduleAlarm(SolarEvents selected) {
        if (dataset.isCalculated()) {
            AlarmDialog alarmDialog = new AlarmDialog();
            alarmDialog.setData(dataset);
            alarmDialog.setChoice(selected);
            alarmDialog.setOnAcceptedListener(alarmDialog.scheduleAlarmClickListener);
            alarmDialog.show(getSupportFragmentManager(), DIALOGTAG_ALARM);

        } else {
            String msg = getString(R.string.schedalarm_dialog_error2);
            Toast errorMsg = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
            errorMsg.show();
        }
    }

    protected void scheduleAlarmFromNote() {
        scheduleAlarm(notes.getNote().noteMode);
    }

    /**
     *
     * @param context a context used to access shared prefs
     */
    private void initData(Context context) {
        dataset = new SuntimesRiseSetDataset(context);
    }

    protected void calculateData(Context context) {
        initData(context);
        dataset.calculateData();
        initNotes();
    }

    protected void invalidateData(Context context) {
        dataset.invalidateCalculation();
        updateViews(context);
    }

    protected void updateViews(Context context) {
        stopTimeTask();

        showWarnings = AppSettings.loadShowWarningsPref(this);
        dateWarning.shouldShow = false;
        timezoneWarning.shouldShow = false;

        location = WidgetSettings.loadLocationPref(context, AppWidgetManager.INVALID_APPWIDGET_ID);
        String locationTitle = location.getLabel();
        String locationSubtitle = location.toString();

        if (actionBar != null) {
            actionBar.setTitle(locationTitle);
            actionBar.setSubtitle(locationSubtitle);
        }

        // today's view
        SuntimesUtils.TimeDisplayText sunriseString_actualTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataActual.sunriseCalendarToday());
        SuntimesUtils.TimeDisplayText sunriseString_civilTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataCivil.sunriseCalendarToday());
        SuntimesUtils.TimeDisplayText sunriseString_nauticalTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataNautical.sunriseCalendarToday());
        SuntimesUtils.TimeDisplayText sunriseString_astroTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataAstro.sunriseCalendarToday());
        SuntimesUtils.TimeDisplayText noonString = utils.calendarTimeShortDisplayString(context,
                dataset.dataNoon.sunriseCalendarToday());
        SuntimesUtils.TimeDisplayText sunsetString_actualTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataActual.sunsetCalendarToday());
        SuntimesUtils.TimeDisplayText sunsetString_civilTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataCivil.sunsetCalendarToday());
        SuntimesUtils.TimeDisplayText sunsetString_nauticalTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataNautical.sunsetCalendarToday());
        SuntimesUtils.TimeDisplayText sunsetString_astroTime = utils.calendarTimeShortDisplayString(context,
                dataset.dataAstro.sunsetCalendarToday());

        // tomorrow's view
        SuntimesUtils.TimeDisplayText sunriseString_actualTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataActual.sunriseCalendarOther());
        SuntimesUtils.TimeDisplayText sunriseString_civilTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataCivil.sunriseCalendarOther());
        SuntimesUtils.TimeDisplayText sunriseString_nauticalTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataNautical.sunriseCalendarOther());
        SuntimesUtils.TimeDisplayText sunriseString_astroTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataAstro.sunriseCalendarOther());
        SuntimesUtils.TimeDisplayText noonString2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataNoon.sunriseCalendarOther());
        SuntimesUtils.TimeDisplayText sunsetString_actualTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataActual.sunsetCalendarOther());
        SuntimesUtils.TimeDisplayText sunsetString_civilTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataCivil.sunsetCalendarOther());
        SuntimesUtils.TimeDisplayText sunsetString_nauticalTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataNautical.sunsetCalendarOther());
        SuntimesUtils.TimeDisplayText sunsetString_astroTime2 = utils.calendarTimeShortDisplayString(context,
                dataset.dataAstro.sunsetCalendarOther());

        if (dataset.isCalculated()) {
            txt_sunrise_actual.setText(sunriseString_actualTime.toString());
            txt_sunrise_civil.setText(sunriseString_civilTime.toString());
            txt_sunrise_nautical.setText(sunriseString_nauticalTime.toString());
            txt_sunrise_astro.setText(sunriseString_astroTime.toString());
            txt_solarnoon.setText(noonString.toString());
            txt_sunset_actual.setText(sunsetString_actualTime.toString());
            txt_sunset_civil.setText(sunsetString_civilTime.toString());
            txt_sunset_nautical.setText(sunsetString_nauticalTime.toString());
            txt_sunset_astro.setText(sunsetString_astroTime.toString());

            txt_sunrise2_actual.setText(sunriseString_actualTime2.toString());
            txt_sunrise2_civil.setText(sunriseString_civilTime2.toString());
            txt_sunrise2_nautical.setText(sunriseString_nauticalTime2.toString());
            txt_sunrise2_astro.setText(sunriseString_astroTime2.toString());
            txt_solarnoon2.setText(noonString2.toString());
            txt_sunset2_actual.setText(sunsetString_actualTime2.toString());
            txt_sunset2_civil.setText(sunsetString_civilTime2.toString());
            txt_sunset2_nautical.setText(sunsetString_nauticalTime2.toString());
            txt_sunset2_astro.setText(sunsetString_astroTime2.toString());

            SuntimesUtils.TimeDisplayText dayLengthDisplay = utils.timeDeltaLongDisplayString(0,
                    dataset.dataActual.dayLengthToday());
            dayLengthDisplay.setSuffix("");
            String dayLength = dayLengthDisplay.toString();
            String dayLength_label = getString(R.string.length_day, dayLength);
            txt_daylength
                    .setText(SuntimesUtils.createBoldColorSpan(dayLength_label, dayLength, color_textTimeDelta));

            SuntimesUtils.TimeDisplayText dayLengthDisplay2 = utils.timeDeltaLongDisplayString(0,
                    dataset.dataActual.dayLengthOther());
            dayLengthDisplay2.setSuffix("");
            String dayLength2 = dayLengthDisplay2.toString();
            String dayLength2_label = getString(R.string.length_day, dayLength2);
            txt_daylength2
                    .setText(SuntimesUtils.createBoldColorSpan(dayLength2_label, dayLength2, color_textTimeDelta));

            SuntimesUtils.TimeDisplayText lightLengthDisplay = utils.timeDeltaLongDisplayString(0,
                    dataset.dataCivil.dayLengthToday());
            lightLengthDisplay.setSuffix("");
            String lightLength = lightLengthDisplay.toString();
            String lightLength_label = getString(R.string.length_light, lightLength);
            txt_lightlength.setText(
                    SuntimesUtils.createBoldColorSpan(lightLength_label, lightLength, color_textTimeDelta));

            SuntimesUtils.TimeDisplayText lightLengthDisplay2 = utils.timeDeltaLongDisplayString(0,
                    dataset.dataCivil.dayLengthOther());
            lightLengthDisplay2.setSuffix("");
            String lightLength2 = lightLengthDisplay2.toString();
            String lightLength2_label = getString(R.string.length_light, lightLength2);
            txt_lightlength2.setText(
                    SuntimesUtils.createBoldColorSpan(lightLength2_label, lightLength2, color_textTimeDelta));

        } else {
            String notCalculated = getString(R.string.time_loading);
            txt_sunrise_actual.setText(notCalculated);
            txt_sunrise_civil.setText(notCalculated);
            txt_sunrise_nautical.setText(notCalculated);
            txt_sunrise_astro.setText(notCalculated);
            txt_solarnoon.setText(notCalculated);
            txt_sunset_actual.setText(notCalculated);
            txt_sunset_civil.setText(notCalculated);
            txt_sunset_nautical.setText(notCalculated);
            txt_sunset_astro.setText(notCalculated);

            txt_sunrise2_actual.setText(notCalculated);
            txt_sunrise2_civil.setText(notCalculated);
            txt_sunrise2_nautical.setText(notCalculated);
            txt_sunrise2_astro.setText(notCalculated);
            txt_solarnoon2.setText(notCalculated);
            txt_sunset2_actual.setText(notCalculated);
            txt_sunset2_civil.setText(notCalculated);
            txt_sunset2_nautical.setText(notCalculated);
            txt_sunset2_astro.setText(notCalculated);
        }

        //
        // clock & date
        //
        Date data_date = dataset.dataActual.date();
        Date data_date2 = dataset.dataActual.dateOther();

        //DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(getApplicationContext());       // 4/11/2016
        DateFormat dateFormat = android.text.format.DateFormat.getMediumDateFormat(getApplicationContext()); // Apr 11, 2016
        //DateFormat dateFormat = android.text.format.DateFormat.getLongDateFormat(getApplicationContext());   // April 11, 2016

        String thisString = getString(R.string.today);
        String otherString = getString(R.string.tomorrow);

        if (dataset.dataActual.todayIsNotToday()) {
            Calendar now = dataset.now();
            WidgetSettings.DateInfo nowInfo = new WidgetSettings.DateInfo(now);
            WidgetSettings.DateInfo dataInfo = new WidgetSettings.DateInfo(dataset.dataActual.calendar());
            if (!nowInfo.equals(dataInfo)) {
                Date time = now.getTime();
                if (data_date.after(time)) {
                    thisString = getString(R.string.future_today);
                    otherString = getString(R.string.future_tomorrow);
                    dateWarning.shouldShow = true;

                } else if (data_date.before(time)) {
                    thisString = getString(R.string.past_today);
                    otherString = getString(R.string.past_tomorrow);
                    dateWarning.shouldShow = true;
                }
            }
        }

        // date fields
        ImageSpan dateWarningIcon = (showWarnings && dateWarning.shouldShow)
                ? SuntimesUtils.createWarningSpan(this, txt_date.getTextSize())
                : null;
        String dateString = getString(R.string.dateField, thisString, dateFormat.format(data_date));
        SpannableStringBuilder dateSpan = SuntimesUtils.createSpan(dateString, dateWarningIcon);
        txt_date.setText(dateSpan);

        String date2String = getString(R.string.dateField, otherString, dateFormat.format(data_date2));
        SpannableStringBuilder date2Span = SuntimesUtils.createSpan(date2String, dateWarningIcon);
        txt_date2.setText(date2Span);

        // timezone field
        timezoneWarning.shouldShow = WidgetTimezones.isProbablyNotLocal(dataset.timezone(), dataset.location(),
                dataset.date());
        ImageSpan timezoneWarningIcon = (showWarnings && timezoneWarning.shouldShow)
                ? SuntimesUtils.createWarningSpan(this, txt_timezone.getTextSize())
                : null;
        String timezoneString = getString(R.string.timezoneField, dataset.timezone().getID());
        SpannableStringBuilder timezoneSpan = SuntimesUtils.createSpan(timezoneString, timezoneWarningIcon);
        txt_timezone.setText(timezoneSpan);

        // "light map"
        boolean enableLightMap = AppSettings.loadShowLightmapPref(this);
        showLightMap(enableLightMap);
        lightmap.updateViews(enableLightMap ? dataset : null);

        showDayLength(dataset.isCalculated());
        showNotes(dataset.isCalculated());
        showWarnings();

        startTimeTask();
    }

    private void showWarnings() {
        if (showWarnings && timezoneWarning.shouldShow && !timezoneWarning.wasDismissed) {
            timezoneWarning.initWarning(this, getString(R.string.timezoneWarning));
            timezoneWarning.snackbar.setAction(getString(R.string.configAction_setTimeZone),
                    new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            configTimeZone();
                        }
                    });
            timezoneWarning.show();
            return;
        }

        if (showWarnings && dateWarning.shouldShow && !dateWarning.wasDismissed) {
            dateWarning.initWarning(this, getString(R.string.dateWarning));
            dateWarning.snackbar.setAction(getString(R.string.configAction_setDate), new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    configDate();
                }
            });
            dateWarning.show();
            return;
        }

        // no warnings shown; clear previous (stale) messages
        timezoneWarning.dismiss();
        dateWarning.dismiss();
    }

    /**
     * Start updates to the clock ui.
     */
    private void startTimeTask() {
        txt_time.post(updateTimeTask);
    }

    /**
     * Stop updates to the clock ui.
     */
    private void stopTimeTask() {
        txt_time.removeCallbacks(updateTimeTask);
    }

    /**
     * Clock ui update rate; once every few seconds.
     */
    public static final int UPDATE_RATE = 3000; // primary update rate: 3s

    /**
     * Update the clock ui at regular intervals to reflect current time (and note).
     */
    private Runnable updateTimeTask = new Runnable() {
        @Override
        public void run() {
            updateTimeViews(SuntimesActivity.this);
            txt_time.postDelayed(this, UPDATE_RATE);
        }
    };

    /**
     * Update the clock ui to reflect current time.
     * @param context the Activity context
     */
    protected void updateTimeViews(Context context) {
        Calendar now = dataset.now();
        //Log.d("DEBUG", "" + now.getTimeZone());
        SuntimesUtils.TimeDisplayText timeText = utils.calendarTimeShortDisplayString(this, now);
        txt_time.setText(timeText.getValue());
        txt_time_suffix.setText(timeText.getSuffix());
        notes.updateNote(context, now);
        lightmap.updateViews(false);
    }

    /**
     * onTouch swipe between the prev/next items in the view_flipper
     * @param event the touch MotionEvent
     * @return true continue gesture (propagate event), false end gesture (consume event)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    /**
     * viewFlipper "note" onTouchListener; swipe between available notes
     */
    private View.OnTouchListener noteTouchListener = new View.OnTouchListener() {
        public int MOVE_SENSITIVITY = 25;
        public int FLING_SENSITIVITY = 10;
        public float firstTouchX, secondTouchX;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstTouchX = event.getX();
                break;

            case MotionEvent.ACTION_UP:
                secondTouchX = event.getX();
                if ((firstTouchX - secondTouchX) >= FLING_SENSITIVITY) {
                    setUserSwappedCard(false, "noteTouchListener (fling next)");
                    if (isRtl)
                        notes.showPrevNote();
                    else
                        notes.showNextNote(); // swipe right: next

                } else if ((secondTouchX - firstTouchX) > FLING_SENSITIVITY) {
                    setUserSwappedCard(false, "noteTouchListener (fling prev)");
                    if (isRtl)
                        notes.showNextNote();
                    else
                        notes.showPrevNote(); // swipe left: prev

                } else { // click: user defined
                    AppSettings.ClockTapAction action = AppSettings.loadNoteTapActionPref(SuntimesActivity.this);
                    switch (action) {
                    case NOTHING:
                        break;

                    case ALARM:
                        scheduleAlarmFromNote();
                        break;

                    case PREV_NOTE:
                        setUserSwappedCard(false, "noteTouchListener (tap prev)");
                        notes.showPrevNote();
                        break;

                    case NEXT_NOTE:
                    default:
                        setUserSwappedCard(false, "noteTouchListener (tap next)");
                        notes.showNextNote();
                        break;
                    }
                }
                break;

            case MotionEvent.ACTION_MOVE:
                final View currentView = note_flipper.getCurrentView();
                int moveDeltaX = (isRtl ? (int) (firstTouchX - event.getX()) : (int) (event.getX() - firstTouchX));
                if (Math.abs(moveDeltaX) < MOVE_SENSITIVITY) {
                    currentView.layout(moveDeltaX, currentView.getTop(), currentView.getWidth(),
                            currentView.getBottom());
                }
                break;
            }
            return false;
        }
    };

    /**
     * viewFlipper "time card" onTouchListener; swipe left/right between viewflipper layouts (today/tomorrow)
     */
    private View.OnTouchListener timeCardTouchListener = new View.OnTouchListener() {
        public int MOVE_SENSITIVITY = 150;
        public int FLING_SENSITIVITY = 25;
        public float firstTouchX, secondTouchX;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstTouchX = event.getX();
                break;

            case MotionEvent.ACTION_UP:
                secondTouchX = event.getX();
                if ((secondTouchX - firstTouchX) > FLING_SENSITIVITY) { // swipe right; back to previous view
                    boolean flipResult = (isRtl ? showNextCard() : showPreviousCard());
                    setUserSwappedCard(userSwappedCard || flipResult, "timeCardTouchListener (fling prev)");

                } else if (firstTouchX - secondTouchX > FLING_SENSITIVITY) {
                    // swipe left; advance to next view
                    boolean flipResult = (isRtl ? showPreviousCard() : showNextCard());
                    setUserSwappedCard(userSwappedCard || flipResult, "timeCardTouchListener (fling next)");

                } else {
                    // swipe cancel; reset current view
                    final View currentView = card_flipper.getCurrentView();
                    currentView.layout(0, currentView.getTop(), currentView.getWidth(), currentView.getBottom());
                }
                break;

            case MotionEvent.ACTION_MOVE:
                float currentTouchX = event.getX();
                int moveDelta = (int) (currentTouchX - firstTouchX);
                boolean isSwipeRight = (moveDelta > 0);

                final View currentView = card_flipper.getCurrentView();
                int currentIndex = card_flipper.getDisplayedChild();

                int otherIndex;
                if (isRtl) {
                    otherIndex = (isSwipeRight ? currentIndex + 1 : currentIndex - 1);
                } else {
                    otherIndex = (isSwipeRight ? currentIndex - 1 : currentIndex + 1);
                }

                if (otherIndex >= 0 && otherIndex < card_flipper.getChildCount()) {
                    // in-between child views; flip between them
                    currentView.layout(moveDelta, currentView.getTop(), moveDelta + currentView.getWidth(),
                            currentView.getBottom());

                    // extended movement; manually trigger swipe/fling
                    if (moveDelta > MOVE_SENSITIVITY || moveDelta < MOVE_SENSITIVITY * -1) {
                        event.setAction(MotionEvent.ACTION_UP);
                        return onTouch(view, event);
                    }

                } //else {
                  // at-a-boundary (the first/last view);
                  // TODO: animate somehow to let user know there aren't additional views
                  //}
                break;
            }
            return true;
        }
    };

    /**
     * Show the 'next' set of data displayed by the main view_flipper.
     */
    public boolean showNextCard() {
        if (hasNextCard()) {
            card_flipper.setOutAnimation(anim_card_outNext);
            card_flipper.setInAnimation(anim_card_inNext);
            card_flipper.showNext();
            return true;
        }
        return false;
    }

    public boolean hasNextCard() {
        int current = card_flipper.getDisplayedChild();
        return ((current + 1) < card_flipper.getChildCount());
    }

    /**
     * Show the 'previous' set of data displayed by the main view_flipper.
     */
    public boolean showPreviousCard() {
        if (hasPreviousCard()) {
            card_flipper.setOutAnimation(anim_card_outPrev);
            card_flipper.setInAnimation(anim_card_inPrev);
            card_flipper.showPrevious();
            return true;
        }
        return false;
    }

    public boolean hasPreviousCard() {
        int current = card_flipper.getDisplayedChild();
        int prev = current - 1;
        return (prev >= 0);
    }

    View.OnClickListener onNextCardClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            setUserSwappedCard(showNextCard(), "onNextCardClick");
        }
    };

    View.OnClickListener onPrevCardClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            setUserSwappedCard(showPreviousCard(), "onPrevCardClick");
        }
    };

    View.OnClickListener onNextNoteClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            setUserSwappedCard(false, "onNextNoteClick");
            notes.showNextNote();
        }
    };

    View.OnClickListener onPrevNoteClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            setUserSwappedCard(false, "onPrevNoteClick");
            notes.showPrevNote();
        }
    };

    View.OnClickListener onClockClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            AppSettings.ClockTapAction action = AppSettings.loadClockTapActionPref(SuntimesActivity.this);
            if (action == AppSettings.ClockTapAction.NOTHING) {
                return;
            }

            if (action == AppSettings.ClockTapAction.ALARM) {
                scheduleAlarm();
                return;
            }

            if (action == AppSettings.ClockTapAction.NEXT_NOTE) {
                setUserSwappedCard(false, "onClockClick (nextNote)");
                notes.showNextNote();
                return;
            }

            if (action == AppSettings.ClockTapAction.PREV_NOTE) {
                setUserSwappedCard(false, "onClockClick (prevNote)");
                notes.showPrevNote();
                return;
            }

            Log.w("SuntimesActivity", "Unrecognized ClockTapAction (so doing nothing)");
        }
    };

    /**
     * Toggle day length visibility.
     * @param value true show daylength ui, false hide daylength ui
     */
    protected void showDayLength(boolean value) {
        layout_daylength.setVisibility((value ? View.VISIBLE : View.INVISIBLE));
        layout_daylength2.setVisibility((value ? View.VISIBLE : View.INVISIBLE));
    }

    /**
     * Toggle note flipper visibility.
     * @param value true show note ui, false hide note ui
     */
    protected void showNotes(boolean value) {
        note_flipper.setVisibility((value ? View.VISIBLE : View.INVISIBLE));
    }

    /**
     * Toggle lightmap visibility.
     * @param value true show lightmap ui, false hide lightmap ui
     */
    protected void showLightMap(boolean value) {
        lightmapLayout.setVisibility((value ? View.VISIBLE : View.GONE));
    }

    /**
     * Show the lightmap dialog.
     */
    protected void showLightMapDialog() {
        LightMapDialog lightMapDialog = new LightMapDialog();
        lightMapDialog.setData(dataset);
        lightMapDialog.show(getSupportFragmentManager(), DIALOGTAG_LIGHTMAP);
    }

    /**
     * @param tomorrow true is "tomorrow" date field, false is "today" date field
     * @return an OnClickListener for the specified date field
     */
    private View.OnClickListener dateTapClickListener(final boolean tomorrow) {
        return new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                AppSettings.DateTapAction action = AppSettings.loadDateTapActionPref(SuntimesActivity.this);
                switch (action) {
                case NOTHING:
                    break;

                case CONFIG_DATE:
                    configDate();
                    break;

                case SHOW_CALENDAR:
                    showCalendar();
                    break;

                case SWAP_CARD:
                default:
                    if (tomorrow) {
                        setUserSwappedCard(showPreviousCard(), "onDateTapClick (prevCard)");
                    } else {
                        setUserSwappedCard(showNextCard(), "onDateTapClick (nextCard)");
                    }
                    break;
                }
            }
        };
    }

    private void initTimeFields() {
        /**for (SolarEvents.SolarEventField key : timeFields.keySet())
        {
        TextView field = timeFields.get(key);
        field.setOnClickListener(createTimeFieldClickListener(key));
        }*/
    }

    private View.OnClickListener createTimeFieldClickListener(final SolarEvents.SolarEventField event) {
        return new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Log.d("DEBUG", "TimeField clicked: " + event.toString());
                notes.showNote(event);
            }
        };
    }

    public void highlightTimeField(SolarEvents.SolarEventField highlightField) {
        int nextCardOffset = 0;
        int currentCard = this.card_flipper.getDisplayedChild();

        for (SolarEvents.SolarEventField field : timeFields.keySet()) {
            TextView txtField = timeFields.get(field);
            if (txtField != null) {
                if (field.equals(highlightField)) {
                    txtField.setTypeface(txtField.getTypeface(), Typeface.BOLD);
                    txtField.setPaintFlags(txtField.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);

                    if (currentCard == 0 && field.tomorrow) {
                        nextCardOffset = 1;

                    } else if (currentCard == 1 && !field.tomorrow) {
                        nextCardOffset = -1;
                    }

                } else {
                    txtField.setTypeface(Typeface.create(txtField.getTypeface(), Typeface.NORMAL), Typeface.NORMAL);
                    txtField.setPaintFlags(txtField.getPaintFlags() & (~Paint.UNDERLINE_TEXT_FLAG));
                }
            }
        }

        if (!userSwappedCard) {
            //Log.d("DEBUG", "Swapping card to show highlighted :: userSwappedCard " + userSwappedCard);
            if (nextCardOffset > 0) {
                showNextCard();

            } else if (nextCardOffset < 0) {
                showPreviousCard();
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void showCalendar() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            long startMillis = dataset.now().getTimeInMillis();
            Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
            builder.appendPath("time");
            ContentUris.appendId(builder, startMillis);
            Intent intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
            startActivity(intent);
        }
    }

    private void adjustNoteIconSize(NoteData note, ImageView icon) {
        Resources resources = getResources();
        int iconWidth = (int) resources.getDimension(R.dimen.sunIconLarge_width);
        int iconHeight = ((note.noteIconResource == R.drawable.ic_noon_large) ? iconWidth
                : (int) resources.getDimension(R.dimen.sunIconLarge_height));

        ViewGroup.LayoutParams iconParams = icon.getLayoutParams();
        iconParams.width = iconWidth;
        iconParams.height = iconHeight;
    }

    protected void updateNoteUI(NoteData note, int transition) {
        if (note_flipper.getDisplayedChild() == 0) {
            // currently using view1, ready view2
            ic_time2_note.setBackgroundResource(note.noteIconResource);
            adjustNoteIconSize(note, ic_time2_note);
            ic_time2_note.setVisibility(View.VISIBLE);
            txt_time2_note1.setText(" " + note.timeText.toString()); // todo: fix spacing in layout
            txt_time2_note2.setText(note.prefixText);
            txt_time2_note3.setText(note.noteText);
            txt_time2_note3.setTextColor(note.noteColor);

        } else {
            // currently using view2, ready view1
            ic_time1_note.setBackgroundResource(note.noteIconResource);
            adjustNoteIconSize(note, ic_time1_note);
            ic_time1_note.setVisibility(View.VISIBLE);
            txt_time1_note1.setText(" " + note.timeText.toString()); // todo: fix spacing in layout
            txt_time1_note2.setText(note.prefixText);
            txt_time1_note3.setText(note.noteText);
            txt_time1_note3.setTextColor(note.noteColor);
        }

        if (transition == NoteChangedListener.TRANSITION_NEXT) {
            note_flipper.setInAnimation(anim_note_inNext);
            note_flipper.setOutAnimation(anim_note_outNext);
            note_flipper.showNext();

        } else {
            note_flipper.setInAnimation(anim_note_inPrev);
            note_flipper.setOutAnimation(anim_note_outPrev);
            note_flipper.showPrevious();
        }

        highlightTimeField(new SolarEvents.SolarEventField(note.noteMode, note.tomorrow));
    }

    /**
     * Stretch the horizontal rule to match the actual table width.. this is a hack to work around
     * unwanted stretching of the GridLayout columns when setting the hr to match_parent or fill_parent.
     */
    private void stretchTableRule() {
        LinearLayout[] cards = new LinearLayout[2];
        cards[0] = (LinearLayout) findViewById(R.id.info_time_all_today);
        cards[1] = (LinearLayout) findViewById(R.id.info_time_all_tomorrow);
        for (LinearLayout card : cards) // for each card
        {
            View tableRule = card.findViewById(R.id.table_rule);
            if (tableRule != null) {
                LinearLayout[] cols = new LinearLayout[3];
                cols[0] = (LinearLayout) card.findViewById(R.id.table_head_date);
                cols[1] = (LinearLayout) card.findViewById(R.id.table_head_rise);
                cols[2] = (LinearLayout) card.findViewById(R.id.table_head_set);

                int tableWidth = 0;
                for (LinearLayout col : cols) // add up the measured column widths
                {
                    if (col != null) {
                        col.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                        tableWidth += col.getMeasuredWidth();
                    }
                }

                ViewGroup.LayoutParams tableRuleParams = tableRule.getLayoutParams();
                tableRuleParams.width = tableWidth;
                tableRule.setLayoutParams(tableRuleParams); // and adjust the horizontal rule width
            }
        }
    }

    /**
     * SuntimesWarning; wraps a Snackbar and some flags.
     */
    private class SuntimesWarning {
        public static final String KEY_WASDISMISSED = "userDismissedWarning";

        public SuntimesWarning(String id) {
            this.id = id;
        }

        protected String id = "";
        protected Snackbar snackbar = null;
        protected boolean shouldShow = false;
        protected boolean wasDismissed = false;

        public void initWarning(Context context, String msg) {
            ImageSpan warningIcon = SuntimesUtils.createWarningSpan(context, txt_date.getTextSize());
            SpannableStringBuilder message = SuntimesUtils.createSpan(msg, warningIcon);

            wasDismissed = false;
            snackbar = Snackbar.make(card_flipper, message, Snackbar.LENGTH_INDEFINITE);
            snackbar.setCallback(new Snackbar.Callback() {
                @Override
                public void onDismissed(Snackbar snackbar, int event) {
                    super.onDismissed(snackbar, event);
                    switch (event) {
                    case DISMISS_EVENT_SWIPE:
                        wasDismissed = true;
                        showNextWarning();
                        break;
                    }
                }
            });
        }

        private void showNextWarning() {
            showWarnings();
        }

        public boolean isShown() {
            return (snackbar != null && snackbar.isShown());
        }

        public void show() {
            if (snackbar != null) {
                snackbar.show();
            }
        }

        public void dismiss() {
            if (isShown()) {
                snackbar.dismiss();
            }
        }

        public void reset() {
            wasDismissed = false;
            shouldShow = false;
        }

        public void save(Bundle outState) {
            if (outState != null) {
                outState.putBoolean(KEY_WASDISMISSED + id, wasDismissed);
            }
        }

        public void restore(Bundle savedState) {
            if (savedState != null) {
                wasDismissed = savedState.getBoolean(KEY_WASDISMISSED + id, false);
            }
        }
    }

    /**
     * Save the state of warning objects to Bundle.
     * @param outState a Bundle to save state to
     */
    private void saveWarnings(Bundle outState) {
        for (SuntimesWarning warning : warnings) {
            warning.save(outState);
        }
    }

    /**
     * Restore the state of warning objects from Bundle.
     * @param savedState a Bundle containing saved state
     */
    private void restoreWarnings(Bundle savedState) {
        for (SuntimesWarning warning : warnings) {
            warning.restore(savedState);
        }
    }

    private void setUserSwappedCard(boolean value, String tag) {
        userSwappedCard = value;
        //Log.d("DEBUG", "userSwappedCard set " + value + " (" + tag + " )");
    }

    /**
     * Get the current theme's resource id (used by test verification).
     * @return the resource id of the current theme/style (or 0 if getTHemeResId failed)
     */
    public int getThemeId() {
        try {
            Method method = Context.class.getMethod("getThemeResId");
            method.setAccessible(true);
            return (Integer) method.invoke(this);

        } catch (Exception e) {
            Log.e("getThemeId", "Failed to get theme ID");
            e.printStackTrace();
        }
        return 0;
    }

}