at.jclehner.rxdroid.NotificationReceiver.java Source code

Java tutorial

Introduction

Here is the source code for at.jclehner.rxdroid.NotificationReceiver.java

Source

/**
 * RxDroid - A Medication Reminder
 * Copyright (C) 2011-2013 Joseph Lehner <joseph.c.lehner@gmail.com>
 *
 *
 * RxDroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RxDroid is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RxDroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 */

package at.jclehner.rxdroid;

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

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigTextStyle;
import android.support.v4.app.NotificationCompat.InboxStyle;
import android.text.Html;
import android.util.Log;
import at.jclehner.androidutils.EventDispatcher;
import at.jclehner.rxdroid.Settings.DoseTimeInfo;
import at.jclehner.rxdroid.db.Database;
import at.jclehner.rxdroid.db.Drug;
import at.jclehner.rxdroid.db.Entries;
import at.jclehner.rxdroid.db.Schedule;
import at.jclehner.rxdroid.preferences.TimePeriodPreference.TimePeriod;
import at.jclehner.rxdroid.util.DateTime;
import at.jclehner.rxdroid.util.Util;

public class NotificationReceiver extends BroadcastReceiver {
    private static final String TAG = NotificationReceiver.class.getSimpleName();
    private static final boolean LOGV = BuildConfig.DEBUG;

    private static final int LED_CYCLE_MS = 5000;
    private static final int LED_ON_MS = 500;
    private static final int LED_OFF_MS = LED_CYCLE_MS - LED_ON_MS;

    private static final Class<?>[] EVENT_HANDLER_ARG_TYPES = { Date.class, int.class };

    public interface OnDoseTimeChangeListener {
        void onDoseTimeBegin(Date date, int doseTime);

        void onDoseTimeEnd(Date date, int doseTime);
    }

    static final String EXTRA_SILENT = "at.jclehner.rxdroid.extra.SILENT";
    static final String EXTRA_DATE = "at.jclehner.rxdroid.extra.DATE";
    static final String EXTRA_DOSE_TIME = "at.jclehner.rxdroid.extra.DOSE_TIME";
    static final String EXTRA_IS_DOSE_TIME_END = "at.jclehner.rxdroid.extra.IS_DOSE_TIME_END";
    static final String EXTRA_IS_ALARM_REPETITION = "at.jclehner.rxdroid.extra.IS_ALARM_REPETITION";
    static final String EXTRA_FORCE_UPDATE = "at.jclehner.rxdroid.extra.FORCE_UPDATE";

    private static final int NOTIFICATION_NORMAL = 0;
    private static final int NOTIFICATION_FORCE_UPDATE = 1;
    private static final int NOTIFICATION_FORCE_SILENT = 2;

    private Context mContext;
    private AlarmManager mAlarmMgr;

    private List<Drug> mAllDrugs;

    private boolean mDoPostSilent = false;
    private boolean mForceUpdate = false;

    private static final EventDispatcher<OnDoseTimeChangeListener> sEventMgr = new EventDispatcher<OnDoseTimeChangeListener>();

    public static void registerOnDoseTimeChangeListener(OnDoseTimeChangeListener l) {
        sEventMgr.register(l);
    }

    public static void unregisterOnDoseTimeChangeListener(OnDoseTimeChangeListener l) {
        sEventMgr.unregister(l);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null)
            return;

        Settings.init();
        Database.init();

        final boolean isAlarmRepetition = intent.getBooleanExtra(EXTRA_IS_ALARM_REPETITION, false);

        final int doseTime = intent.getIntExtra(EXTRA_DOSE_TIME, Schedule.TIME_INVALID);
        if (doseTime != Schedule.TIME_INVALID) {
            if (!isAlarmRepetition) {
                final Date date = (Date) intent.getSerializableExtra(EXTRA_DATE);
                final boolean isDoseTimeEnd = intent.getBooleanExtra(EXTRA_IS_DOSE_TIME_END, false);
                final String eventName = isDoseTimeEnd ? "onDoseTimeEnd" : "onDoseTimeBegin";

                sEventMgr.post(eventName, EVENT_HANDLER_ARG_TYPES, date, doseTime);
            }
        }

        mContext = context;
        mAlarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        mDoPostSilent = intent.getBooleanExtra(EXTRA_SILENT, false);
        mForceUpdate = isAlarmRepetition ? true : intent.getBooleanExtra(EXTRA_FORCE_UPDATE, false);
        mAllDrugs = Database.getAll(Drug.class);

        rescheduleAlarms();
        updateCurrentNotifications();
    }

    private void rescheduleAlarms() {
        cancelAllAlarms();
        scheduleNextAlarms();
    }

    private void scheduleNextAlarms() {
        if (Settings.getDoseTimeBegin(Drug.TIME_MORNING) == null) {
            Log.w(TAG, "No dose-time settings available. Not scheduling alarms.");
            return;
        }

        if (LOGV)
            Log.i(TAG, "Scheduling next alarms...");

        final DoseTimeInfo dtInfo = Settings.getDoseTimeInfo();

        if (dtInfo.activeDoseTime() != Schedule.TIME_INVALID)
            scheduleNextBeginOrEndAlarm(dtInfo, true);
        else
            scheduleNextBeginOrEndAlarm(dtInfo, false);
    }

    private void updateCurrentNotifications() {
        final DoseTimeInfo dtInfo = Settings.getDoseTimeInfo();
        final boolean isActiveDoseTime;

        Date date = dtInfo.activeDate();
        int doseTime = dtInfo.activeDoseTime();

        if (doseTime == Schedule.TIME_INVALID) {
            isActiveDoseTime = false;
            doseTime = dtInfo.nextDoseTime();
            date = dtInfo.nextDoseTimeDate();
        } else
            isActiveDoseTime = true;

        final int mode;

        if (mForceUpdate)
            mode = NOTIFICATION_FORCE_UPDATE;
        else if (mDoPostSilent)
            mode = NOTIFICATION_FORCE_SILENT;
        else
            mode = NOTIFICATION_NORMAL;

        updateNotification(date, doseTime, isActiveDoseTime, mode);
    }

    private void scheduleNextBeginOrEndAlarm(DoseTimeInfo dtInfo, boolean scheduleEnd) {
        final int doseTime = scheduleEnd ? dtInfo.activeDoseTime() : dtInfo.nextDoseTime();
        final Calendar time = dtInfo.currentTime();
        final Date doseTimeDate = scheduleEnd ? dtInfo.activeDate() : dtInfo.nextDoseTimeDate();

        final Bundle alarmExtras = new Bundle();
        alarmExtras.putSerializable(EXTRA_DATE, doseTimeDate);
        alarmExtras.putInt(EXTRA_DOSE_TIME, doseTime);
        alarmExtras.putBoolean(EXTRA_IS_DOSE_TIME_END, scheduleEnd);
        alarmExtras.putBoolean(EXTRA_SILENT, false);

        long offset;

        if (scheduleEnd)
            offset = Settings.getMillisUntilDoseTimeEnd(time, doseTime);
        else
            offset = Settings.getMillisUntilDoseTimeBegin(time, doseTime);

        long triggerAtMillis = time.getTimeInMillis() + offset;

        final long alarmRepeatMins = Settings.getStringAsInt(Settings.Keys.ALARM_REPEAT, 0);
        final long alarmRepeatMillis = alarmRepeatMins == -1 ? 10000 : alarmRepeatMins * 60000;

        if (alarmRepeatMillis > 0) {
            alarmExtras.putBoolean(EXTRA_FORCE_UPDATE, true);

            final long base = dtInfo.activeDate().getTime();
            int i = 0;

            while (base + (i * alarmRepeatMillis) < time.getTimeInMillis())
                ++i;

            // We must tell the receiver whether the alarm is an actual dose time's
            // end or begin, or merely a repetition.

            final long triggerAtMillisWithRepeatedAlarm = base + i * alarmRepeatMillis;
            if (triggerAtMillisWithRepeatedAlarm < triggerAtMillis) {
                triggerAtMillis = triggerAtMillisWithRepeatedAlarm;
                alarmExtras.putBoolean(EXTRA_IS_ALARM_REPETITION, true);
            }

            //triggerAtMillis = base + (i * alarmRepeatMillis);
        }

        final long triggerDiffFromNow = triggerAtMillis - System.currentTimeMillis();
        if (triggerDiffFromNow < 0) {
            if (triggerDiffFromNow < -50000)
                Log.w(TAG, "Alarm time is in the past by less than 5 seconds.");
            else {
                Log.w(TAG, "Alarm time is in the past. Ignoring...");
                return;
            }
        }

        if (alarmExtras.getBoolean(EXTRA_IS_ALARM_REPETITION))
            Log.i(TAG, "Scheduling next alarm for " + DateTime.toString(triggerAtMillis));
        else {
            Log.i(TAG, "Scheduling " + (scheduleEnd ? "end" : "begin") + " of doseTime " + doseTime + " on date "
                    + DateTime.toDateString(doseTimeDate) + " for " + DateTime.toString(triggerAtMillis));
        }

        Log.i(TAG, "Alarm will go off in " + Util.millis(triggerDiffFromNow));

        mAlarmMgr.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, createOperation(alarmExtras));
    }

    private void cancelAllAlarms() {
        mAlarmMgr.cancel(createOperation(null));
    }

    private PendingIntent createOperation(Bundle extras) {
        Intent intent = new Intent(mContext, NotificationReceiver.class);
        intent.setAction(Intent.ACTION_MAIN);

        if (extras != null)
            intent.putExtras(extras);

        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
    }

    private PendingIntent createDrugListIntent(Date date) {
        final Intent intent = new Intent(mContext, DrugListActivity.class);
        intent.putExtra(DrugListActivity.EXTRA_STARTED_FROM_NOTIFICATION, true);
        intent.putExtra(DrugListActivity.EXTRA_DATE, date);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);

        return PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
    }

    public void updateNotification(Date date, int doseTime, boolean isActiveDoseTime, int mode) {
        final List<Drug> drugsWithLowSupplies = new ArrayList<Drug>();
        final int lowSupplyDrugCount = getDrugsWithLowSupplies(date, doseTime, drugsWithLowSupplies);
        final int missedDoseCount = getDrugsWithMissedDoses(date, doseTime, isActiveDoseTime, null);
        final int dueDoseCount = isActiveDoseTime ? getDrugsWithDueDoses(date, doseTime, null) : 0;

        int titleResId = R.string._title_notification_doses;
        int icon = R.drawable.ic_stat_normal;

        final StringBuilder sb = new StringBuilder();
        final String[] lines = new String[2];

        int lineCount = 0;

        if (missedDoseCount != 0 || dueDoseCount != 0) {
            if (dueDoseCount != 0)
                sb.append(RxDroid.getQuantityString(R.plurals._qmsg_due, dueDoseCount));

            if (missedDoseCount != 0) {
                if (sb.length() != 0)
                    sb.append(", ");

                sb.append(RxDroid.getQuantityString(R.plurals._qmsg_missed, missedDoseCount));
            }

            lines[1] = "<b>" + getString(R.string._title_notification_doses) + "</b> "
                    + Util.escapeHtml(sb.toString());
        }

        final boolean isShowingLowSupplyNotification;

        if (lowSupplyDrugCount != 0) {
            final String msg;
            final String first = drugsWithLowSupplies.get(0).getName();

            icon = R.drawable.ic_stat_exclamation;
            isShowingLowSupplyNotification = sb.length() == 0;
            //titleResId = R.string._title_notification_low_supplies;

            if (lowSupplyDrugCount == 1)
                msg = getString(R.string._qmsg_low_supply_single, first);
            else {
                final String second = drugsWithLowSupplies.get(1).getName();
                msg = RxDroid.getQuantityString(R.plurals._qmsg_low_supply_multiple, lowSupplyDrugCount - 1, first,
                        second);
            }

            if (isShowingLowSupplyNotification) {
                sb.append(msg);
                titleResId = R.string._title_notification_low_supplies;
            }

            lines[0] = "<b>" + getString(R.string._title_notification_low_supplies) + "</b> "
                    + Util.escapeHtml(msg);
        } else
            isShowingLowSupplyNotification = false;

        final int priority;
        if (isShowingLowSupplyNotification)
            priority = NotificationCompat.PRIORITY_DEFAULT;
        else
            priority = NotificationCompat.PRIORITY_HIGH;

        final String message = sb.toString();
        final int currentHash = message.hashCode();
        final int lastHash = Settings.getInt(Settings.Keys.LAST_MSG_HASH);

        if (message.length() == 0) {
            getNotificationManager().cancel(R.id.notification);
            return;
        }

        final StringBuilder source = new StringBuilder();

        //      final InboxStyle inboxStyle = new InboxStyle();
        //      inboxStyle.setBigContentTitle(getString(R.string.app_name) +
        //            " (" + (dueDoseCount + missedDoseCount + lowSupplyDrugCount) + ")");

        for (String line : lines) {
            if (line != null) {
                if (lineCount != 0)
                    source.append("\n<br/>\n");

                source.append(line);
                //            inboxStyle.addLine(Html.fromHtml(line));
                ++lineCount;
            }
        }

        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
        builder.setContentTitle(getString(titleResId));
        builder.setContentIntent(createDrugListIntent(date));
        builder.setContentText(message);
        builder.setTicker(getString(R.string._msg_new_notification));
        builder.setSmallIcon(icon);
        builder.setOngoing(true);
        builder.setUsesChronometer(false);
        builder.setWhen(0);
        builder.setPriority(priority);

        if (lineCount > 1) {
            final BigTextStyle style = new BigTextStyle();
            style.setBigContentTitle(getString(R.string.app_name));
            style.bigText(Html.fromHtml(source.toString()));
            builder.setStyle(style);
        }

        //      final long offset;
        //
        //      if(isActiveDoseTime)
        //         offset = Settings.getDoseTimeBeginOffset(doseTime);
        //      else
        //         offset = Settings.getTrueDoseTimeEndOffset(doseTime);
        //
        //      builder.setWhen(date.getTime() + offset);

        if (mode == NOTIFICATION_FORCE_UPDATE || currentHash != lastHash) {
            builder.setOnlyAlertOnce(false);
            Settings.putInt(Settings.Keys.LAST_MSG_HASH, currentHash);
        } else
            builder.setOnlyAlertOnce(true);

        // Prevents low supplies from constantly annoying the user with
        // notification's sound and/or vibration if alarms are repeated.
        if (isShowingLowSupplyNotification)
            mode = NOTIFICATION_FORCE_SILENT;

        int defaults = 0;

        final String lightColor = Settings.getString(Settings.Keys.NOTIFICATION_LIGHT_COLOR, "");
        if (lightColor.length() == 0)
            defaults |= Notification.DEFAULT_LIGHTS;
        else {
            try {
                int ledARGB = Integer.parseInt(lightColor, 16);
                if (ledARGB != 0) {
                    ledARGB |= 0xff000000; // set alpha to ff
                    builder.setLights(ledARGB, LED_ON_MS, LED_OFF_MS);
                }
            } catch (NumberFormatException e) {
                Log.e(TAG, "Failed to parse light color; using default", e);
                defaults |= Notification.DEFAULT_LIGHTS;
            }
        }

        if (mode != NOTIFICATION_FORCE_SILENT) {
            boolean isNowWithinQuietHours = false;

            do {
                if (!Settings.isChecked(Settings.Keys.QUIET_HOURS, false))
                    break;

                final String quietHoursStr = Settings.getString(Settings.Keys.QUIET_HOURS);
                if (quietHoursStr == null)
                    break;

                final TimePeriod quietHours = TimePeriod.fromString(quietHoursStr);
                if (quietHours.contains(DumbTime.now()))
                    isNowWithinQuietHours = true;

            } while (false);

            if (!isNowWithinQuietHours) {
                final String ringtone = Settings.getString(Settings.Keys.NOTIFICATION_SOUND);
                if (ringtone != null)
                    builder.setSound(Uri.parse(ringtone));
                else
                    defaults |= Notification.DEFAULT_SOUND;

                if (LOGV)
                    Log.i(TAG, "Sound: " + (ringtone != null ? ringtone.toString() : "(default)"));
            } else
                Log.i(TAG, "Currently within quiet hours; muting sound...");
        }

        if (mode != NOTIFICATION_FORCE_SILENT && Settings.getBoolean(Settings.Keys.USE_VIBRATOR, true))
            defaults |= Notification.DEFAULT_VIBRATE;

        builder.setDefaults(defaults);

        getNotificationManager().notify(R.id.notification, builder.build());
    }

    private int getDrugsWithDueDoses(Date date, int doseTime, List<Drug> outDrugs) {
        int count = 0;

        for (Drug drug : mAllDrugs) {
            final Fraction dose = drug.getDose(doseTime, date);

            if (!drug.isActive() || dose.isZero() || drug.hasAutoDoseEvents()
                    || drug.getRepeatMode() == Drug.REPEAT_AS_NEEDED)
                continue;

            if (Entries.countDoseEvents(drug, date, doseTime) == 0) {
                ++count;

                if (outDrugs != null)
                    outDrugs.add(drug);
            }
        }

        return count;
    }

    private int getDrugsWithMissedDoses(Date date, int activeOrNextDoseTime, boolean isActiveDoseTime,
            List<Drug> outDrugs) {
        final int end;

        if (!isActiveDoseTime && activeOrNextDoseTime == Drug.TIME_MORNING) {
            date = DateTime.add(date, Calendar.DAY_OF_MONTH, -1);
            end = Drug.TIME_INVALID;
        } else
            end = activeOrNextDoseTime;

        int count = 0;

        for (int doseTime = Schedule.TIME_MORNING; doseTime != end; ++doseTime)
            count += getDrugsWithDueDoses(date, doseTime, outDrugs);

        return count;
    }

    private int getDrugsWithLowSupplies(Date date, int doseTime, List<Drug> outDrugs) {
        int count = 0;

        for (Drug drug : mAllDrugs) {
            if (Entries.hasLowSupplies(drug)) {
                ++count;

                if (outDrugs != null)
                    outDrugs.add(drug);
            }
        }

        return count;
    }

    private String getString(int resId, Object... formatArgs) {
        return mContext.getString(resId, formatArgs);
    }

    private NotificationManager getNotificationManager() {
        return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
    }

    /* package */ static void rescheduleAlarmsAndUpdateNotification(boolean silent) {
        rescheduleAlarmsAndUpdateNotification(null, silent);
    }

    /* package */ static void rescheduleAlarmsAndUpdateNotification(Context context, boolean silent) {
        if (context == null)
            context = RxDroid.getContext();
        final Intent intent = new Intent(context, NotificationReceiver.class);
        intent.setAction(Intent.ACTION_MAIN);
        intent.putExtra(NotificationReceiver.EXTRA_SILENT, silent);
        context.sendBroadcast(intent);
    }
}