Android Open Source - callmeter Rule Matcher






From Project

Back to project page callmeter.

License

The source code is released under:

GNU General Public License

If you think the Android project callmeter listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2009-2013 Felix Bechstein
 * //from  w w w .ja va  2s. c om
 * 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.HUNDRET, "<", limit);
                        return limit - billedCost * CallMeter.HUNDRET;
                    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 (time % nl == 0 || nl == 1) {
                return time;
            }
            // round up to next full slot
            return ((time / 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.HUNDRET;
            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.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.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.HUNDRET) / plan.limit)
                            : 0;
                    if (a100 && usedRate >= CallMeter.HUNDRET) {
                        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;
    }
}




Java Source Code List

android.preference.DatePreference.java
com.actionbarsherlock.BuildConfig.java
de.ub0r.android.callmeter.Ads.java
de.ub0r.android.callmeter.CallMeter.java
de.ub0r.android.callmeter.TrackingUtils.java
de.ub0r.android.callmeter.data.DataProvider.java
de.ub0r.android.callmeter.data.Device.java
de.ub0r.android.callmeter.data.ExportProvider.java
de.ub0r.android.callmeter.data.LogRunnerReceiver.java
de.ub0r.android.callmeter.data.LogRunnerService.java
de.ub0r.android.callmeter.data.NameCache.java
de.ub0r.android.callmeter.data.NameLoader.java
de.ub0r.android.callmeter.data.RuleMatcher.java
de.ub0r.android.callmeter.data.SysClassNet.java
de.ub0r.android.callmeter.ui.AboutActivity.java
de.ub0r.android.callmeter.ui.AddLogActivity.java
de.ub0r.android.callmeter.ui.AskForPlan.java
de.ub0r.android.callmeter.ui.Common.java
de.ub0r.android.callmeter.ui.HelpActivity.java
de.ub0r.android.callmeter.ui.IntroActivity.java
de.ub0r.android.callmeter.ui.LogsFragment.java
de.ub0r.android.callmeter.ui.PlansFragment.java
de.ub0r.android.callmeter.ui.Plans.java
de.ub0r.android.callmeter.ui.TrackingActivity.java
de.ub0r.android.callmeter.ui.TrackingSherlockActivity.java
de.ub0r.android.callmeter.ui.TrackingSherlockFragmentActivity.java
de.ub0r.android.callmeter.ui.TrackingSherlockPreferenceActivity.java
de.ub0r.android.callmeter.ui.prefs.BillModeListPreference.java
de.ub0r.android.callmeter.ui.prefs.CV2EditTextPreference.java
de.ub0r.android.callmeter.ui.prefs.CVBillModePreference.java
de.ub0r.android.callmeter.ui.prefs.CVCheckBoxPreference.java
de.ub0r.android.callmeter.ui.prefs.CVDatePreference.java
de.ub0r.android.callmeter.ui.prefs.CVEditTextPreference.java
de.ub0r.android.callmeter.ui.prefs.CVListPreference.java
de.ub0r.android.callmeter.ui.prefs.HourGroupEdit.java
de.ub0r.android.callmeter.ui.prefs.HourGroups.java
de.ub0r.android.callmeter.ui.prefs.NumberGroupEdit.java
de.ub0r.android.callmeter.ui.prefs.NumberGroups.java
de.ub0r.android.callmeter.ui.prefs.PlanEdit.java
de.ub0r.android.callmeter.ui.prefs.Plans.java
de.ub0r.android.callmeter.ui.prefs.PreferencesImport.java
de.ub0r.android.callmeter.ui.prefs.PreferencesPlain.java
de.ub0r.android.callmeter.ui.prefs.PreferencesRules.java
de.ub0r.android.callmeter.ui.prefs.Preferences.java
de.ub0r.android.callmeter.ui.prefs.RuleEdit.java
de.ub0r.android.callmeter.ui.prefs.Rules.java
de.ub0r.android.callmeter.ui.prefs.SimplePreferencesChild.java
de.ub0r.android.callmeter.ui.prefs.SimplePreferences.java
de.ub0r.android.callmeter.ui.prefs.UpDownPreference.java
de.ub0r.android.callmeter.ui.prefs.UpdateListener.java
de.ub0r.android.callmeter.widget.LogsAppWidgetConfigure.java
de.ub0r.android.callmeter.widget.LogsAppWidgetProvider.java
de.ub0r.android.callmeter.widget.StatsAppWidgetConfigure.java
de.ub0r.android.callmeter.widget.StatsAppWidgetProvider.java
yuku.ambilwarna.AmbilWarnaDialog.java
yuku.ambilwarna.AmbilWarnaKotak.java