com.googlecode.mindbell.MindBellPreferences.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.mindbell.MindBellPreferences.java

Source

/*
 * MindBell - Aims to give you a support for staying mindful in a busy life -
 *            for remembering what really counts
 *
 *     Copyright (C) 2010-2014 Marc Schroeder
 *     Copyright (C) 2014-2018 Uwe Damken
 *
 * 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 com.googlecode.mindbell;

import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import android.preference.RingtonePreference;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;

import com.googlecode.mindbell.accessors.ContextAccessor;
import com.googlecode.mindbell.accessors.PrefsAccessor;
import com.googlecode.mindbell.preference.ListPreferenceWithSummaryFix;
import com.googlecode.mindbell.preference.MediaVolumePreference;
import com.googlecode.mindbell.preference.MinutesIntervalPickerPreference;
import com.googlecode.mindbell.preference.MultiSelectListPreferenceWithSummary;
import com.googlecode.mindbell.util.TimeOfDay;
import com.googlecode.mindbell.util.Utils;

import java.util.Set;

public class MindBellPreferences extends PreferenceActivity
        implements ActivityCompat.OnRequestPermissionsResultCallback {

    public static final String TAG = "MindBell";

    private static final int REQUEST_CODE_STATUS = 0;

    private static final int REQUEST_CODE_MUTE_OFF_HOOK = 1;

    private static final int REQUEST_CODE_RINGTONE = 2;

    // Weird, but ringtone cannot be retrieved from RingtonePreference, only from SharedPreference or in ChangeListener
    private String preferenceRingtoneValue;

    @SuppressWarnings("deprecation") // deprecation is because MindBell is not fragment-based
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // check settings, delete any settings that are not valid
        final PrefsAccessor prefs = ContextAccessor.getInstance(this).getPrefs();

        // Load the preferences from an XML resource
        addPreferencesFromResource(R.xml.preferences_1);
        addPreferencesFromResource(R.xml.preferences_2); // notifications depend on SDK
        addPreferencesFromResource(R.xml.preferences_3);

        final CheckBoxPreference preferenceUseAudioStreamVolumeSetting = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyUseAudioStreamVolumeSetting));
        final CheckBoxPreference preferenceStatus = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyStatus));
        final CheckBoxPreference preferenceShow = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyShow));
        final CheckBoxPreference preferenceSound = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keySound));
        final Preference preferenceReminderSoundLength = (Preference) getPreferenceScreen()
                .findPreference(getText(R.string.keyReminderSoundLength));
        final ListPreferenceWithSummaryFix preferenceReminderBell = (ListPreferenceWithSummaryFix) getPreferenceScreen()
                .findPreference(getText(R.string.keyReminderBell));
        final MediaVolumePreference preferenceVolume = (MediaVolumePreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyVolume));
        final RingtonePreference preferenceRingtone = (RingtonePreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyRingtone));
        final CheckBoxPreference preferenceVibrate = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyVibrate));
        final ListPreferenceWithSummaryFix preferencePattern = (ListPreferenceWithSummaryFix) getPreferenceScreen()
                .findPreference(getText(R.string.keyPattern));
        final CheckBoxPreference preferenceMuteOffHook = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyMuteOffHook));
        final MinutesIntervalPickerPreference preferenceFrequency = (MinutesIntervalPickerPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyFrequency));
        final CheckBoxPreference preferenceRandomize = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyRandomize));
        final ListPreferenceWithSummaryFix preferenceNormalize = (ListPreferenceWithSummaryFix) getPreferenceScreen()
                .findPreference(getText(R.string.keyNormalize));
        final MultiSelectListPreferenceWithSummary preferenceActiveOnDaysOfWeek = (MultiSelectListPreferenceWithSummary) getPreferenceScreen()
                .findPreference(getText(R.string.keyActiveOnDaysOfWeek));
        final MediaVolumePreference preferenceMeditationVolume = (MediaVolumePreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyMeditationVolume));
        final CheckBoxPreference preferenceUseWorkaroundBell = (CheckBoxPreference) getPreferenceScreen()
                .findPreference(getText(R.string.keyUseWorkaroundBell));
        final Preference preferenceFAQ = (Preference) getPreferenceScreen()
                .findPreference(getText(R.string.keyFAQ));
        final Preference preferenceBatterySettings = (Preference) getPreferenceScreen()
                .findPreference(getText(R.string.keyBatterySettings));
        final Preference preferenceSendMail = (Preference) getPreferenceScreen()
                .findPreference(getText(R.string.keySendMail));

        preferenceUseAudioStreamVolumeSetting.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                boolean isChecked = (Boolean) newValue;
                if (!isChecked && prefs.mustUseAudioStreamVolumeSetting()) {
                    Toast.makeText(MindBellPreferences.this, R.string.mustUseAudioStreamSetting, Toast.LENGTH_SHORT)
                            .show();
                    return false;
                } else {
                    preferenceVolume.setEnabled(preferenceSound.isChecked() && !isChecked);
                    preferenceMeditationVolume.setEnabled(!isChecked);
                    return true;
                }
            }

        });

        preferenceStatus.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                return mediateMuteOffHookAndStatus(preferenceMuteOffHook, newValue, REQUEST_CODE_STATUS);
            }

        });

        preferenceShow.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                return mediateShowAndSoundAndVibrate(preferenceSound, preferenceVibrate, newValue);
            }

        });

        preferenceSound.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (mediateShowAndSoundAndVibrate(preferenceShow, preferenceVibrate, newValue)
                        && mediateSoundDurationRelatedSettings(preferenceFrequency, preferenceUseWorkaroundBell,
                                preferenceReminderBell, preferenceRingtoneValue, newValue)) {
                    boolean isChecked = (Boolean) newValue;
                    preferenceReminderBell.setEnabled(isChecked);
                    preferenceRingtone.setEnabled(
                            isChecked && !PrefsAccessor.isUseStandardBell(preferenceReminderBell.getValue()));
                    preferenceVolume.setEnabled(!preferenceUseAudioStreamVolumeSetting.isChecked() && isChecked);
                    return true;
                } else {
                    return false;
                }
            }

        });

        preferenceReminderSoundLength.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

            @Override
            public boolean onPreferenceClick(Preference preference) {
                onPreferenceReminderSoundLength(preferenceUseWorkaroundBell.isChecked(),
                        preferenceReminderBell.getValue(), preferenceRingtoneValue);
                return true;
            }

        });

        preferenceReminderBell.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                String reminderBell = (String) newValue;
                boolean isChecked = PrefsAccessor.isUseStandardBell(reminderBell);
                if (PrefsAccessor.isUseStandardBell(reminderBell)
                        || ContextCompat.checkSelfPermission(MindBellPreferences.this,
                                Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
                    if (mediateSoundDurationRelatedSettings(preferenceFrequency, preferenceUseWorkaroundBell,
                            reminderBell, preferenceRingtoneValue, preferenceSound)) {
                        // Allow setting this option to "off" if permission is granted
                        preferenceRingtone.setEnabled(preferenceSound.isChecked() && !isChecked);
                        // Weird, but ringtone cannot be retrieved from RingtonePreference, only from SharedPreference
                        setPreferenceVolumeSoundUri(preferenceVolume, reminderBell,
                                preferenceUseWorkaroundBell.isChecked(), preferenceRingtoneValue);
                        return true;
                    } else {
                        return false;
                    }
                } else {
                    // Ask for permission if this option shall be set to "off" but permission is missing
                    ActivityCompat.requestPermissions(MindBellPreferences.this,
                            new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, REQUEST_CODE_RINGTONE);
                    // As the permission request is asynchronous we have to deny setting this option (to "off")
                    return false;
                }
            }

        });

        preferenceRingtone.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                String newRingtoneValue = (String) newValue;
                if (validatePreferenceRingtone(newRingtoneValue)
                        && mediateSoundDurationRelatedSettings(preferenceFrequency, preferenceUseWorkaroundBell,
                                preferenceReminderBell, newRingtoneValue, preferenceSound)) {
                    setPreferenceRingtoneSummary(preferenceRingtone, newRingtoneValue);
                    setPreferenceVolumeSoundUri(preferenceVolume, preferenceReminderBell.getValue(),
                            preferenceUseWorkaroundBell.isChecked(), newRingtoneValue);
                    preferenceRingtoneValue = newRingtoneValue;
                    return true;
                } else {
                    return false;
                }
            }

        });

        preferenceVibrate.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                return mediateShowAndSoundAndVibrate(preferenceShow, preferenceSound, newValue);
            }

        });

        preferencePattern.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                Vibrator vibrator = (Vibrator) MindBellPreferences.this.getSystemService(Context.VIBRATOR_SERVICE);
                vibrator.vibrate(PrefsAccessor.getVibrationPattern((String) newValue), -1);
                return true;
            }

        });

        preferenceMuteOffHook.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                return mediateMuteOffHookAndStatus(preferenceStatus, newValue, REQUEST_CODE_MUTE_OFF_HOOK);
            }

        });

        preferenceRandomize.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if ((Boolean) newValue) {
                    // if interval deviation is selected, normalize is disabled on screen but it must be disabled in preferences,
                    // too. Otherwise the following scenario could happen: set interval 1 h, de-select randomize, set normalize to
                    // hh:00, select randomize, set interval 2 h, de-select randomize again ... hh:00 would be left in normalize
                    // erroneously.
                    preferenceNormalize.setValue(PrefsAccessor.NORMALIZE_NONE);
                }
                return true;
            }

        });

        preferenceFrequency.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (!mediateSoundDurationRelatedSettings(newValue, preferenceUseWorkaroundBell,
                        preferenceReminderBell, preferenceRingtoneValue, preferenceSound)) {
                    return false;
                } else if (preferenceRandomize.isChecked()) {
                    // if interval varies randomly, ringing on the minute is disabled and set to "no" anyway
                    return true;
                } else if (isFrequencyDividesAnHour(new TimeOfDay((String) newValue))) {
                    // if frequency is factor of an hour, ringing on the minute may be requested
                    preferenceNormalize.setEnabled(true);
                } else {
                    // if frequency is NOT factor of an hour, ringing on the minute may NOT be set
                    if (preferenceNormalize.isEnabled() && isNormalize(preferenceNormalize.getValue())) {
                        Toast.makeText(MindBellPreferences.this, R.string.frequencyDoesNotFitIntoAnHour,
                                Toast.LENGTH_SHORT).show();
                        return false;
                    } else {
                        preferenceNormalize.setEnabled(false);
                    }
                }
                return true;
            }

        });

        preferenceNormalize.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (!isNormalize((String) newValue)) {
                    // if normalize - ringing on the minute - is not wanted, it's fine, no more to check here
                    return true;
                } else if (isFrequencyDividesAnHour(preferenceFrequency.getTime())) {
                    // if frequency is factor of an hour, requesting ringing on the minute is allowed
                    return true;
                } else {
                    // if frequency is NOT factor of an hour, ringing on the minute may NOT be set
                    Toast.makeText(MindBellPreferences.this, R.string.frequencyDoesNotFitIntoAnHour,
                            Toast.LENGTH_SHORT).show();
                    return false;
                }
            }

        });

        preferenceActiveOnDaysOfWeek.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValues) {
                if (((Set<?>) newValues).isEmpty()) {
                    Toast.makeText(MindBellPreferences.this, R.string.atLeastOneActiveDayNeeded, Toast.LENGTH_SHORT)
                            .show();
                    return false;
                }
                return true;
            }

        });

        preferenceFAQ.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

            @Override
            public boolean onPreferenceClick(Preference preference) {
                Uri faqUri = Uri.parse(getText(R.string.faq_url).toString());
                Intent browserIntent = new Intent(Intent.ACTION_VIEW, faqUri);
                startActivity(browserIntent);
                return true;
            }

        });

        preferenceBatterySettings.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

            @Override
            public boolean onPreferenceClick(Preference preference) {
                onPreferenceClickBatterySettings();
                return true;
            }

        });

        preferenceUseWorkaroundBell.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (mediateSoundDurationRelatedSettings(preferenceFrequency, newValue, preferenceReminderBell,
                        preferenceRingtoneValue, preferenceSound)) {
                    boolean isChecked = (Boolean) newValue;
                    setPreferenceVolumeSoundUri(preferenceVolume, preferenceReminderBell.getValue(), isChecked,
                            preferenceRingtoneValue);
                    return true;
                } else {
                    return false;
                }
            }

        });

        preferenceSendMail.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

            @Override
            public boolean onPreferenceClick(Preference preference) {
                new AlertDialog.Builder(MindBellPreferences.this) //
                        .setTitle(R.string.prefsSendMail) //
                        .setMessage(R.string.mailInfo1) //
                        .setIcon(R.mipmap.ic_launcher) //
                        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                onClickReallySendInfo();
                            }
                        }) //
                        .setNegativeButton(android.R.string.cancel, null) //
                        .show();
                return true;
            }

        });

        // As no PreferenceChangeListener is called without change *BY USER*, some settings have to be made explicitly
        preferenceVolume
                .setEnabled(preferenceSound.isChecked() && !preferenceUseAudioStreamVolumeSetting.isChecked());
        preferenceMeditationVolume.setEnabled(!preferenceUseAudioStreamVolumeSetting.isChecked());
        preferenceReminderBell.setEnabled(preferenceSound.isChecked());
        preferenceRingtone.setEnabled(
                preferenceSound.isChecked() && !PrefsAccessor.isUseStandardBell(preferenceReminderBell.getValue()));
        preferenceRingtoneValue = prefs.getRingtone(); // cannot be retrieved from preference
        setPreferenceRingtoneSummary(preferenceRingtone, preferenceRingtoneValue);
        setPreferenceVolumeSoundUri(preferenceVolume, preferenceReminderBell.getValue(),
                preferenceUseWorkaroundBell.isChecked(), preferenceRingtoneValue);

    }

    /**
     * Ensures that the CheckBoxPreferences checkBoxPreferenceMuteOffHook and checkBoxPreferenceStatus cannot be both "on" without
     * having READ_PHONE_STATE permission by returning false when this rule is violated.
     */
    private boolean mediateMuteOffHookAndStatus(CheckBoxPreference other, Object newValue, int requestCode) {
        if (!other.isChecked() || !((Boolean) newValue) || ContextCompat.checkSelfPermission(this,
                Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
            // Allow setting this option to "on" if other option is "off" or permission is granted
            return true;
        } else {
            // Ask for permission if other option is "on" and this option shall be set to "on" but permission is missing
            ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_PHONE_STATE },
                    requestCode);
            // As the permission request is asynchronous we have to deny setting this option (to "on")
            return false;
        }
    }

    /**
     * Ensures that the CheckBoxPreferences checkBoxPreferenceShow, checkBoxPreferenceSound and checkBoxPreferenceVibrate cannot be
     * all "off", at least one must be checked.
     */
    private boolean mediateShowAndSoundAndVibrate(CheckBoxPreference firstOther, CheckBoxPreference secondOther,
            Object newValue) {
        if (!firstOther.isChecked() && !secondOther.isChecked() && !((Boolean) newValue)) {
            Toast.makeText(this, R.string.atLeastOneRingingActionNeeded, Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval.
     */
    private boolean mediateSoundDurationRelatedSettings(MinutesIntervalPickerPreference preferenceFrequency,
            CheckBoxPreference preferenceUseWorkaroundBell, ListPreferenceWithSummaryFix preferenceReminderBell,
            String preferenceRingtoneValue, Object newSoundValue) {
        return mediateSoundDurationRelatedSettings(preferenceFrequency.getTime(),
                preferenceUseWorkaroundBell.isChecked(), preferenceReminderBell.getValue(), preferenceRingtoneValue,
                (Boolean) newSoundValue);
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval.
     */
    private boolean mediateSoundDurationRelatedSettings(MinutesIntervalPickerPreference preferenceFrequency,
            CheckBoxPreference preferenceUseWorkaroundBell, String newReminderBellValue,
            String preferenceRingtoneValue, CheckBoxPreference preferenceSound) {
        return mediateSoundDurationRelatedSettings(preferenceFrequency.getTime(),
                preferenceUseWorkaroundBell.isChecked(), newReminderBellValue, preferenceRingtoneValue,
                preferenceSound.isChecked());
    }

    /**
     * Set sound uri in preferenceVolume depending on preferenceUseStandardBell and preferenceRingtone, so real sound is used for
     * volume setting.
     *
     * @param preferenceVolume
     * @param reminderBell
     * @param isUseWorkaroundBell
     * @param ringtone
     */
    private void setPreferenceVolumeSoundUri(MediaVolumePreference preferenceVolume, String reminderBell,
            boolean isUseWorkaroundBell, String ringtone) {
        preferenceVolume.setSoundUri(getReminderSoundUri(reminderBell, isUseWorkaroundBell, ringtone));
    }

    /**
     * Returns the chosen sound depending on settings for reminderBell, ringtone and useWorkaroundBell.
     */
    private Uri getReminderSoundUri(String reminderBell, boolean isUseWorkaroundBell, String ringtone) {
        // This implementation is almost the same as PrefsAccessor#getReminderSoundUri()
        Uri soundUri = PrefsAccessor.getBellSoundUri(this, reminderBell, isUseWorkaroundBell);
        if (soundUri == null) { // use system notification ringtone if reminder bell sound is not set
            if (ringtone.isEmpty()) {
                soundUri = PrefsAccessor.getDefaultReminderBellSoundUri(this, isUseWorkaroundBell);
            } else {
                soundUri = Uri.parse(ringtone);
            }
        }
        return soundUri;
    }

    /**
     * Returns true if the ringtone specified by newRingtoneValue is unset, empty or accessible with valid length.
     */
    private boolean validatePreferenceRingtone(String newRingtoneValue) {
        if (newRingtoneValue != null && !newRingtoneValue.isEmpty()) {
            Long ringtoneDuration = Utils.getSoundDuration(this, Uri.parse(newRingtoneValue));
            if (ringtoneDuration == null) {
                Toast.makeText(this, R.string.ringtoneNotAccessible, Toast.LENGTH_SHORT).show();
                return false;
            }
        }
        return true;
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval.
     */
    private boolean mediateSoundDurationRelatedSettings(MinutesIntervalPickerPreference preferenceFrequency,
            CheckBoxPreference preferenceUseWorkaroundBell, ListPreferenceWithSummaryFix preferenceReminderBell,
            String newRingtoneValue, CheckBoxPreference preferenceSound) {
        return mediateSoundDurationRelatedSettings(preferenceFrequency.getTime(),
                preferenceUseWorkaroundBell.isChecked(), preferenceReminderBell.getValue(), newRingtoneValue,
                preferenceSound.isChecked());
    }

    /**
     * Sets the ringtone title into the summary of the ringtone preference.
     *
     * @param preferenceRingtone
     * @param uriString
     */
    private void setPreferenceRingtoneSummary(RingtonePreference preferenceRingtone, String uriString) {
        CharSequence summary;
        if (uriString == null || uriString.isEmpty()) {
            summary = getText(R.string.summaryRingtoneNotSet);
        } else {
            Uri ringtoneUri = Uri.parse(uriString);
            Ringtone ringtone = RingtoneManager.getRingtone(this, ringtoneUri);
            summary = ringtone.getTitle(this);
        }
        preferenceRingtone.setSummary(summary);
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval.
     */
    private boolean mediateSoundDurationRelatedSettings(Object newFrequencyValue,
            CheckBoxPreference preferenceUseWorkaroundBell, ListPreferenceWithSummaryFix preferenceReminderBell,
            String preferenceRingtoneValue, CheckBoxPreference preferenceSound) {
        return mediateSoundDurationRelatedSettings(new TimeOfDay((String) newFrequencyValue),
                preferenceUseWorkaroundBell.isChecked(), preferenceReminderBell.getValue(), preferenceRingtoneValue,
                preferenceSound.isChecked());
    }

    /**
     * Returns true, if frequency divides an hour in whole numbers, e.g. true for 20 minutes, or if frequency is a multiple of an
     * hour (a frequency of 0 is prohibited by MinutesIntervalPickerPreference).
     */
    private boolean isFrequencyDividesAnHour(TimeOfDay frequencyValue) {
        int interval = frequencyValue.getInterval();
        return interval % 60 == 0 || 60 % interval == 0;
    }

    /**
     * Returns true, if normalize - ringing on the minute - is requested
     */
    private boolean isNormalize(String normalizeValue) {
        return !PrefsAccessor.NORMALIZE_NONE.equals(normalizeValue);
    }

    private void onPreferenceReminderSoundLength(boolean useWorkaroundBell, String reminderBell,
            String ringtoneValue) {
        Uri soundUri = getReminderSoundUri(reminderBell, useWorkaroundBell, ringtoneValue);
        Long soundDuration = Utils.getSoundDuration(this, soundUri);
        String msg = null;
        if (soundDuration == null) {
            msg = getText(R.string.ringtoneNotAccessible).toString();
        } else {
            soundDuration /= 1000L; // in seconds
            msg = String.format(getText(R.string.reminderSoundLength).toString(), soundDuration,
                    getText(R.string.summaryReminderSoundLength));
        }
        new AlertDialog.Builder(this) //
                .setTitle(R.string.prefsReminderSoundLength) //
                .setMessage(msg) //
                .setPositiveButton(android.R.string.ok, null) //
                .show();
    }

    private void onPreferenceClickBatterySettings() {
        if (Build.VERSION.SDK_INT >= 23) {
            if (Utils.isAppWhitelisted(this)) {
                new AlertDialog.Builder(this) //
                        .setTitle(R.string.prefsBatterySettings) //
                        .setMessage(R.string.summaryBatterySettingsWhitelisted) //
                        .setNegativeButton(android.R.string.cancel, null) //
                        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                // Take user to the battery settings so he can check the settings
                                startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
                            }
                        }) //
                        .show();
            } else {
                new AlertDialog.Builder(this) //
                        .setTitle(R.string.prefsBatterySettings) //
                        .setMessage(R.string.summaryBatterySettingsNotWhitelisted) //
                        .setNegativeButton(android.R.string.cancel, null) //
                        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                // Take user to the battery settings instead of adding a new permission that might result in
                                // suspending MindBell from Google Play Store. See the comments for this answer:
                                // https://stackoverflow.com/a/33114136/2532583
                                startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
                                Context context = MindBellPreferences.this;
                                Toast.makeText(context, context.getText(R.string.battery_settings_guidance1),
                                        Toast.LENGTH_LONG).show();
                                Toast.makeText(context, context.getText(R.string.battery_settings_guidance2),
                                        Toast.LENGTH_LONG).show();
                                Toast.makeText(context, context.getText(R.string.battery_settings_guidance3),
                                        Toast.LENGTH_LONG).show();
                            }
                        }) //
                        .show();
            }
        } else {
            new AlertDialog.Builder(this) //
                    .setTitle(R.string.prefsBatterySettings) //
                    .setMessage(R.string.summaryBatterySettingsUnknown) //
                    .setPositiveButton(android.R.string.ok, null) //
                    .show();
        }
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval.
     */
    private boolean mediateSoundDurationRelatedSettings(MinutesIntervalPickerPreference preferenceFrequency,
            Object newUseWorkaroundBellValue, ListPreferenceWithSummaryFix preferenceReminderBell,
            String preferenceRingtoneValue, CheckBoxPreference preferenceSound) {
        return mediateSoundDurationRelatedSettings(preferenceFrequency.getTime(),
                (Boolean) newUseWorkaroundBellValue, preferenceReminderBell.getValue(), preferenceRingtoneValue,
                preferenceSound.isChecked());
    }

    /**
     * Handles click on confirmation to send info.
     */
    private void onClickReallySendInfo() {
        ContextAccessor.getInstanceAndLogPreferences(this); // write settings to log
        MindBell.logDebug("Excluded from battery optimization (always false for SDK < 23)? -> "
                + Utils.isAppWhitelisted(this));
        Intent i = new Intent(Intent.ACTION_SENDTO,
                Uri.fromParts("mailto", getText(R.string.emailAddress).toString(), null));
        i.putExtra(Intent.EXTRA_SUBJECT, getText(R.string.emailSubject));
        i.putExtra(Intent.EXTRA_TEXT, getInfoMailText());
        try {
            startActivity(Intent.createChooser(i, getText(R.string.emailChooseApp)));
        } catch (android.content.ActivityNotFoundException ex) {
            Toast.makeText(this, getText(R.string.noEmailClients), Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Ensures that the duration of the chosen sound does not exceed a quarter of the chosen interval. Sound might occur every
     * interval +/- 50%. So minimum time for sound and silence if half of the interval. IMHO at least half of that time should be
     * silence. That's where the quarter comes from. This check ignores the fact that randomizing the interval might be switched
     * off.
     */
    private boolean mediateSoundDurationRelatedSettings(TimeOfDay frequency, boolean useWorkaroundBell,
            String reminderBell, String ringtoneValue, boolean soundValue) {
        if (!soundValue) { // everything's fine if no sound has to be played at all
            return true;
        }
        Uri soundUri = getReminderSoundUri(reminderBell, useWorkaroundBell, ringtoneValue);
        Long soundDuration = Utils.getSoundDuration(this, soundUri);
        if (soundDuration == null) {
            Toast.makeText(this, R.string.ringtoneNotAccessible, Toast.LENGTH_SHORT).show();
            return false;
        }
        soundDuration /= 1000L; // in seconds
        long maxDuration = frequency.getInterval() * 60L / 4L; // in seconds
        if (soundDuration > maxDuration) {
            String msg = String.format(getText(R.string.ringtoneDurationTooLong).toString(), soundDuration,
                    maxDuration, frequency.getInterval() * 60L);
            Log.w(TAG, msg + " (" + soundUri.toString() + ")");
            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
            return false;
        } else if (soundDuration > 10L) {
            Toast.makeText(this, getText(R.string.summaryReminderSoundLength), Toast.LENGTH_LONG).show();
        }
        return true;
    }

    /**
     * Return information to be sent by mail.
     */
    private String getInfoMailText() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n\n------------------------------\n");
        sb.append(getText(R.string.mailInfo1));
        sb.append("\n\n");
        sb.append(getText(R.string.mailInfo2));
        sb.append("\n\n");
        sb.append(Utils.getApplicationInformation(getPackageManager(), getPackageName()));
        sb.append("\n");
        sb.append(Utils.getSystemInformation());
        sb.append("\n");
        sb.append(Utils.getLimitedLogEntriesAsString());
        sb.append("\n");
        sb.append(getText(R.string.mailInfo2));
        sb.append("\n\n");
        sb.append(getText(R.string.mailInfo1));
        sb.append("\n------------------------------\n\n");
        return sb.toString();
    }

    @Override
    public void onPause() {
        super.onPause();
        ContextAccessor.getInstanceAndLogPreferences(this).updateBellScheduleForReminder(true);
    }

    @SuppressWarnings("deprecation") // deprecation is because MindBell is not fragment-based
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
        case REQUEST_CODE_STATUS:
            CheckBoxPreference checkBoxPreferenceStatus = (CheckBoxPreference) getPreferenceScreen()
                    .findPreference(getText(R.string.keyStatus));
            handleMuteHookOffAndStatusPermissionRequestResult(checkBoxPreferenceStatus, grantResults);
            break;
        case REQUEST_CODE_MUTE_OFF_HOOK:
            CheckBoxPreference checkBoxPreferenceMuteOffHook = (CheckBoxPreference) getPreferenceScreen()
                    .findPreference(getText(R.string.keyMuteOffHook));
            handleMuteHookOffAndStatusPermissionRequestResult(checkBoxPreferenceMuteOffHook, grantResults);
            break;
        case REQUEST_CODE_RINGTONE:
            handleRingtonePermissionRequestResult(grantResults);
            break;
        default:
            break;
        }
    }

    private void handleMuteHookOffAndStatusPermissionRequestResult(CheckBoxPreference one, int[] grantResults) {
        if (grantResults.length == 0) {
            // if request is cancelled, the result arrays are empty, so leave this option "off" and don't explain it
        } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // User granted the needed permission therefore this option is set to "on"
            if (one.getOnPreferenceChangeListener().onPreferenceChange(one, Boolean.TRUE)) {
                one.setChecked(true); // WARNING: This does NOT call the onPreferenceValueChangeListener
            }
        } else if (ActivityCompat.shouldShowRequestPermissionRationale(MindBellPreferences.this,
                Manifest.permission.READ_PHONE_STATE)) {
            // User denied the needed permission and can be given an explanation, so we show an explanation
            new AlertDialog.Builder(MindBellPreferences.this) //
                    .setTitle(R.string.reasonReadPhoneStateTitle) //
                    .setMessage(R.string.reasonReadPhoneStateText) //
                    .setPositiveButton(android.R.string.ok, null) //
                    .create() //
                    .show();
        } else {
            // User denied the needed permission and checked never ask again
            new AlertDialog.Builder(MindBellPreferences.this) //
                    .setTitle(R.string.neverAskAgainReadPhoneStateTitle) //
                    .setMessage(R.string.neverAskAgainReadPhoneStateText) //
                    .setPositiveButton(android.R.string.ok, null) //
                    .create()//
                    .show();
        }
    }

    private void handleRingtonePermissionRequestResult(int[] grantResults) {
        ListPreferenceWithSummaryFix preferenceReminderBell = (ListPreferenceWithSummaryFix) getPreferenceScreen()
                .findPreference(getText(R.string.keyReminderBell));
        if (grantResults.length == 0) {
            // if request is cancelled, the result arrays are empty, so leave this option "on" and don't explain it
        } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // User granted the needed permission therefore this option is set to "off"
            if (preferenceReminderBell.getOnPreferenceChangeListener().onPreferenceChange(preferenceReminderBell,
                    "0")) {
                preferenceReminderBell.setValue("0"); // WARNING: This does NOT call the onPreferenceValueChangeListener
            }
        } else if (ActivityCompat.shouldShowRequestPermissionRationale(MindBellPreferences.this,
                Manifest.permission.READ_EXTERNAL_STORAGE)) {
            // User denied the needed permission and can be given an explanation, so we show an explanation
            new AlertDialog.Builder(MindBellPreferences.this) //
                    .setTitle(R.string.reasonReadExternalStorageTitle) //
                    .setMessage(R.string.reasonReadExternalStorageText) //
                    .setPositiveButton(android.R.string.ok, null) //
                    .create() //
                    .show();
        } else {
            // User denied the needed permission and checked never ask again
            new AlertDialog.Builder(MindBellPreferences.this) //
                    .setTitle(R.string.neverAskAgainReadExternalStorageTitle) //
                    .setMessage(R.string.neverAskAgainReadExternalStorageText) //
                    .setPositiveButton(android.R.string.ok, null) //
                    .create()//
                    .show();
        }
    }

}