de.ub0r.android.callmeter.data.RuleMatcher.java Source code

Java tutorial

Introduction

Here is the source code for de.ub0r.android.callmeter.data.RuleMatcher.java

Source

/*
 * Copyright (C) 2009-2013 Felix Bechstein
 * 
 * This file is part of Call Meter 3G.
 * 
 * This program 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.
 * 
 * This program 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
 * this program; If not, see <http://www.gnu.org/licenses/>.
 */
package de.ub0r.android.callmeter.data;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.SparseArray;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;

import de.ub0r.android.callmeter.CallMeter;
import de.ub0r.android.callmeter.R;
import de.ub0r.android.callmeter.ui.Plans;
import de.ub0r.android.callmeter.ui.prefs.Preferences;
import de.ub0r.android.lib.Utils;
import de.ub0r.android.logg0r.Log;

/**
 * Class matching logs via rules to plans.
 *
 * @author flx
 */
public final class RuleMatcher {

    /**
     * Tag for output.
     */
    private static final String TAG = "RuleMatcher";

    /**
     * Minimal number length for converting national to international numbers.
     */
    private static final int NUMBER_MIN_LENGTH = 7;

    /**
     * Steps for updating the GUI.
     */
    private static final int PROGRESS_STEPS = 25;

    /**
     * Strip leading zeros.
     */
    private static boolean stripLeadingZeros = false;

    /**
     * International number prefix.
     */
    private static String intPrefix = "";

    /**
     * Concat prefix and number without leading zeros at number.
     */
    private static boolean zeroPrefix = true;

    /**
     * A single Rule.
     *
     * @author flx
     */
    private static class Rule {

        /**
         * Get the {@link NumbersGroup}.
         *
         * @param cr   {@link ContentResolver}
         * @param gids ids of group
         * @return {@link NumbersGroup}s
         */
        static NumbersGroup[] getNumberGroups(final ContentResolver cr, final String gids) {
            if (gids == null) {
                return null;
            }
            final String[] split = gids.split(",");
            ArrayList<NumbersGroup> list = new ArrayList<NumbersGroup>();
            for (String s : split) {
                if (s == null || s.length() == 0 || s.equals("-1")) {
                    continue;
                }
                final NumbersGroup ng = new NumbersGroup(cr, Utils.parseLong(s, -1L));
                if (ng.numbers.size() > 0) {
                    list.add(ng);
                }
            }
            if (list.size() == 0) {
                return null;
            }
            return list.toArray(new NumbersGroup[list.size()]);
        }

        /**
         * Get the {@link HoursGroup}.
         *
         * @param cr   {@link ContentResolver}
         * @param gids id of group
         * @return {@link HoursGroup}s
         */
        static HoursGroup[] getHourGroups(final ContentResolver cr, final String gids) {
            if (gids == null) {
                return null;
            }
            final String[] split = gids.split(",");
            ArrayList<HoursGroup> list = new ArrayList<HoursGroup>();
            for (String s : split) {
                if (s == null || s.length() == 0 || s.equals("-1")) {
                    continue;
                }
                final HoursGroup ng = new HoursGroup(cr, Utils.parseLong(s, -1L));
                if (ng.hours.size() > 0) {
                    list.add(ng);
                }
            }
            if (list.size() == 0) {
                return null;
            }
            return list.toArray(new HoursGroup[list.size()]);
        }

        /**
         * Group of numbers.
         */
        private static final class NumbersGroup {

            /**
             * List of numbers.
             */
            private final ArrayList<String> numbers = new ArrayList<String>();

            /**
             * Default Constructor.
             *
             * @param cr    {@link ContentResolver}
             * @param what0 argument
             */
            private NumbersGroup(final ContentResolver cr, final long what0) {
                //noinspection ConstantConditions
                final Cursor cursor = cr.query(ContentUris.withAppendedId(DataProvider.Numbers.GROUP_URI, what0),
                        DataProvider.Numbers.PROJECTION, null, null, null);
                if (cursor != null && cursor.moveToFirst()) {
                    final boolean doPrefix = intPrefix.length() > 1;
                    do {
                        String s = cursor.getString(DataProvider.Numbers.INDEX_NUMBER);
                        if (s == null || s.length() == 0) {
                            continue;
                        }
                        if (stripLeadingZeros) {
                            s = s.replaceFirst("^00*", "");
                        }
                        if (doPrefix && !s.startsWith("%")) {
                            s = national2international(intPrefix, zeroPrefix, s);
                        }
                        numbers.add(s);
                    } while (cursor.moveToNext());
                }
                if (cursor != null && !cursor.isClosed()) {
                    cursor.close();
                }
            }

            /**
             * Convert national number to international. Old format internationals were converted to
             * new format.
             *
             * @param iPrefix default prefix
             * @param zPrefix concat prefix and number without leading zeros at number
             * @param number  national number
             * @return international number
             */
            private static String national2international(final String iPrefix, final boolean zPrefix,
                    final String number) {
                if (number.length() < NUMBER_MIN_LENGTH && !number.endsWith("%")) {
                    return number;
                } else if (number.startsWith("0800") || number.startsWith("00800")) {
                    return number;
                } else if (number.startsWith("+")) {
                    return number;
                } else if (number.startsWith("00")) {
                    return "+" + number.substring(2);
                } else if (number.startsWith("0")) {
                    return iPrefix + number.substring(1);
                } else if (iPrefix.length() > 1 && number.startsWith(iPrefix.substring(1))) {
                    return "+" + number;
                } else if (zPrefix) {
                    return iPrefix + number;
                } else {
                    return number;
                }
            }

            /**
             * Match a given log.
             *
             * @param log {@link Cursor} representing log
             * @return true if log matches
             */
            boolean match(final Cursor log) {
                String number = log.getString(DataProvider.Logs.INDEX_REMOTE);
                if (number == null) {
                    return false;
                }
                if (number.length() == 0) {
                    return false;
                }
                Log.d(TAG, "NumbersGroup.match(", number, ")");
                if (number.length() > 1) {
                    if (stripLeadingZeros) {
                        number = number.replaceFirst("^00*", "");
                    }
                    if (intPrefix.length() > 1) {
                        number = national2international(intPrefix, zeroPrefix, number);
                    }
                }
                final int l = numbers.size();
                for (int i = 0; i < l; i++) {
                    String n = numbers.get(i);
                    if (n == null) {
                        Log.w(TAG, "numbers[", i, "] = null");
                        return false;
                    }
                    int nl = n.length();
                    if (nl <= 1) {
                        Log.w(TAG, "#numbers[", i, "] = ", nl);
                        return false;
                    }

                    if (n.startsWith("%")) {
                        if (n.endsWith("%")) {
                            if (nl == 2) {
                                Log.w(TAG, "numbers[", i, "] = ", n);
                                return false;
                            }
                            if (number.contains(n.substring(1, nl - 1))) {
                                Log.d(TAG, "match: ", n);
                                return true;
                            }
                        } else {
                            if (number.endsWith(n.substring(1))) {
                                Log.d(TAG, "match: ", n);
                                return true;
                            }
                        }
                    } else if (n.endsWith("%")) {
                        if (number.startsWith(n.substring(0, nl - 1))) {
                            Log.d(TAG, "match: ", n);
                            return true;
                        }
                    } else if (PhoneNumberUtils.compare(number, n)) {
                        Log.d(TAG, "match: ", n);
                        return true;
                    }
                    Log.v(TAG, "no match: ", n);
                }
                return false;
            }
        }

        /**
         * Group of hours.
         */
        private static final class HoursGroup {

            /**
             * List of hours.
             */
            private final SparseArray<HashSet<Integer>> hours = new SparseArray<HashSet<Integer>>();

            /**
             * Entry for monday - sunday.
             */
            private static final int ALL_WEEK = 0;

            /**
             * Entry for monday.
             */
            private static final int MON = 1;
            /** Entry for tuesday. */
            // private static final int TUE = 2;
            /** Entry for wednesday. */
            // private static final int WED = 3;
            /** Entry for thrusday. */
            // private static final int THU = 4;
            /** Entry for friday. */
            // private static final int FRI = 5;

            /**
             * Entry for satadurday.
             */
            private static final int SAT = 6;

            /**
             * Entry for sunday.
             */
            private static final int SUN = 7;

            /**
             * Entry for monday - friday.
             */
            private static final int MON_FRI = 8;

            /**
             * Default Constructor.
             *
             * @param cr    {@link ContentResolver}
             * @param what0 argument
             */
            private HoursGroup(final ContentResolver cr, final long what0) {
                //noinspection ConstantConditions
                final Cursor cursor = cr.query(ContentUris.withAppendedId(DataProvider.Hours.GROUP_URI, what0),
                        DataProvider.Hours.PROJECTION, null, null, null);
                if (cursor != null && cursor.moveToFirst()) {
                    do {
                        final int d = cursor.getInt(DataProvider.Hours.INDEX_DAY);
                        final int h = cursor.getInt(DataProvider.Hours.INDEX_HOUR);
                        HashSet<Integer> hs = hours.get(d);
                        if (hs == null) {
                            hs = new HashSet<Integer>();
                            hs.add(h);
                            hours.put(d, hs);
                        } else {
                            hs.add(h);
                        }
                    } while (cursor.moveToNext());
                }
                if (cursor != null && !cursor.isClosed()) {
                    cursor.close();
                }
            }

            /**
             * Internal var for match().
             */
            private static final Calendar CAL = Calendar.getInstance();

            /**
             * Match a given log.
             *
             * @param log {@link Cursor} representing log
             * @return true if log matches
             */
            boolean match(final Cursor log) {
                long date = log.getLong(DataProvider.Logs.INDEX_DATE);
                CAL.setTimeInMillis(date);
                final int d = (CAL.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY) % SUN;
                final int h = CAL.get(Calendar.HOUR_OF_DAY) + 1;
                int l = hours.size();
                for (int i = 0; i < l; i++) {
                    int k = hours.keyAt(i);
                    if (k == ALL_WEEK || (k == MON_FRI && d < SAT && d >= MON) || k % SUN == d) {
                        for (int v : hours.get(k)) {
                            if (v == 0 || v == h) {
                                return true;
                            }
                        }
                    }
                }
                return false;
            }
        }

        /**
         * Id.
         */
        private final int id;

        /**
         * ID of plan referred by this rule.
         */
        private final int planId;

        /**
         * Kind of rule.
         */
        private final int what;

        /**
         * My own number.
         */
        private final String myNumber;

        /**
         * Is roamed?
         */
        private final int roamed;

        /**
         * Is direction?
         */
        private final int direction;

        /**
         * Match hours?
         */
        private final HoursGroup[] inhours, exhours;

        /**
         * Match numbers?
         */
        private final NumbersGroup[] innumbers, exnumbers;

        /**
         * Match only if limit is not reached?
         */
        private final boolean limitNotReached;

        /**
         * Match only websms.
         */
        private final int iswebsms;

        /**
         * Match only specific websms connector.
         */
        private final String iswebsmsConnector;

        /**
         * Match only sipcalls.
         */
        private final int issipcall;

        /**
         * Load a {@link Rule}.
         *
         * @param cr              {@link ContentResolver}
         * @param overwritePlanId overwrite plan id
         * @param cursor          {@link Cursor}
         */
        Rule(final ContentResolver cr, final Cursor cursor, final int overwritePlanId) {
            id = cursor.getInt(DataProvider.Rules.INDEX_ID);
            if (overwritePlanId >= 0) {
                planId = overwritePlanId;
            } else {
                planId = cursor.getInt(DataProvider.Rules.INDEX_PLAN_ID);
            }
            what = cursor.getInt(DataProvider.Rules.INDEX_WHAT);
            direction = cursor.getInt(DataProvider.Rules.INDEX_DIRECTION);
            String s = cursor.getString(DataProvider.Rules.INDEX_MYNUMBER);
            if (TextUtils.isEmpty(s)) {
                myNumber = null;
            } else {
                myNumber = s;
            }
            roamed = cursor.getInt(DataProvider.Rules.INDEX_ROAMED);
            inhours = getHourGroups(cr, cursor.getString(DataProvider.Rules.INDEX_INHOURS_ID));
            exhours = getHourGroups(cr, cursor.getString(DataProvider.Rules.INDEX_EXHOURS_ID));
            innumbers = getNumberGroups(cr, cursor.getString(DataProvider.Rules.INDEX_INNUMBERS_ID));
            exnumbers = getNumberGroups(cr, cursor.getString(DataProvider.Rules.INDEX_EXNUMBERS_ID));
            limitNotReached = cursor.getInt(DataProvider.Rules.INDEX_LIMIT_NOT_REACHED) > 0;
            if (cursor.isNull(DataProvider.Rules.INDEX_IS_WEBSMS)) {
                iswebsms = DataProvider.Rules.NO_MATTER;
            } else {
                final int i = cursor.getInt(DataProvider.Rules.INDEX_IS_WEBSMS);
                if (i >= 0) {
                    iswebsms = i;
                } else {
                    iswebsms = DataProvider.Rules.NO_MATTER;
                }
            }
            s = cursor.getString(DataProvider.Rules.INDEX_IS_WEBSMS_CONNETOR);
            if (TextUtils.isEmpty(s)) {
                iswebsmsConnector = "";
            } else {
                iswebsmsConnector = " AND " + DataProvider.WebSMS.CONNECTOR + " LIKE '%" + s.toLowerCase() + "%'";
            }
            if (cursor.isNull(DataProvider.Rules.INDEX_IS_SIPCALL)) {
                issipcall = DataProvider.Rules.NO_MATTER;
            } else {
                final int i = cursor.getInt(DataProvider.Rules.INDEX_IS_SIPCALL);
                if (i >= 0) {
                    issipcall = i;
                } else {
                    issipcall = DataProvider.Rules.NO_MATTER;
                }
            }
        }

        /**
         * @return {@link Rule}'s id
         */
        int getId() {
            return id;
        }

        /**
         * @return {@link Plan}'s id
         */
        int getPlanId() {
            return planId;
        }

        /**
         * Internal var for match().
         */
        private static final String[] S1 = new String[1];

        /**
         * Math a log.
         *
         * @param cr  {@link ContentResolver}
         * @param log {@link Cursor} representing the log.
         * @return matched?
         */
        boolean match(final ContentResolver cr, final Cursor log) {
            Log.d(TAG, "match()");
            Log.d(TAG, "what: ", what);
            final int t = log.getInt(DataProvider.Logs.INDEX_TYPE);
            Log.d(TAG, "type: ", t);
            boolean ret = false;

            if (roamed == 0 || roamed == 1) {
                // rule.roamed=0: yes
                // rule.roamed=1: no
                // log.roamed=0: not roamed
                // log.roamed=1: roamed
                ret = log.getInt(DataProvider.Logs.INDEX_ROAMED) != roamed;
                Log.d(TAG, "ret after romaing: ", ret);
                if (!ret) {
                    return false;
                }
            }

            if (direction >= 0 && direction != DataProvider.Rules.NO_MATTER) {
                ret = log.getInt(DataProvider.Logs.INDEX_DIRECTION) == direction;
                Log.d(TAG, "ret after direction: ", ret);
                if (!ret) {
                    return false;
                }
            }

            switch (what) {
            case DataProvider.Rules.WHAT_CALL:
                ret = (t == DataProvider.TYPE_CALL);
                if (ret && issipcall != DataProvider.Rules.NO_MATTER) {
                    final long d = log.getLong(DataProvider.Logs.INDEX_DATE);
                    Log.d(TAG, "match sipcall: ", issipcall);
                    S1[0] = String.valueOf(d);
                    if (issipcall == 1) {
                        // match no sipcall
                        final Cursor c = cr.query(DataProvider.SipCall.CONTENT_URI, DataProvider.SipCall.PROJECTION,
                                DataProvider.SipCall.DATE + " = ?", S1, null);
                        if (c != null && c.getCount() > 0) {
                            ret = false;
                        }
                        if (c != null && !c.isClosed()) {
                            c.close();
                        }
                    } else {
                        // match only sipcall
                        final Cursor c = cr.query(DataProvider.SipCall.CONTENT_URI, DataProvider.SipCall.PROJECTION,
                                DataProvider.SipCall.DATE + " = ?", S1, null);
                        ret = c != null && c.getCount() > 0;
                        if (c != null && !c.isClosed()) {
                            c.close();
                        }
                    }
                    Log.d(TAG, "match sipcall: ", issipcall, "; ", ret);
                }
                break;
            case DataProvider.Rules.WHAT_DATA:
                ret = (t == DataProvider.TYPE_DATA);
                break;
            case DataProvider.Rules.WHAT_MMS:
                ret = (t == DataProvider.TYPE_MMS);
                break;
            case DataProvider.Rules.WHAT_SMS:
                ret = (t == DataProvider.TYPE_SMS);
                if (ret && iswebsms != DataProvider.Rules.NO_MATTER) {
                    final long d = log.getLong(DataProvider.Logs.INDEX_DATE);
                    Log.d(TAG, "match websms: ", iswebsms);
                    S1[0] = String.valueOf(d);
                    if (iswebsms == 1) {
                        // match no websms
                        final Cursor c = cr.query(DataProvider.WebSMS.CONTENT_URI, DataProvider.WebSMS.PROJECTION,
                                DataProvider.WebSMS.DATE + " = ?", S1, null);
                        if (c != null && c.getCount() > 0) {
                            ret = false;
                        }
                        if (c != null && !c.isClosed()) {
                            c.close();
                        }
                    } else {
                        // match only websms
                        final Cursor c = cr.query(DataProvider.WebSMS.CONTENT_URI, DataProvider.WebSMS.PROJECTION,
                                DataProvider.WebSMS.DATE + " = ? " + iswebsmsConnector, S1, null);
                        ret = c != null && c.getCount() > 0;
                        if (c != null && !c.isClosed()) {
                            c.close();
                        }
                    }
                    Log.d(TAG, "match websms: ", iswebsms, "; ", ret);
                }
                break;
            default:
                break;
            }
            Log.d(TAG, "ret after type: ", ret);
            if (!ret) {
                return false;
            }
            if (limitNotReached) {
                final Plan p = plans.get(planId);
                if (p != null) {
                    p.checkBillday(log);
                    ret = p.getRemainingLimit() > 0f;
                }
                if (!ret) {
                    Log.d(TAG, "limit reached: ", planId);
                }
            }
            Log.d(TAG, "ret after limit: ", ret);
            if (!ret) {
                return false;
            }

            if (myNumber != null) {
                // FIXME: do equals?
                ret = myNumber.equals(log.getString(DataProvider.Logs.INDEX_MYNUMBER));
                Log.d(TAG, "ret after mynumber: ", ret);
                if (!ret) {
                    return false;
                }
            }

            if (inhours != null) {
                ret = false;
                for (HoursGroup inhour : inhours) {
                    ret = inhour.match(log);
                    if (ret) {
                        break;
                    }
                }
            }
            Log.d(TAG, "ret after inhours: ", ret);
            if (!ret) {
                return false;
            }
            if (exhours != null) {
                for (HoursGroup exhour : exhours) {
                    ret = !exhour.match(log);
                    if (!ret) {
                        break;
                    }
                }
            }
            Log.d(TAG, "ret after exhours: ", ret);
            if (!ret) {
                return false;
            }
            if (innumbers != null) {
                ret = false;
                for (NumbersGroup innumber : innumbers) {
                    ret = innumber.match(log);
                    if (ret) {
                        break;
                    }
                }
            }
            Log.d(TAG, "ret after innumbers: ", ret);
            if (!ret) {
                return false;
            }
            if (exnumbers != null) {
                for (NumbersGroup exnumber : exnumbers) {
                    ret = !exnumber.match(log);
                    if (!ret) {
                        break;
                    }
                }
            }
            Log.d(TAG, "ret after exnumbers: ", ret);
            return ret;
        }
    }

    /**
     * A single Plan.
     *
     * @author flx
     */
    private static class Plan {

        /**
         * Id.
         */
        private final int id;

        /**
         * Name of plan.
         */
        private final String name;

        /**
         * Type of log.
         */
        private final int type;

        /**
         * Type of limit.
         */
        private final int limitType;

        /**
         * Limit.
         */
        private final long limit;

        /**
         * Billmode.
         */
        private final int billModeFirstLength, billModeNextLength;

        /**
         * Billday.
         */
        private final Calendar billday;

        /**
         * Billperiod.
         */
        private final int billperiod;

        /**
         * Cost per item.
         */
        private final float costPerItem;

        /**
         * Cost per amount.
         */
        private final float costPerAmount1, costPerAmount2;

        /**
         * Cost per item in limit.
         */
        private final float costPerItemInLimit;

        /**
         * Cost per amount in limit.
         */
        private final float costPerAmountInLimit1, costPerAmountInLimit2;

        /**
         * Units for mixed plans.
         */
        private final int upc, ups, upm, upd;

        /**
         * Strip first x seconds.
         */
        private final int stripSeconds;

        /**
         * Strip everything but first x seconds.
         */
        private final int stripPast;

        /**
         * Parent plan id.
         */
        private final int ppid;

        /**
         * PArent plan. Set in RuleMatcher.load().
         */
        private Plan parent = null;

        /**
         * Time of next alert.
         */
        private long nextAlert = 0;

        /**
         * Last valid billday.
         */
        private Calendar currentBillday = null;

        /**
         * Time of nextBillday.
         */
        private long nextBillday = -1L;

        /**
         * Amount billed this period.
         */
        private float billedAmount = 0f;

        /**
         * Cost billed this period.
         */
        private float billedCost = 0f;

        /**
         * {@link ContentResolver}.
         */
        private final ContentResolver cResolver;

        /**
         * Load a {@link Plan}.
         *
         * @param cr     {@link ContentResolver}
         * @param cursor {@link Cursor}
         */
        Plan(final ContentResolver cr, final Cursor cursor) {
            cResolver = cr;
            id = cursor.getInt(DataProvider.Plans.INDEX_ID);
            name = cursor.getString(DataProvider.Plans.INDEX_NAME);
            type = cursor.getInt(DataProvider.Plans.INDEX_TYPE);
            limitType = cursor.getInt(DataProvider.Plans.INDEX_LIMIT_TYPE);
            final long l = DataProvider.Plans.getLimit(type, limitType,
                    cursor.getFloat(DataProvider.Plans.INDEX_LIMIT));
            if (limitType == DataProvider.LIMIT_TYPE_UNITS && type == DataProvider.TYPE_DATA) {
                // normally amount is saved as kB, here it is B
                limit = l * CallMeter.BYTE_KB;
            } else {
                limit = l;
            }

            costPerItem = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_ITEM);
            costPerAmount1 = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_AMOUNT1);
            costPerAmount2 = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_AMOUNT2);
            costPerItemInLimit = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_ITEM_IN_LIMIT);
            costPerAmountInLimit1 = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_AMOUNT_IN_LIMIT1);
            costPerAmountInLimit2 = cursor.getFloat(DataProvider.Plans.INDEX_COST_PER_AMOUNT_IN_LIMIT2);
            upc = cursor.getInt(DataProvider.Plans.INDEX_MIXED_UNITS_CALL);
            ups = cursor.getInt(DataProvider.Plans.INDEX_MIXED_UNITS_SMS);
            upm = cursor.getInt(DataProvider.Plans.INDEX_MIXED_UNITS_MMS);
            upd = cursor.getInt(DataProvider.Plans.INDEX_MIXED_UNITS_DATA);
            nextAlert = cursor.getLong(DataProvider.Plans.INDEX_NEXT_ALERT);
            stripSeconds = cursor.getInt(DataProvider.Plans.INDEX_STRIP_SECONDS);
            stripPast = cursor.getInt(DataProvider.Plans.INDEX_STRIP_PAST);

            final long bp = cursor.getLong(DataProvider.Plans.INDEX_BILLPERIOD_ID);
            if (bp >= 0) {
                //noinspection ConstantConditions
                final Cursor c = cr.query(ContentUris.withAppendedId(DataProvider.Plans.CONTENT_URI, bp),
                        DataProvider.Plans.PROJECTION, null, null, null);
                if (c != null && c.moveToFirst()) {
                    billday = Calendar.getInstance();
                    billday.setTimeInMillis(c.getLong(DataProvider.Plans.INDEX_BILLDAY));
                    billperiod = c.getInt(DataProvider.Plans.INDEX_BILLPERIOD);
                } else {
                    billperiod = DataProvider.BILLPERIOD_INFINITE;
                    billday = null;
                }
                if (c != null && !c.isClosed()) {
                    c.close();
                }
            } else {
                billperiod = DataProvider.BILLPERIOD_INFINITE;
                billday = null;
            }
            final String billmode = cursor.getString(DataProvider.Plans.INDEX_BILLMODE);
            if (billmode != null && billmode.contains("/")) {
                String[] billmodes = billmode.split("/");
                billModeFirstLength = Utils.parseInt(billmodes[0], 1);
                billModeNextLength = Utils.parseInt(billmodes[1], 1);
            } else {
                billModeFirstLength = 1;
                billModeNextLength = 1;
            }
            ppid = DataProvider.Plans.getParent(cr, id);
        }

        /**
         * Get {@link Plan}'s id.
         *
         * @return {@link Plan}'s id
         */
        long getId() {
            return id;
        }

        /**
         * Get upc/upd/upm/ups according to log type.
         *
         * @param logType log type
         * @return units per *
         */
        int getUP(final int logType) {
            switch (logType) {
            case DataProvider.TYPE_CALL:
                return upc;
            case DataProvider.TYPE_DATA:
                return upd;
            case DataProvider.TYPE_MMS:
                return upm;
            case DataProvider.TYPE_SMS:
                return ups;
            default:
                return 0;
            }
        }

        /**
         * Check if this log is starting a new billing period.
         *
         * @param log {@link Cursor} pointing to log
         */
        void checkBillday(final Cursor log) {
            // skip for infinite bill periods
            if (billperiod == DataProvider.BILLPERIOD_INFINITE) {
                return;
            }

            // check whether date is in current bill period
            final long d = log.getLong(DataProvider.Logs.INDEX_DATE);
            if (currentBillday == null || nextBillday < d || d < currentBillday.getTimeInMillis()) {
                final Calendar now = Calendar.getInstance();
                now.setTimeInMillis(d);
                currentBillday = DataProvider.Plans.getBillDay(billperiod, billday, now, false);
                if (currentBillday == null) {
                    return;
                }
                final Calendar nbd = DataProvider.Plans.getBillDay(billperiod, billday, now, true);
                if (nbd == null) {
                    return;
                }
                nextBillday = nbd.getTimeInMillis();

                // load old stats
                final DataProvider.Plans.Plan plan = DataProvider.Plans.Plan.getPlan(cResolver, id, now, false,
                        false);
                if (plan == null) {
                    billedAmount = 0f;
                    billedCost = 0f;
                } else {
                    billedAmount = plan.bpBa;
                    billedCost = plan.cost;
                }
            }
            if (parent != null) {
                parent.checkBillday(log);
            }
        }

        /**
         * @return remaining limit before it is reached.
         */
        float getRemainingLimit() {
            Log.d(TAG, "getRemainingLimit(): ", id);
            if (parent != null && limitType == DataProvider.LIMIT_TYPE_NONE) {
                Log.d(TAG, "check parent");
                return parent.getRemainingLimit();
            } else {
                Log.d(TAG, "ltype: ", limitType);
                switch (limitType) {
                case DataProvider.LIMIT_TYPE_COST:
                    Log.d(TAG, "bc<lt ", billedCost * CallMeter.HUNDRED, "<", limit);
                    return limit - billedCost * CallMeter.HUNDRED;
                case DataProvider.LIMIT_TYPE_UNITS:
                    Log.d(TAG, "ba<lt ", billedAmount, "<", limit);
                    return limit - billedAmount;
                default:
                    return 0;
                }
            }
        }

        /**
         * Round up time with bill mode in mind.
         *
         * @param time time
         * @return rounded time
         */
        private long roundTime(final long time) {
            // 0 => 0
            if (time <= 0) {
                return 0;
            }
            final long fl = billModeFirstLength;
            final long nl = billModeNextLength;
            // !0 ..
            if (time <= fl) { // round first slot
                return fl;
            }
            if (nl == 0) {
                return fl;
            }
            if (nl == 1) {
                return time;
            }
            final long nt = time - fl;
            if (nt % nl == 0) {
                return time;
            }
            // round up to next full slot
            return fl + ((nt / nl) + 1) * nl;
        }

        /**
         * Update {@link Plan}.
         *
         * @param amount billed amount
         * @param cost   billed cost
         * @param t      type of log
         */
        void updatePlan(final float amount, final float cost, final int t) {
            billedAmount += amount;
            billedCost += cost;
            final Plan pp = parent;
            if (pp != null) {
                if (type != DataProvider.TYPE_MIXED && pp.type == DataProvider.TYPE_MIXED) {
                    switch (t) {
                    case DataProvider.TYPE_CALL:
                        pp.billedAmount += amount * pp.upc / CallMeter.SECONDS_MINUTE;
                        break;
                    case DataProvider.TYPE_MMS:
                        pp.billedAmount += amount * pp.upm;
                        break;
                    case DataProvider.TYPE_SMS:
                        pp.billedAmount += amount * pp.ups;
                        break;
                    default:
                        break;
                    }
                } else {
                    pp.billedAmount += amount;
                }
                parent.billedCost += cost;
            }
        }

        /**
         * Get billed amount for amount.
         *
         * @param log {@link Cursor} pointing to log
         * @return billed amount.
         */
        float getBilledAmount(final Cursor log) {
            long amount = log.getLong(DataProvider.Logs.INDEX_AMOUNT);
            final int t = log.getInt(DataProvider.Logs.INDEX_TYPE);
            float ret;
            switch (t) {
            case DataProvider.TYPE_CALL:
                ret = roundTime(amount);
                if (stripSeconds > 0) {
                    ret -= stripSeconds;
                    if (ret < 0f) {
                        ret = 0f;
                    }
                }
                if (stripPast > 0 && ret > stripPast) {
                    ret = stripPast;
                }
                break;
            default:
                ret = amount;
                break;
            }

            if (type == DataProvider.TYPE_MIXED) {
                switch (t) {
                case DataProvider.TYPE_CALL:
                    ret = ret * upc / CallMeter.SECONDS_MINUTE;
                    break;
                case DataProvider.TYPE_SMS:
                    ret = ret * ups;
                    break;
                case DataProvider.TYPE_MMS:
                    ret = ret * upm;
                    break;
                case DataProvider.TYPE_DATA:
                    ret = ret * upd / CallMeter.BYTE_MB;
                default:
                    break;
                }
            }
            return ret;
        }

        /**
         * Get cost for amount.
         *
         * @param log     {@link Cursor} pointing to log
         * @param bAmount billed amount
         * @return cost
         */
        float getCost(final Cursor log, final float bAmount) {
            final int t = log.getInt(DataProvider.Logs.INDEX_TYPE);
            final int pt = type;

            float ret = 0f;
            float as0; // split amount: before limit
            float as1; // split amount: after limit
            Plan p;
            float f = 1; // factor for mixed plans with limits merging this plan
            if (parent != null && limitType == DataProvider.LIMIT_TYPE_NONE) {
                p = parent;
                if (pt != DataProvider.TYPE_MIXED && p.type == DataProvider.TYPE_MIXED) {
                    f = 1f / p.getUP(t);
                    switch (t) {
                    case DataProvider.TYPE_CALL:
                        f *= CallMeter.SECONDS_MINUTE;
                        break;
                    case DataProvider.TYPE_DATA:
                        f *= CallMeter.BYTE_MB;
                        break;
                    default:
                        // nothing to do
                        break;
                    }
                }
            } else {
                p = this;
            }
            // split amount at limit
            float remaining = p.getRemainingLimit() * f;
            if (p.limitType == DataProvider.LIMIT_TYPE_NONE || remaining <= 0f) {
                as0 = 0;
                as1 = bAmount;
            } else if (p.limitType == DataProvider.LIMIT_TYPE_UNITS && remaining < bAmount) {
                as0 = remaining;
                as1 = bAmount - remaining;
            } else { // TODO: fix for LIMIT_TYPE_COST
                as0 = bAmount;
                as1 = 0;
            }

            if (t == DataProvider.TYPE_SMS || pt == DataProvider.TYPE_MIXED) {
                ret += as0 * costPerItemInLimit + as1 * costPerItem;
            } else {
                ret += as0 > 0f ? costPerItemInLimit : costPerItem;
            }

            switch (t) {
            case DataProvider.TYPE_CALL:
                if (bAmount <= billModeFirstLength) {
                    // bAmount is most likely < remaining
                    ret += (as0 * costPerAmountInLimit1 + as1 * costPerAmount1) / CallMeter.SECONDS_MINUTE;
                } else if (as0 == 0f) {
                    ret += costPerAmount1 * billModeFirstLength / CallMeter.SECONDS_MINUTE;
                    ret += costPerAmount2 * (bAmount - billModeFirstLength) / CallMeter.SECONDS_MINUTE;
                } else if (as1 == 0f) {
                    ret += costPerAmountInLimit1 * billModeFirstLength / CallMeter.SECONDS_MINUTE;
                    ret += costPerAmountInLimit2 * (bAmount - billModeFirstLength) / CallMeter.SECONDS_MINUTE;
                } else if (as0 == billModeFirstLength) {
                    ret += costPerAmountInLimit1 * billModeFirstLength / CallMeter.SECONDS_MINUTE;
                    ret += costPerAmount2 * (bAmount - billModeFirstLength) / CallMeter.SECONDS_MINUTE;
                } else if (as0 > billModeFirstLength) {
                    ret += costPerAmountInLimit1 * billModeFirstLength / CallMeter.SECONDS_MINUTE;
                    ret += (as0 - billModeFirstLength) * costPerAmountInLimit2 / CallMeter.SECONDS_MINUTE;
                    ret += as1 * costPerAmount2 / CallMeter.SECONDS_MINUTE;
                } else { // as0 < billModeFirstLength && as0 > 0 && as1 > 0
                    ret += as0 * costPerAmountInLimit1 / CallMeter.SECONDS_MINUTE;
                    ret += (billModeFirstLength - as0) * costPerAmount1 / CallMeter.SECONDS_MINUTE;
                    ret += costPerAmount2 * (bAmount - billModeFirstLength) / CallMeter.SECONDS_MINUTE;
                }
                break;
            case DataProvider.TYPE_DATA:
                ret += (as0 * costPerAmountInLimit1 + as1 * costPerAmount1) / CallMeter.BYTE_MB;
                break;
            default:
                break;
            }
            return ret;
        }

        /**
         * Get amount of free cost.
         *
         * @param log  {@link Cursor} pointing to log
         * @param cost cost calculated by getCost()
         * @return free cost
         */
        float getFree(final Cursor log, final float cost) {
            if (limitType != DataProvider.LIMIT_TYPE_COST) {
                if (parent != null) {
                    return parent.getFree(log, cost);
                }
                return 0f;
            }
            final float l = ((float) limit) / CallMeter.HUNDRED;
            if (l <= billedCost) {
                return 0f;
            }
            if (l >= billedCost + cost) {
                return cost;
            }
            return l - billedCost;
        }

        @Override
        public String toString() {
            return "RuleMatcher.Plan: " + name;
        }
    }

    /**
     * List of {@link Rule}s.
     */
    private static ArrayList<Rule> rules = null;

    /**
     * List of {@link Plan}s.
     */
    private static SparseArray<Plan> plans = null;

    /**
     * Default constructor.
     */
    private RuleMatcher() {
    }

    /**
     * Load {@link Rule}s and {@link Plan}s.
     *
     * @param context {@link Context}
     */
    private static void load(final Context context) {
        Log.d(TAG, "load()");
        if (rules != null && plans != null) {
            return;
        }
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        stripLeadingZeros = prefs.getBoolean(Preferences.PREFS_STRIP_LEADING_ZEROS, false);
        intPrefix = prefs.getString(Preferences.PREFS_INT_PREFIX, "");
        zeroPrefix = !intPrefix.equals("+44") && !intPrefix.equals("+49");

        final ContentResolver cr = context.getContentResolver();

        // load rules
        rules = new ArrayList<Rule>();
        Cursor cursor = cr.query(DataProvider.Rules.CONTENT_URI, DataProvider.Rules.PROJECTION,
                DataProvider.Rules.ACTIVE + ">0", null, DataProvider.Rules.ORDER);
        if (cursor != null && cursor.moveToFirst()) {
            do {
                rules.add(new Rule(cr, cursor, -1));
            } while (cursor.moveToNext());
        }
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }

        // load plans
        plans = new SparseArray<Plan>();
        cursor = cr.query(DataProvider.Plans.CONTENT_URI, DataProvider.Plans.PROJECTION,
                DataProvider.Plans.WHERE_REALPLANS, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            do {
                final int i = cursor.getInt(DataProvider.Plans.INDEX_ID);
                plans.put(i, new Plan(cr, cursor));
            } while (cursor.moveToNext());
        }
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
        // update parent references
        int l = plans.size();
        for (int i = 0; i < l; i++) {
            Plan p = plans.valueAt(i);
            p.parent = plans.get(p.ppid);
        }
    }

    /**
     * Reload Rules and plans.
     */
    static void flush() {
        Log.d(TAG, "flush()");
        rules = null;
        plans = null;
    }

    /**
     * Unmatch all logs.
     *
     * @param context {@link Context}
     */
    public static void unmatch(final Context context) {
        Log.d(TAG, "unmatch()");
        ContentValues cv = new ContentValues();
        final ContentResolver cr = context.getContentResolver();
        cv.put(DataProvider.Logs.PLAN_ID, DataProvider.NO_ID);
        cv.put(DataProvider.Logs.RULE_ID, DataProvider.NO_ID);
        // reset all but manually set plans
        cr.update(DataProvider.Logs.CONTENT_URI, cv,
                DataProvider.Logs.RULE_ID + " is null or NOT (" + DataProvider.Logs.RULE_ID + " = "
                        + DataProvider.NOT_FOUND + " AND " + DataProvider.Logs.PLAN_ID + " != "
                        + DataProvider.NOT_FOUND + ")",
                null);
        resetAlert(context);
    }

    public static void resetAlert(final Context context) {
        Log.d(TAG, "resetAlert()");
        ContentValues cv = new ContentValues();
        final ContentResolver cr = context.getContentResolver();
        cv.put(DataProvider.Plans.NEXT_ALERT, 0);
        cr.update(DataProvider.Plans.CONTENT_URI, cv, null, null);
        flush();
    }

    /**
     * Internal ar for matchLog().
     */
    private static final String WHERE = DataProvider.Logs.ID + " = ?";

    /**
     * Match a single log record given as {@link Cursor}.
     *
     * @param cr  {@link ContentResolver}
     * @param ops List of {@link ContentProviderOperation}s
     * @param log {@link Cursor} representing the log
     * @return true if a log was matched
     */
    private static boolean matchLog(final ContentResolver cr, final ArrayList<ContentProviderOperation> ops,
            final Cursor log) {
        if (cr == null) {
            Log.e(TAG, "matchLog(null, ops, log)");
            return false;
        }
        if (log == null) {
            Log.e(TAG, "matchLog(cr, ops, null)");
            return false;
        }
        final long lid = log.getLong(DataProvider.Logs.INDEX_ID);
        final int t = log.getInt(DataProvider.Logs.INDEX_TYPE);
        Log.d(TAG, "matchLog(cr, ", lid, ")");
        boolean matched = false;
        if (rules == null) {
            Log.e(TAG, "rules = null");
            return false;
        }
        if (plans == null) {
            Log.e(TAG, "plans = null");
            return false;
        }
        for (final Rule r : rules) {
            if (r == null || !r.match(cr, log) || plans == null) {
                continue;
            }
            Log.d(TAG, "matched rule: ", r.getId());
            final Plan p = plans.get(r.getPlanId());
            if (p != null) {
                final long pid = p.getId();
                final long rid = r.getId();
                Log.d(TAG, "found plan: ", pid);
                p.checkBillday(log);
                final float ba = p.getBilledAmount(log);
                final float bc = p.getCost(log, ba);
                ContentProviderOperation op = ContentProviderOperation.newUpdate(DataProvider.Logs.CONTENT_URI)
                        .withValue(DataProvider.Logs.PLAN_ID, pid).withValue(DataProvider.Logs.RULE_ID, rid)
                        .withValue(DataProvider.Logs.BILL_AMOUNT, ba).withValue(DataProvider.Logs.COST, bc)
                        .withValue(DataProvider.Logs.FREE, p.getFree(log, bc))
                        .withSelection(WHERE, new String[] { String.valueOf(lid) }).build();
                p.updatePlan(ba, bc, t);
                ops.add(op);
                matched = true;
                break;
            }
        }
        if (!matched) {
            ContentProviderOperation op = ContentProviderOperation.newUpdate(DataProvider.Logs.CONTENT_URI)
                    .withValue(DataProvider.Logs.PLAN_ID, DataProvider.NOT_FOUND)
                    .withValue(DataProvider.Logs.RULE_ID, DataProvider.NOT_FOUND)
                    .withSelection(WHERE, new String[] { String.valueOf(lid) }).build();
            ops.add(op);
        }
        return matched;
    }

    /**
     * Match a single log record.
     *
     * @param cr  {@link ContentResolver}
     * @param lid id of log item
     * @param pid id of plan
     */
    public static void matchLog(final ContentResolver cr, final long lid, final int pid) {
        if (cr == null) {
            Log.e(TAG, "matchLog(null, lid, pid)");
            return;
        }
        if (lid < 0L || pid < 0L) {
            Log.e(TAG, "matchLog(cr, " + lid + "," + pid + ")");
            return;
        }
        Log.d(TAG, "matchLog(cr, ", lid, ",", pid, ")");

        if (plans == null) {
            Log.e(TAG, "plans = null");
            return;
        }
        final Plan p = plans.get(pid);
        if (p == null) {
            Log.e(TAG, "plan=null");
            return;
        }
        final Cursor log = cr.query(DataProvider.Logs.CONTENT_URI, DataProvider.Logs.PROJECTION,
                DataProvider.Logs.ID + " = ?", new String[] { String.valueOf(lid) }, null);
        if (log == null) {
            return;
        }
        if (!log.moveToFirst()) {
            Log.e(TAG, "no log: " + log);
            log.close();
            return;
        }
        final int t = log.getInt(DataProvider.Logs.INDEX_TYPE);
        p.checkBillday(log);
        final ContentValues cv = new ContentValues();
        cv.put(DataProvider.Logs.PLAN_ID, pid);
        final float ba = p.getBilledAmount(log);
        cv.put(DataProvider.Logs.BILL_AMOUNT, ba);
        final float bc = p.getCost(log, ba);
        cv.put(DataProvider.Logs.COST, bc);
        cv.put(DataProvider.Logs.FREE, p.getFree(log, bc));
        p.updatePlan(ba, bc, t);
        cr.update(DataProvider.Logs.CONTENT_URI, cv, DataProvider.Logs.ID + " = ?",
                new String[] { String.valueOf(lid) });
        log.close();
    }

    /**
     * Match all unmatched logs.
     *
     * @param context    {@link Context}
     * @param showStatus post status to dialog/handler
     * @return true if a log was matched
     */
    static synchronized boolean match(final Context context, final boolean showStatus) {
        Log.d(TAG, "match(ctx, ", showStatus, ")");
        long start = System.currentTimeMillis();
        boolean ret = false;
        load(context);
        final ContentResolver cr = context.getContentResolver();
        final Cursor cursor = cr.query(DataProvider.Logs.CONTENT_URI, DataProvider.Logs.PROJECTION,
                DataProvider.Logs.PLAN_ID + " = " + DataProvider.NO_ID, null, DataProvider.Logs.DATE + " ASC");
        if (cursor != null && cursor.moveToFirst()) {
            final int l = cursor.getCount();
            Handler h;
            if (showStatus) {
                h = Plans.getHandler();
                if (h != null) {
                    final Message m = h.obtainMessage(Plans.MSG_BACKGROUND_PROGRESS_MATCHER);
                    m.arg1 = 0;
                    m.arg2 = l;
                    m.sendToTarget();
                }
            }
            try {
                ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
                int i = 1;
                do {
                    ret |= matchLog(cr, ops, cursor);
                    if (i % PROGRESS_STEPS == 0 || (i < PROGRESS_STEPS && i % CallMeter.TEN == 0)) {
                        h = Plans.getHandler();
                        if (h != null) {
                            final Message m = h.obtainMessage(Plans.MSG_BACKGROUND_PROGRESS_MATCHER);
                            m.arg1 = i;
                            m.arg2 = l;
                            Log.d(TAG, "send progress: ", i, "/", l);
                            m.sendToTarget();
                        } else {
                            Log.d(TAG, "send progress: ", i, " handler=null");
                        }
                        Log.d(TAG, "save logs..");
                        cr.applyBatch(DataProvider.AUTHORITY, ops);
                        ops.clear();
                        Log.d(TAG, "sleeping..");
                        try {
                            Thread.sleep(CallMeter.MILLIS);
                        } catch (InterruptedException e) {
                            Log.e(TAG, "sleep interrupted", e);
                        }
                        Log.d(TAG, "sleep finished");
                    }
                    ++i;
                } while (cursor.moveToNext());
                if (ops.size() > 0) {
                    cr.applyBatch(DataProvider.AUTHORITY, ops);
                }
            } catch (IllegalStateException e) {
                Log.e(TAG, "illegal state in RuleMatcher's loop", e);
            } catch (OperationApplicationException e) {
                Log.e(TAG, "illegal operation in RuleMatcher's loop", e);
            } catch (RemoteException e) {
                Log.e(TAG, "remote exception in RuleMatcher's loop", e);
            }
        }
        try {
            if (cursor != null && !cursor.isClosed()) {
                cursor.close();
            }
        } catch (IllegalStateException e) {
            Log.e(TAG, "illegal state while closing cursor", e);
        }

        if (ret) {
            final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context);
            final boolean a80 = p.getBoolean(Preferences.PREFS_ALERT80, true);
            final boolean a100 = p.getBoolean(Preferences.PREFS_ALERT100, true);
            // check for alerts
            if ((a80 || a100) && plans != null && plans.size() > 0) {
                final long now = System.currentTimeMillis();
                int alert = 0;
                Plan alertPlan = null;
                int l = plans.size();
                for (int i = 0; i < l; i++) {
                    final Plan plan = plans.valueAt(i);
                    if (plan == null) {
                        continue;
                    }
                    if (plan.nextAlert > now) {
                        Log.d(TAG, "%s: skip alert until: %d now=%d", plan, plan.nextAlert, now);
                        continue;
                    }
                    int used = DataProvider.Plans.getUsed(plan.type, plan.limitType, plan.billedAmount,
                            plan.billedCost);
                    int usedRate = plan.limit > 0 ? (int) ((used * CallMeter.HUNDRED) / plan.limit) : 0;
                    if (a100 && usedRate >= CallMeter.HUNDRED) {
                        alert = usedRate;
                        alertPlan = plan;
                    } else if (a80 && alert < CallMeter.EIGHTY && usedRate >= CallMeter.EIGHTY) {
                        alert = usedRate;
                        alertPlan = plan;
                    }
                }
                if (alert > 0) {
                    final NotificationManager mNotificationMgr = (NotificationManager) context
                            .getSystemService(Context.NOTIFICATION_SERVICE);
                    final String t = String.format(context.getString(R.string.alerts_message), alertPlan.name,
                            alert);
                    NotificationCompat.Builder b = new NotificationCompat.Builder(context);
                    b.setSmallIcon(android.R.drawable.stat_notify_error);
                    b.setTicker(t);
                    b.setWhen(now);
                    b.setContentTitle(context.getString(R.string.alerts_title));
                    b.setContentText(t);
                    Intent i = new Intent(context, Plans.class);
                    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    b.setContentIntent(PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT));
                    mNotificationMgr.notify(0, b.build());
                    // set nextAlert to beginning of next day
                    Calendar cal = Calendar.getInstance();
                    cal.add(Calendar.DAY_OF_MONTH, 1);
                    cal.set(Calendar.HOUR_OF_DAY, 0);
                    cal.set(Calendar.MINUTE, 0);
                    cal.set(Calendar.SECOND, 0);
                    cal.set(Calendar.MILLISECOND, 0);
                    alertPlan.nextAlert = cal.getTimeInMillis();
                    final ContentValues cv = new ContentValues();
                    cv.put(DataProvider.Plans.NEXT_ALERT, alertPlan.nextAlert);
                    cr.update(DataProvider.Plans.CONTENT_URI, cv, DataProvider.Plans.ID + " = ?",
                            new String[] { String.valueOf(alertPlan.id) });
                }
            }
        }
        long end = System.currentTimeMillis();
        Log.i(TAG, "match(): ", end - start, "ms");
        return ret;
    }
}