public class MainActivity extends AppCompatActivity implements SiteSettingsAdapter.Handler, TaskCallback<CallSiteTask, Void, List<Pair<SiteSettings, SiteCall>>>, NetworkBroadcastReceiver.Listener, AlarmBroadcastReceiver.Listener {
    private static final String TAG_TASK_FRAGMENT = "main_activity_task_fragment";
    private static final String TAG = MainActivity.class.getSimpleName();
    private static final String PARCEL_SITE_LIST = "SITE_LIST";

    private MainActivity context = this;
    private ListView listView;
    private TextView connectivityAlertView;
    private TaskFragment taskFragment;
    private Chronometer chronometer;
    private CountDownTimer countDownTimer;
    private NetworkBroadcastReceiver networkBroadcastReceiver;
    private AlarmBroadcastReceiver alarmBroadcastReceiver;
    private View timerBannerView;
    private DBHelper dbHelper;
    private AlarmUtil alarmUtil = AlarmUtil.instance();
    private SiteSettingsAdapter siteSettingsAdapter;
    private List<SiteSettingsBusiness> siteSettingsList;
    private boolean loadDataFromDb;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = DBHelper.getHelper(this);
        listView = this.findViewById(R.id.listView);
        connectivityAlertView = this.findViewById(R.id.connectivityAlert);
        chronometer = this.findViewById(R.id.chronometer);
        timerBannerView = this.findViewById(R.id.timerBanner);

        FragmentManager fragmentManager = getSupportFragmentManager();
        taskFragment = (TaskFragment) fragmentManager.findFragmentByTag(TAG_TASK_FRAGMENT);
        if (taskFragment == null) {
            taskFragment = new TaskFragment();
            fragmentManager.beginTransaction().add(taskFragment, TAG_TASK_FRAGMENT).commit();
        }

        if (savedInstanceState == null || savedInstanceState.isEmpty()) {
            siteSettingsList = new ArrayList<>();
            loadDataFromDb = true;
        } else {
            siteSettingsList = savedInstanceState.getParcelableArrayList(PARCEL_SITE_LIST);
        }
        siteSettingsAdapter = new SiteSettingsAdapter(context, this, siteSettingsList);
        listView.setAdapter(siteSettingsAdapter);
        if (networkBroadcastReceiver == null) {
            networkBroadcastReceiver = new NetworkBroadcastReceiver(this);
        }
        if (alarmBroadcastReceiver == null) {
            alarmBroadcastReceiver = new AlarmBroadcastReceiver(this);
        }

        NotificationUtil.createNotificationChannel(this);
        // start alarm on 1st install or if not started on system boot
        alarmUtil.startAlarmIfNeeded(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        loadDataFromDb = true;
        LocalBroadcastManager.getInstance(this).unregisterReceiver(alarmBroadcastReceiver);
        LocalBroadcastManager.getInstance(this).unregisterReceiver(networkBroadcastReceiver);
        unregisterReceiver(networkBroadcastReceiver);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (dbHelper != null) {
            dbHelper.release();
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelableArrayList(PARCEL_SITE_LIST, new ArrayList<Parcelable>(siteSettingsList));
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (loadDataFromDb) {
            loadSiteSettingsBusinesses();
        } else {
            loadDataFromDb = false;
        }
        scheduleTimer();
        onNetworkStateChanged(ConnectivityUtil.isConnected(this));

        LocalBroadcastManager.getInstance(this).registerReceiver(alarmBroadcastReceiver, new IntentFilter(AlarmUtil.ACTION_NEXT_ALARM_SET));
        LocalBroadcastManager.getInstance(this).registerReceiver(networkBroadcastReceiver, new IntentFilter(NetworkService.ACTION_SITE_START_REFRESH));
        LocalBroadcastManager.getInstance(this).registerReceiver(networkBroadcastReceiver, new IntentFilter(NetworkService.ACTION_SITE_END_REFRESH));
        LocalBroadcastManager.getInstance(this).registerReceiver(networkBroadcastReceiver, new IntentFilter(FavIconService.ACTION_FAVICON_UPDATED));
        registerReceiver(networkBroadcastReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
    }

    private void loadSiteSettingsBusinesses() {
        try {
            List<SiteSettings> list = dbHelper.getDBSiteSettings().queryForAll();
            siteSettingsList.clear();
            for (SiteSettings siteSettings : list) {
                siteSettingsList.add(new SiteSettingsBusiness(siteSettings));
            }
        } catch (SQLException e) {
            Log.e(TAG, "queryForAll", e);
        }

        Collections.sort(siteSettingsList, SiteSettingsBusiness.NAME_COMPARATOR);
        siteSettingsAdapter.notifyDataSetChanged();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);

        if (BuildConfig.DEBUG) {
            MenuItem debugItem = menu.findItem(R.id.action_debug);
            debugItem.setVisible(true);
            MenuItem injectItem = menu.findItem(R.id.action_inject);
            injectItem.setVisible(true);
        }
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        if (id == R.id.action_refresh) {
            if (BuildConfig.DEBUG) {
                Log.i(TAG, "global refresh requested");
            }
            NetworkService.enqueueCheckSitesWork(this);
            return true;
        }
        if (id == R.id.action_settings) {
            PrefSettingsActivity.start(context);
            return true;
        }
        if (id == R.id.action_about) {
            AboutActivity.start(context);
            return true;
        }
        if (id == R.id.action_inject) {
            try {
                DummySiteInjector.inject(this.getCurrentFocus(), taskFragment, dbHelper.getDBSiteSettings());
                loadSiteSettingsBusinesses();
                alarmUtil.startAlarmIfNeeded(context);
            } catch (SQLException e) {
                Log.e(TAG, "dummyinject", e);
            }
            return true;
        }
        if (id == R.id.action_debug) {
            StringBuilder sb = new StringBuilder("you're on alpha version").append("\n");
            sb.append("Connectivity: ").append(ConnectivityUtil.isConnected(context)).append("\n");

            int startupBootState = context.getPackageManager().getComponentEnabledSetting(new ComponentName(context, StartupBootReceiver.class));
            boolean startupBootEnable = (startupBootState == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || startupBootState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
            sb.append("StartupBoot state: ").append(startupBootEnable).append("\n");
            sb.append("Alarm set: ").append(alarmUtil.getCurrentInterval()).append("\n");
            sb.append("Battery: ").append(BatteryLevelReceiver.getLastAction()).append("\n");
            Toast.makeText(this, sb.toString(), Toast.LENGTH_LONG).show();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onPreExecute(CallSiteTask task) {
    }

    @Override
    public void onProgressUpdate(CallSiteTask task, Void... percent) {
    }

    @Override
    public void onPostExecute(CallSiteTask task, List<Pair<SiteSettings, SiteCall>> result) {
    }


    @Override
    public void onCancelled(CallSiteTask task) {
    }

    @Override
    public void onSiteStartRefresh(SiteSettings siteSettings) {
        int position = siteSettingsAdapter.getPosition(new SiteSettingsBusiness(siteSettings));
        if (position != -1) {
            siteSettingsAdapter.getItem(position).setIsChecking(true);
        }
        siteSettingsAdapter.notifyDataSetChanged();
    }

    @Override
    public void onSiteEndRefresh(SiteSettings siteSettings, SiteCall siteCall) {
        int position = siteSettingsAdapter.getPosition(new SiteSettingsBusiness(siteSettings));
        if (position != -1) {
            SiteSettingsBusiness siteSettingsView = siteSettingsAdapter.getItem(position);
            siteSettingsView.setIsChecking(false);
            siteSettingsView.getCalls().add(siteCall);
            siteSettingsAdapter.notifyDataSetChanged();
        }
    }

    @Override
    public void onFaviconUpdated(SiteSettings siteSettings, Bitmap favicon) {
        int position = siteSettingsAdapter.getPosition(new SiteSettingsBusiness(siteSettings));
        if (position != -1) {
            SiteSettingsBusiness siteSettingsView = siteSettingsAdapter.getItem(position);
            siteSettingsView.setFavicon(favicon);
            siteSettingsAdapter.notifyDataSetChanged();
        }
    }

    @Override
    public void onNetworkStateChanged(boolean hasConnectivity) {
        if (hasConnectivity && connectivityAlertView.getVisibility() != View.GONE) {
            connectivityAlertView.setVisibility(View.GONE);
        } else if (!hasConnectivity && connectivityAlertView.getVisibility() != View.VISIBLE) {
            connectivityAlertView.setVisibility(View.VISIBLE);
        }
    }

    private void scheduleTimer() {
        if (countDownTimer != null) {
            countDownTimer.cancel();
        }
        final long nextAlarmTime = alarmUtil.getNextAlarmTime(this);
        final long nextAlarmInterval = alarmUtil.getCountUntilNextAlarmTime(this);
        if (nextAlarmInterval > 0) {
            timerBannerView.setVisibility(View.VISIBLE);
            countDownTimer = new CountDownTimer(nextAlarmInterval, TimeUtil._1_SEC * 5) {
                public void onTick(long millisUntilFinished) {
                    chronometer.setText(DateUtils.getRelativeTimeSpanString(nextAlarmTime));
                    chronometer.setText(DateUtils.getRelativeTimeSpanString(nextAlarmTime, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS));
                }

                public void onFinish() {
                    chronometer.setText(R.string.imminent);
                }
            }.start();
        } else {
            timerBannerView.setVisibility(View.GONE);
        }
    }

    public void floatingAddSite(final View v) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(R.string.add_monitor);
        final EditText input = new EditText(context);
        input.setHint(R.string.hint_site_url);
        input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_URI);
        builder.setView(input);
        builder.setPositiveButton(R.string.action_add, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String host = input.getText().toString().trim();
                if (host.isEmpty()) {
                    return;
                }
                try {
                    DBSiteSettings dbSiteSettings = dbHelper.getDBSiteSettings();
                    if (dbSiteSettings.findForHost(host) != null) {
                        Snackbar.make(v, host + getString(R.string.already_exists), Snackbar.LENGTH_SHORT).show();
                        return;
                    }
                    SiteSettings siteSettings = new SiteSettings(host);
                    dbSiteSettings.create(siteSettings);
                    alarmUtil.startAlarmIfNeeded(context);
                    new CallSiteTask(context, taskFragment).execute(siteSettings);
                    SiteSettingsActivity.start(context, siteSettings.getHost());
                } catch (SQLException e) {
                    Log.e(TAG, "create", e);
                }
            }
        });
        builder.setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
            }
        });
        builder.show();
    }

    @Override
    public void touched(SiteSettingsBusiness siteSettings) {
        SiteSettingsActivity.start(context, siteSettings.getHost());
    }

    @Override
    public void onNextAlarmChange(long nextAlarm) {
        scheduleTimer();
    }
} public class PrefSettingsActivity extends AppCompatPreferenceActivity {

    public static final String NOTIFICATIONS_VIBRATE = "notifications_vibrate";
    public static final String NOTIFICATIONS_RINGTONE = "notifications_ringtone";
    public static final String NOTIFICATION_ENABLE = "notifications_enable";
    public static final String NOTIFICATION_LIMIT_TO_NEW_FAIL = "notifications_limit_to_new_fail";

    public static final String NOTIFICATION_LIGHT_COLOR = "notification_light_color";
    public static final String BOOT_START = "boot_start";
    public static final String FREQUENCY = "frequency";
    /**
     * Determines whether to always show the simplified settings UI, where
     * settings are presented in a single list. When false, settings are shown
     * as a master/detail two-pane view on tablets. When true, a single pane is
     * shown on tablets.
     */
    private static final boolean ALWAYS_SIMPLE_PREFS = false;
    private static AlarmUtil alarmUtil = AlarmUtil.instance();
    /**
     * A preference value change listener that updates the preference's summary to reflect its new value.
     */
    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
        @Override
        public boolean onPreferenceChange(Preference preference, Object value) {
            String stringValue = value.toString();

            if (preference instanceof ListPreference) {
                // For list preferences, look up the correct display value in the preference's 'entries' list.
                ListPreference listPreference = (ListPreference) preference;
                int index = listPreference.findIndexOfValue(stringValue);
                preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null);

                if (preference.getKey().equals(FREQUENCY)) {
                    String currentValue = preference.getSharedPreferences().getString(FREQUENCY, "");
                    if (!currentValue.equals(stringValue)) {
                        alarmUtil.rescheduleAlarm(preference.getContext(), Long.parseLong(stringValue) * TimeUtil.MINUTE_2_MILLISEC);
                    }
                }

            } else if (preference instanceof RingtonePreference) {
                // For ringtone preferences, look up the correct display value using RingtoneManager.
                if (TextUtils.isEmpty(stringValue)) {
                    preference.setSummary(R.string.pref_ringtone_silent);
                } else {
                    Ringtone ringtone = RingtoneManager.getRingtone(preference.getContext(), Uri.parse(stringValue));
                    if (ringtone == null) {
                        // Clear the summary if there was a lookup error.
                        preference.setSummary(null);
                    } else {
                        String name = ringtone.getTitle(preference.getContext());
                        preference.setSummary(name);
                    }
                }

            } else {
                // For all other preferences, set the summary to the value's simple string representation.
                preference.setSummary(stringValue);
            }
            return true;
        }
    };

    private static Preference.OnPreferenceChangeListener sPreferenceListener = new Preference.OnPreferenceChangeListener() {
        @Override
        public boolean onPreferenceChange(Preference preference, Object value) {
            boolean state = (Boolean) value;
            if (preference.getKey().equals(BOOT_START)) {
                StartupBootReceiver.setCanBeInitiatedBySystem(preference.getContext(), state);
            }
            return true;
        }
    };


    /**
     * Helper method to determine if the device has an extra-large screen. For example, 10" tablets are extra-large.
     */

    private static boolean isXLargeTablet(Context context) {
        return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
    }

    /**
     * Determines whether the simplified settings UI should be shown. This is  true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
     * doesn't have newer APIs like {@link PreferenceFragment}, or the device doesn't have an extra-large screen. In these cases, a single-pane
     * "simplified" settings UI should be shown.
     */
    private static boolean isSimplePreferences(Context context) {
        return !isXLargeTablet(context) || ALWAYS_SIMPLE_PREFS;
    }

    /**
     * Binds a preference's summary to its value. More specifically, when the preference's value is changed, its summary (line of text below the
     * preference title) is dataChanged to reflect the value. The summary is also immediately dataChanged upon calling this method. The exact display format is
     * dependent on the type of preference.
     *
     * @see #sBindPreferenceSummaryToValueListener
     */
    private static void bindPreferenceSummaryToValue(Preference preference) {
        // Set the listener to watch for value changes.
        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);

        // Trigger the listener immediately with the preference's current value.
        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, PreferenceManager.getDefaultSharedPreferences(preference.getContext()).getString(preference.getKey(), ""));
    }

    public static void start(Context context) {
        context.startActivity(new Intent(context, PrefSettingsActivity.class));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupActionBar();
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        setupSimplePreferencesScreen();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onIsMultiPane() {
        return isXLargeTablet(this) && !isSimplePreferences(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onBuildHeaders(List<Header> target) {
        if (!isSimplePreferences(this)) {
            loadHeadersFromResource(R.xml.pref_headers, target);
        }
    }

    /**
     * Set up the {@link android.app.ActionBar}
     */
    private void setupActionBar() {
        // Show the Up button in the action bar.
        ActionBar actionBar = getActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            // This ID represents the Home or Up button. In the case of this activity, the Up button is shown. Use NavUtils to allow users
            // to navigate up one level in the application structure. For more details, see the Navigation pattern on Android Design:
            //
            // http://developer.android.com/design/patterns/navigation.html#up-vs-back
            //
            NavUtils.navigateUpFromSameTask(this);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected boolean isValidFragment(String fragmentName) {
        return true;
    }

    /**
     * Shows the simplified settings UI if the device configuration if the
     * device configuration dictates that a simplified, single-pane UI should be shown.
     */
    private void setupSimplePreferencesScreen() {
        if (!isSimplePreferences(this)) {
            return;
        }

        // In the simplified UI, fragments are not used at all and we instead use the older PreferenceActivity APIs.
        addPreferencesFromResource(R.xml.pref_monitoring);
        addPreferencesFromResource(R.xml.pref_notification);
        addPreferencesFromResource(R.xml.pref_history);

        // Bind the summaries of EditText/List/Dialog/Ringtone preferences to their values. When their values change,
        // their summaries are dataChanged to reflect the new value, per the Android Design guidelines.
        bindPreferenceSummaryToValue(findPreference(NOTIFICATIONS_RINGTONE));
        bindPreferenceSummaryToValue(findPreference(FREQUENCY));
        bindPreferenceSummaryToValue(findPreference(NOTIFICATION_LIGHT_COLOR));

        findPreference(BOOT_START).setOnPreferenceChangeListener(sPreferenceListener);
        findPreference(NOTIFICATION_LIMIT_TO_NEW_FAIL).setOnPreferenceChangeListener(sPreferenceListener);
        findPreference(NOTIFICATION_ENABLE).setOnPreferenceChangeListener(sPreferenceListener);
    }

    /**
     * This fragment shows notification preferences only. It is used when the activity is showing a two-pane settings UI.
     */
    public static class NotificationPreferenceFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.pref_notification);

            // Bind the summaries of EditText/List/Dialog/Ringtone preferences to their values. When their values change, their summaries are
            // dataChanged to reflect the new value, per the Android Design guidelines.
            bindPreferenceSummaryToValue(findPreference(NOTIFICATIONS_RINGTONE));
            bindPreferenceSummaryToValue(findPreference(NOTIFICATION_LIGHT_COLOR));

            findPreference(NOTIFICATION_ENABLE).setOnPreferenceChangeListener(sPreferenceListener);
            findPreference(NOTIFICATION_LIMIT_TO_NEW_FAIL).setOnPreferenceChangeListener(sPreferenceListener);
        }
    }

    /**
     * This fragment shows monitoring preferences only.
     */
    public static class MonitoringPreferenceFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.pref_monitoring);

            // Bind the summaries of EditText/List/Dialog/Ringtone preferences to their values. When their values change, their summaries are
            // dataChanged to reflect the new value, per the Android Design guidelines.
            bindPreferenceSummaryToValue(findPreference(FREQUENCY));

            findPreference(BOOT_START).setOnPreferenceChangeListener(sPreferenceListener);
        }
    }


    /**
     * This fragment shows history preferences only.
     */
    public static class HistoryPreferenceFragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.pref_history);
        }
    }
} public enum NetworkCallResult {
    FAIL,
    NO_CONNECTIVITY,
    SUCCESS
} @DatabaseTable
public class SiteCall implements Parcelable {
    public static final Creator<SiteCall> CREATOR = new Creator<SiteCall>() {
        @Override
        public SiteCall createFromParcel(Parcel in) {
            return new SiteCall(in);
        }

        @Override
        public SiteCall[] newArray(int size) {
            return new SiteCall[size];
        }
    };
    public static final Comparator<SiteCall> ASC_DATE = new Comparator<SiteCall>() {
        @Override
        public int compare(SiteCall lhs, SiteCall rhs) {
            return lhs.date.compareTo(rhs.date);
        }
    };
    public static final Comparator<SiteCall> DESC_DATE = new Comparator<SiteCall>() {
        @Override
        public int compare(SiteCall lhs, SiteCall rhs) {
            return rhs.date.compareTo(lhs.date);
        }
    };
    @DatabaseField(generatedId = true)
    private Long id;
    @Expose
    @DatabaseField(canBeNull = false)
    private Date date;
    @Expose
    @DatabaseField(canBeNull = false)
    private NetworkCallResult result;
    @Expose
    @DatabaseField
    private Integer responseCode;
    @Expose
    @DatabaseField(canBeNull = false)
    private Long responseTime;
    @Expose
    @DatabaseField
    private String exception;
    @DatabaseField(foreign = true, canBeNull = false)
    private SiteSettings siteSettings;

    public SiteCall() {
    }

    public SiteCall(Date date, NetworkCallResult callResult) {
        this(date, callResult, 0L, null);
    }

    public SiteCall(Date date, NetworkCallResult callResult, Long responseTime, int responseCode) {
        this(date, callResult, responseCode, responseTime, null);
    }

    public SiteCall(Date date, NetworkCallResult callResult, Long responseTime, Exception e) {
        this(date, callResult, HttpURLConnection.HTTP_NOT_FOUND, responseTime, e);
    }

    public SiteCall(Date date, NetworkCallResult callResult, Integer responseCode, Long responseTime, Exception e) {
        this.date = date;
        this.result = callResult;
        this.responseCode = responseCode;
        if (e != null) {
            this.exception = e.getLocalizedMessage();
            if (exception == null && e instanceof SocketTimeoutException) {
                this.exception = "timeout";
            }
        }
        this.responseTime = responseTime;
    }

    public SiteCall(Parcel in) {
        id = in.readLong();
        date = new Date(in.readLong());
        result = NetworkCallResult.values()[in.readInt()];
        responseCode = (Integer) in.readValue(Integer.class.getClassLoader());
        exception = in.readString();
    }

    public Integer getResponseCode() {
        return responseCode;
    }

    public String getException() {
        return exception;
    }

    public Date getDate() {
        return date;
    }

    public NetworkCallResult getResult() {
        return result;
    }

    public Long getResponseTime() {
        return responseTime;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public SiteSettings getSiteSettings() {
        return siteSettings;
    }

    public void setSiteSettings(SiteSettings siteSettings) {
        this.siteSettings = siteSettings;
    }

    @Override
    public String toString() {
        return "SiteCall{" +
                "date=" + date +
                ", result=" + result +
                ", responseCode=" + responseCode +
                ", responseTime=" + responseTime +
                ", exception='" + exception + '\'' +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeLong(date.getTime());
        dest.writeInt(result.ordinal());
        // write int doesn't accept null
        dest.writeValue(responseCode);
        dest.writeString(exception);
    }
} @DatabaseTable
public class SiteSettings implements Parcelable {

    public static final Creator<SiteSettings> CREATOR = new Creator<SiteSettings>() {
        @Override
        public SiteSettings createFromParcel(Parcel in) {
            return new SiteSettings(in);
        }

        @Override
        public SiteSettings[] newArray(int size) {
            return new SiteSettings[size];
        }
    };

    @DatabaseField(generatedId = true)
    private Long id;
    @DatabaseField
    private String name;
    @DatabaseField(canBeNull = false, uniqueIndex = true)
    private String host;
    @DatabaseField(canBeNull = true, uniqueIndex = true)
    private String internalUrl;
    @DatabaseField
    private boolean isNotificationEnabled = true;
    @DatabaseField
    private boolean forcedCertificate = false;
    @DatabaseField(dataType = DataType.BYTE_ARRAY)
    private byte[] favicon;
    @ForeignCollectionField(eager = true)
    private ForeignCollection<SiteCall> siteCalls;


    public SiteSettings() {
    }

    public SiteSettings(String host) {
        this.host = host;
        this.name = host;
    }

    public SiteSettings(Parcel in) {
        name = in.readString();
        host = in.readString();
        internalUrl = in.readString();
        isNotificationEnabled = in.readInt() == 1;
        in.readByteArray(favicon);
    }

    public String getHost() {
        return host;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isNotificationEnabled() {
        return isNotificationEnabled;
    }

    public void setNotificationEnabled(boolean notificationEnabled) {
        this.isNotificationEnabled = notificationEnabled;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public ForeignCollection<SiteCall> getSiteCalls() {
        return siteCalls;
    }

    public void setSiteCalls(ForeignCollection<SiteCall> siteCalls) {
        this.siteCalls = siteCalls;
    }

    public boolean isForcedCertificate() {
        return forcedCertificate;
    }

    public void setForcedCertificate(boolean forcedCertificate) {
        this.forcedCertificate = forcedCertificate;
    }

    public byte[] getFavicon() {
        return favicon;
    }

    public void setFavicon(byte[] favicon) {
        this.favicon = favicon;
    }

    public String getInternalUrl() {
        return internalUrl;
    }

    public void setInternalUrl(String internalUrl) {
        this.internalUrl = internalUrl;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof SiteSettings) {
            SiteSettings obj = (SiteSettings) o;
            return host.equals(obj.host);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return host.hashCode();
    }

    @Override
    public String toString() {
        return "SiteSettings{" +
                "name='" + name + '\'' +
                ", host='" + host + '\'' +
                ", internalUrl='" + internalUrl + '\'' +
                ", isNotificationEnabled=" + isNotificationEnabled +
                ", calls=" + (siteCalls == null ? "0" : siteCalls.size()) +
                '}';
    }


    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(host);
        dest.writeString(internalUrl);
        dest.writeInt(isNotificationEnabled ? 1 : 0);
        if (favicon != null) {
            dest.writeByteArray(favicon);
        }
    }

} public class DBHelper extends OrmLiteSqliteOpenHelper {

    private static final String DATABASE_NAME = "sitemonitor.db";
    private static final int DATABASE_VERSION = 2;
    private static final String TAG = DBHelper.class.getSimpleName();

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    /**
     * Returns DBHelper. Each time you call this method, you must call #release
     *
     * @param context
     * @return DBHelper
     * @see #release()
     */
    public static DBHelper getHelper(Context context) {
        return OpenHelperManager.getHelper(context, DBHelper.class);
    }

    /**
     * This is called when the database is first created. Usually you should call createTable statements here to create
     * the tables that will store your data.
     */
    @Override
    public void onCreate(SQLiteDatabase db, ConnectionSource connectionSource) {
        try {
            TableUtils.createTable(connectionSource, SiteSettings.class);
            TableUtils.createTable(connectionSource, SiteCall.class);
            Log.i(DBHelper.class.getName(), "database created");
        } catch (SQLException e) {
            Log.e(TAG, "Can't create database", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * This is called when your application is upgraded and it has a higher version number. This allows you to adjust
     * the various data to match the new version number.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            try {
                Dao<SiteSettings, Long> dao = getDao(SiteSettings.class);
                dao.executeRaw("ALTER TABLE `sitesettings` ADD COLUMN internalUrl VARCHAR;");
            } catch (SQLException e) {
                Log.e(TAG, "onUpgrade < 2, oldVersion: " + oldVersion + " newVersion: " + newVersion, e);
            }
        }
    }

    /**
     * Close the database connections and clear any cached DAOs.
     */
    @Override
    public void close() {
        super.close();
    }

    public DBSiteSettings getDBSiteSettings() throws SQLException {
        return new DBSiteSettings(this.<Dao<SiteSettings, Long>, SiteSettings>getDao(SiteSettings.class));
    }

    public DBSiteCall getDBSiteCall() throws SQLException {
        return new DBSiteCall(this.<Dao<SiteCall, Long>, SiteCall>getDao(SiteCall.class));
    }


    /**
     * Must be call by each entities that has call #getHelper before when no more using it.
     */
    public void release() {
        OpenHelperManager.releaseHelper();
    }
} public class DBSiteCall {

    private Dao<SiteCall, Long> dao;

    public DBSiteCall(Dao<SiteCall, Long> dao) {
        this.dao = dao;
    }

    public int create(SiteCall siteCall) throws SQLException {
        return dao.create(siteCall);
    }

    public int removeCallsBefore(Date date) throws SQLException {
        return dao.delete((PreparedDelete<SiteCall>) dao.deleteBuilder().where().lt("date", date).prepare());
    }
} public class DBSiteSettings {

    private static final String F_HOST = "host";

    private Dao<SiteSettings, Long> dao;

    public DBSiteSettings(Dao<SiteSettings, Long> dao) {
        this.dao = dao;
    }

    public SiteSettings findForHost(String host) throws SQLException {
        return dao.queryBuilder().where().eq(DBSiteSettings.F_HOST, host).queryForFirst();
    }

    public int create(SiteSettings siteSettings) throws SQLException {
        return dao.create(siteSettings);
    }

    public int update(SiteSettings siteSettings) throws SQLException {
        return dao.update(siteSettings);
    }

    public int delete(SiteSettings siteSettings) throws SQLException {
        return dao.delete(siteSettings);
    }

    public List<SiteSettings> queryForAll() throws SQLException {
        return dao.queryForAll();
    }

    public long countOf() throws SQLException {
        return dao.countOf();
    }

} public class BroadcastUtil {

    public static final String EXTRA_SITE = "org.site_monitor.service.extra.SITE";
    public static final String EXTRA_CALL = "org.site_monitor.service.extra.CALL";
    public static final String EXTRA_ALARM = "org.site_monitor.service.extra.ALARM";
    public static final String EXTRA_FAVICON = "org.site_monitor.service.extra.FAVICON";

    /**
     * Broadcasts siteSettings for given action as EXTRA_SITE
     *
     * @param context
     * @param action
     * @param siteSettings
     */
    public static void broadcast(Context context, String action, SiteSettings siteSettings) {
        Intent localIntent = new Intent(action).putExtra(EXTRA_SITE, siteSettings);
        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
    }

    /**
     * Broadcasts siteSettings for given action as EXTRA_SITE
     *
     * @param context
     * @param action
     * @param extraName
     * @param bitmap
     */
    public static void broadcast(Context context, String action, SiteSettings siteSettings, String extraName, Bitmap bitmap) {
        Intent localIntent = new Intent(action).putExtra(EXTRA_SITE, siteSettings).putExtra(extraName, bitmap);
        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
    }

    /**
     * Broadcasts long for given action as EXTRA_SITE
     *
     * @param context
     * @param action
     * @param extraName
     * @param value
     */
    public static void broadcast(Context context, String action, String extraName, long value) {
        Intent localIntent = new Intent(action).putExtra(extraName, value);
        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
    }

    /**
     * Broadcasts siteSettings for given action as EXTRA_SITE
     *
     * @param context
     * @param action
     * @param siteSettings
     */
    public static void broadcast(Context context, String action, SiteSettings siteSettings, SiteCall siteCall) {
        Intent localIntent = new Intent(action).putExtra(EXTRA_SITE, siteSettings).putExtra(EXTRA_CALL, siteCall);
        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
    }

} public class NetworkUtil {

    private static final String TAG = NetworkUtil.class.getSimpleName();

    private static final String RECVFROM_FAILED_ECONNRESET = "recvfrom failed: ECONNRESET";
    private static final String BOT_AGENT = "bot-site-monitor";
    private static final String USER_AGENT = "User-Agent";
    private static final String CLOSE = "close";
    private static final String CONNECTION = "Connection";
    private static final String HTTP = "http";
    private static final String ROOT_PROTOCOL = "://";
    private static final int TIMEOUT_10 = (int) (10 * TimeUtil.SEC_2_MILLISEC);
    private static final String METHOD_HEAD = "HEAD";
    private static final String FAVICON_SERVICE_URL = "https://www.google.com/s2/favicons?domain=";

    /**
     * Retrieves favicon for given url
     */
    public static Bitmap loadFaviconFor(String url) {
        InputStream is = null;
        try {
            is = (InputStream) new URL(FAVICON_SERVICE_URL + url).getContent();
            if (BuildConfig.DEBUG) {
                Log.v(TAG, "loadFaviconFor " + url + " succeed");
            }
            return BitmapFactory.decodeStream(is);
        } catch (IOException e) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "loadFaviconFor " + url + " fails: " + e, e);
            }
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        Log.w(TAG, "loadFaviconFor " + url + " fails: " + e, e);
                    }
                    return null;
                }
            }
        }
    }

    /**
     * Builds and performs http request for given siteSettings
     * @return SiteCall result
     */
    public SiteCall buildHeadHttpConnectionThenDoCall(Context context, SiteSettings siteSettings) {
        SiteCall siteCall;
        if (ConnectivityUtil.isConnected(context)) {
            HttpURLConnection urlConnection = null;
            Timer timer = new Timer();
            try {
                urlConnection = buildHeadHttpConnection(siteSettings.getHost(), siteSettings.isForcedCertificate());
                siteCall = doCall(urlConnection, timer);
            } catch (IOException e) {
                disconnect(urlConnection);
                if (e.getLocalizedMessage() != null && e.getLocalizedMessage().startsWith(RECVFROM_FAILED_ECONNRESET)) {
                    Log.d(TAG, "RECVFROM_FAILED_ECONNRESET - retry once: " + siteSettings);
                    try {
                        timer = new Timer();
                        urlConnection = buildHeadHttpConnection(siteSettings.getHost(), siteSettings.isForcedCertificate());
                        siteCall = doCall(urlConnection, timer);
                    } catch (IOException eConnReset) {
                        disconnect(urlConnection);
                        siteCall = new SiteCall(timer.getReferenceDate(), NetworkCallResult.FAIL, timer.getElapsedTime(), e);
                    }
                } else {
                    try {
                        if (siteSettings.getInternalUrl() != null) {
                            Log.d(TAG, "retry with internal URL: " + siteSettings);
                            timer = new Timer();
                            urlConnection = buildHeadHttpConnection(siteSettings.getInternalUrl(), siteSettings.isForcedCertificate());
                            siteCall = doCall(urlConnection, timer);
                        } else {
                            siteCall = new SiteCall(timer.getReferenceDate(), NetworkCallResult.FAIL, timer.getElapsedTime(), e);
                        }
                    } catch (IOException eInternalIp) {
                        disconnect(urlConnection);
                        siteCall = new SiteCall(timer.getReferenceDate(), NetworkCallResult.FAIL, timer.getElapsedTime(), e);
                    }
                }
            } finally {
                disconnect(urlConnection);
            }
        } else {
            siteCall = new SiteCall(new Date(), NetworkCallResult.NO_CONNECTIVITY);
        }
        return siteCall;
    }

    public void disconnect(HttpURLConnection urlConnection) {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }

    /**
     * Performs call represented by urlConnection
     *
     * @param urlConnection
     * @param timer
     * @return call result
     * @throws IOException
     */
    private SiteCall doCall(HttpURLConnection urlConnection, Timer timer) throws IOException {
        urlConnection.connect();
        int responseCode = urlConnection.getResponseCode();
        if (responseCode != HttpURLConnection.HTTP_OK) {
            return new SiteCall(timer.getReferenceDate(), NetworkCallResult.FAIL, timer.getElapsedTime(), responseCode);
        }
        return new SiteCall(timer.getReferenceDate(), NetworkCallResult.SUCCESS, timer.getElapsedTime(), responseCode);
    }

    /**
     * Builds HttpURLConnection (requestMethod = head, property = connection/close, no cache, follow redirects, connection/read timeout 10sec
     *
     * @param host
     * @param forceTrust
     * @return HttpURLConnection
     * @throws IOException
     */
    private HttpURLConnection buildHeadHttpConnection(String host, boolean forceTrust) throws IOException {
        URL url;
        if (host.toLowerCase().startsWith(HTTP)) {
            url = new URL(host);
        } else {
            url = new URL(HTTP + ROOT_PROTOCOL + host);
        }
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod(METHOD_HEAD);
        urlConnection.setRequestProperty(CONNECTION, CLOSE);
        urlConnection.setRequestProperty(USER_AGENT, BOT_AGENT);
        urlConnection.setUseCaches(false);
        urlConnection.setDefaultUseCaches(false);
        urlConnection.setDoInput(false);
        urlConnection.setDoOutput(false);
        urlConnection.setInstanceFollowRedirects(true);
        urlConnection.setConnectTimeout(TIMEOUT_10);
        urlConnection.setReadTimeout(TIMEOUT_10);
        if (forceTrust) {
            ((HttpsURLConnection) urlConnection).setSSLSocketFactory(CertificateTrustAllManager.sslSocketFactory());
        }
        return urlConnection;
    }

} public class NotificationUtil {

    private static final String TAG = NotificationUtil.class.getSimpleName();
    private static final String CHANNEL_ID_ALERTS = "ALERTS";
    public static int ID_NOT_REACHABLE = 1;

    public static void createNotificationChannel(Context context) {
        // Create the NotificationChannel, but only on API 26+ because the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID_ALERTS, context.getString(R.string.channel_title), NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription(context.getString(R.string.channel_description));
            // Register the channel with the system; you can't change the importance or other notification behaviors after this
            NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }

    /**
     * Pre builds notification.
     */
    public static NotificationCompat.Builder build(Context context, String title, String text, PendingIntent pendingIntent) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID_ALERTS);
        notificationBuilder.setSmallIcon(R.drawable.ic_app);
        if (!BuildConfig.DEBUG) {
            notificationBuilder.setColor(context.getResources().getColor(R.color.primary));
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            notificationBuilder.setCategory(Notification.CATEGORY_SERVICE);
        }
        notificationBuilder.setContentTitle(title).setContentText(text);
        notificationBuilder.setAutoCancel(true);

        String ringtonePath = preferences.getString(PrefSettingsActivity.NOTIFICATIONS_RINGTONE, null);
        Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        if (ringtonePath != null && !ringtonePath.isEmpty()) {
            ringtoneUri = Uri.parse(ringtonePath);
        }

        notificationBuilder.setSound(ringtoneUri);
        String colorString = preferences.getString(PrefSettingsActivity.NOTIFICATION_LIGHT_COLOR, Color.WHITE + "");
        int color = Integer.valueOf(colorString);
        notificationBuilder.setLights(color, TimeUtil._1_SEC_INT * 2, TimeUtil._1_SEC_INT * 10);
        if (preferences.getBoolean(PrefSettingsActivity.NOTIFICATIONS_VIBRATE, false)) {
            long[] pattern = {NotificationCompat.DEFAULT_VIBRATE * TimeUtil.SEC_2_MILLISEC};
            notificationBuilder.setVibrate(pattern);
        }
        notificationBuilder.setContentIntent(pendingIntent);
        return notificationBuilder;
    }

    /**
     * @return false if not sent {@see #shouldNotify}
     */
    public static boolean send(Context context, int typeId, Notification notification) {
        //if (!shouldNotify(context)) {
        //     return false;
        // }
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(typeId, notification);
        return true;
    }

    /**
     * @return false if app foreground or notification disabled
     */
    public static boolean shouldNotify(Context context) {
        if (App.isForeground()) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doesn't send notification (app foreground)");
            }
            return false;
        }
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        boolean notificationEnable = preferences.getBoolean(PrefSettingsActivity.NOTIFICATION_ENABLE, false);
        if (!notificationEnable) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doesn't send notification (disabled)");
            }
            return false;
        }
        return true;
    }
} public class WidgetManager {

    private static final String TAG = "WidgetManager";

    /**
     * Sends broadcast to existing widgets for refresh.
     *
     * @param context
     */
    public static void refresh(Context context) {
        sendRefreshTo(context, LineWidget.class);
        sendRefreshTo(context, SquareWidget.class);
    }

    private static void sendRefreshTo(Context context, Class clazz) {
        Intent intent = new Intent(context, clazz);
        intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, clazz));
        if (ids.length > 0) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "refresh: " + clazz.getSimpleName());
            }
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
            context.sendBroadcast(intent);
        }
    }

}
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.Pair;
import org.site_monitor.BuildConfig;
import org.site_monitor.R;
import org.site_monitor.activity.MainActivity;
import org.site_monitor.activity.PrefSettingsActivity;
import org.site_monitor.model.bo.NetworkCallResult;
import org.site_monitor.model.bo.SiteCall;
import org.site_monitor.model.bo.SiteSettings;
import org.site_monitor.model.db.DBHelper;
import org.site_monitor.model.db.DBSiteCall;
import org.site_monitor.model.db.DBSiteSettings;
import org.site_monitor.util.BroadcastUtil;
import org.site_monitor.util.NetworkUtil;
import org.site_monitor.util.NotificationUtil;
import org.site_monitor.widget.WidgetManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/*
 * Copyright (c) 2016 Martin Norbert
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.site_monitor.service;




/**
 * An {@link IntentService} subclass for handling asynchronous task requests in
 * a service on a separate handler thread.
 */
public class NetworkService extends JobIntentService {

    public static final String ACTION_SITE_START_REFRESH = "org.site_monitor.service.action.SITE_START_REFRESH";
    public static final String ACTION_SITE_END_REFRESH = "org.site_monitor.service.action.SITE_END_REFRESH";
    public static final String REQUEST_REFRESH_SITES = "refreshSites";
    public static final String COMA = ",";
    private static final String TAG = NetworkService.class.getSimpleName();
    private NetworkUtil networkUtil = new NetworkUtil();

    /**
     * @param context
     * @return intent to call start service
     */
    public static Intent intentToCheckSites(Context context) {
        return new Intent(context, NetworkService.class).setAction(REQUEST_REFRESH_SITES);
    }

    public static void enqueueCheckSitesWork(@NonNull Context context) {
        NetworkService.enqueueWork(context, NetworkService.class, JobEnum.CHECK_SITES.ordinal(), intentToCheckSites(context));
    }

    /**
     * Called serially for each work dispatched to and processed by the service.  This
     * method is called on a background thread, so you can do long blocking operations
     * here.  Upon returning, that work will be considered complete and either the next
     * pending work dispatched here or the overall service destroyed now that it has
     * nothing else to do.
     * <p>
     * <p>Be aware that when running as a job, you are limited by the maximum job execution
     * time and any single or total sequential items of work that exceeds that limit will
     * cause the service to be stopped while in progress and later restarted with the
     * last unfinished work.  (There is currently no limit on execution duration when
     * running as a pre-O plain Service.)</p>
     *
     * @param intent The intent describing the work to now be processed.
     */
    @Override
    public void onHandleWork(@NonNull Intent intent) {
        if (intent.getAction() == null) {
            if (BuildConfig.DEBUG) {
                Log.e(TAG, "onHandleIntent: no action");
            }
            return;
        }
        DBHelper dbHelper = DBHelper.getHelper(this);