com.csipsimple.db.DBProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.csipsimple.db.DBProvider.java

Source

/**
 * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
 * This file is part of CSipSimple.
 *
 *  CSipSimple 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.
 *  If you own a pjsip commercial license you can also redistribute it
 *  and/or modify it under the terms of the GNU Lesser General Public License
 *  as an android library.
 *
 *  CSipSimple 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 CSipSimple.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.csipsimple.db;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.provider.CallLog;
import android.support.v4.database.DatabaseUtilsCompat;
import android.text.TextUtils;

import com.csipsimple.api.SipManager;
import com.csipsimple.api.SipMessage;
import com.csipsimple.api.SipProfile;
import com.csipsimple.api.SipProfileState;
import com.csipsimple.db.DBAdapter.DatabaseHelper;
import com.csipsimple.models.Filter;
import com.csipsimple.utils.Log;
import com.csipsimple.utils.backup.BackupWrapper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class DBProvider extends ContentProvider {

    private DatabaseHelper mOpenHelper;
    private static final String UNKNOWN_URI_LOG = "Unknown URI ";

    // Ids for matcher
    private static final int ACCOUNTS = 1, ACCOUNTS_ID = 2;
    private static final int ACCOUNTS_STATUS = 3, ACCOUNTS_STATUS_ID = 4;
    private static final int CALLLOGS = 5, CALLLOGS_ID = 6;
    private static final int FILTERS = 7, FILTERS_ID = 8;
    private static final int MESSAGES = 9, MESSAGES_ID = 10;
    private static final int THREADS = 11, THREADS_ID = 12;

    /**
     * A UriMatcher instance
     */
    private static final UriMatcher URI_MATCHER;
    static {
        // Create and initialize URI matcher.
        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

        URI_MATCHER.addURI(SipManager.AUTHORITY, SipProfile.ACCOUNTS_TABLE_NAME, ACCOUNTS);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipProfile.ACCOUNTS_TABLE_NAME + "/#", ACCOUNTS_ID);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipProfile.ACCOUNTS_STATUS_TABLE_NAME, ACCOUNTS_STATUS);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipProfile.ACCOUNTS_STATUS_TABLE_NAME + "/#", ACCOUNTS_STATUS_ID);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipManager.CALLLOGS_TABLE_NAME, CALLLOGS);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipManager.CALLLOGS_TABLE_NAME + "/#", CALLLOGS_ID);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipManager.FILTERS_TABLE_NAME, FILTERS);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipManager.FILTERS_TABLE_NAME + "/#", FILTERS_ID);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipMessage.MESSAGES_TABLE_NAME, MESSAGES);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipMessage.MESSAGES_TABLE_NAME + "/#", MESSAGES_ID);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipMessage.THREAD_ALIAS, THREADS);
        URI_MATCHER.addURI(SipManager.AUTHORITY, SipMessage.THREAD_ALIAS + "/*", THREADS_ID);
    }

    public final static String[] ACCOUNT_FULL_PROJECTION = { SipProfile.FIELD_ID,
            // Application relative fields
            SipProfile.FIELD_ACTIVE, SipProfile.FIELD_WIZARD, SipProfile.FIELD_DISPLAY_NAME,
            // Custom datas
            SipProfile.FIELD_WIZARD_DATA,

            // Here comes pjsua_acc_config fields
            SipProfile.FIELD_PRIORITY, SipProfile.FIELD_ACC_ID, SipProfile.FIELD_REG_URI,
            SipProfile.FIELD_MWI_ENABLED, SipProfile.FIELD_PUBLISH_ENABLED, SipProfile.FIELD_REG_TIMEOUT,
            SipProfile.FIELD_KA_INTERVAL, SipProfile.FIELD_PIDF_TUPLE_ID, SipProfile.FIELD_FORCE_CONTACT,
            SipProfile.FIELD_ALLOW_CONTACT_REWRITE, SipProfile.FIELD_CONTACT_REWRITE_METHOD,
            SipProfile.FIELD_ALLOW_VIA_REWRITE, SipProfile.FIELD_ALLOW_SDP_NAT_REWRITE,
            SipProfile.FIELD_CONTACT_PARAMS, SipProfile.FIELD_CONTACT_URI_PARAMS, SipProfile.FIELD_TRANSPORT,
            SipProfile.FIELD_DEFAULT_URI_SCHEME, SipProfile.FIELD_USE_SRTP, SipProfile.FIELD_USE_ZRTP,
            SipProfile.FIELD_REG_DELAY_BEFORE_REFRESH,

            // RTP config
            SipProfile.FIELD_RTP_PORT, SipProfile.FIELD_RTP_PUBLIC_ADDR, SipProfile.FIELD_RTP_BOUND_ADDR,
            SipProfile.FIELD_RTP_ENABLE_QOS, SipProfile.FIELD_RTP_QOS_DSCP,

            // Proxy infos
            SipProfile.FIELD_PROXY, SipProfile.FIELD_REG_USE_PROXY,

            // And now cred_info since for now only one cred info can be managed
            // In future release a credential table should be created
            SipProfile.FIELD_REALM, SipProfile.FIELD_SCHEME, SipProfile.FIELD_USERNAME, SipProfile.FIELD_DATATYPE,
            SipProfile.FIELD_DATA,

            SipProfile.FIELD_AUTH_INITIAL_AUTH, SipProfile.FIELD_AUTH_ALGO,

            // CSipSimple specific
            SipProfile.FIELD_SIP_STACK, SipProfile.FIELD_VOICE_MAIL_NBR, SipProfile.FIELD_TRY_CLEAN_REGISTERS,
            SipProfile.FIELD_ANDROID_GROUP,

            // RFC 5626
            SipProfile.FIELD_USE_RFC5626, SipProfile.FIELD_RFC5626_INSTANCE_ID, SipProfile.FIELD_RFC5626_REG_ID,

            // Video
            SipProfile.FIELD_VID_IN_AUTO_SHOW, SipProfile.FIELD_VID_OUT_AUTO_TRANSMIT,

            // STUN, ICE, TURN
            SipProfile.FIELD_SIP_STUN_USE, SipProfile.FIELD_MEDIA_STUN_USE, SipProfile.FIELD_ICE_CFG_USE,
            SipProfile.FIELD_ICE_CFG_ENABLE, SipProfile.FIELD_TURN_CFG_USE, SipProfile.FIELD_TURN_CFG_ENABLE,
            SipProfile.FIELD_TURN_CFG_SERVER, SipProfile.FIELD_TURN_CFG_USER, SipProfile.FIELD_TURN_CFG_PASSWORD,

            SipProfile.FIELD_IPV6_MEDIA_USE, };
    public final static Class<?>[] ACCOUNT_FULL_PROJECTION_TYPES = { Long.class,

            Integer.class, String.class, String.class,

            String.class,

            Integer.class, String.class, String.class, Boolean.class, Integer.class, Integer.class, Integer.class,
            String.class, String.class, Integer.class, Integer.class, Integer.class, Integer.class, String.class,
            String.class, Integer.class, String.class, Integer.class, Integer.class, Integer.class,

            // RTP config
            Integer.class, String.class, String.class, Integer.class, Integer.class,

            // Proxy infos
            String.class, Integer.class,

            // Credentials
            String.class, String.class, String.class, Integer.class, String.class,

            Boolean.class, String.class,

            // CSipSimple specific
            Integer.class, String.class, Integer.class, String.class,

            // RFC 5626
            Integer.class, String.class, String.class,

            // Video
            Integer.class, Integer.class,

            // STUN, ICE, TURN
            Integer.class, Integer.class, Integer.class, Integer.class, Integer.class, Integer.class, String.class,
            String.class, String.class,

            // IPV6
            Integer.class };

    private static final String[] CALL_LOG_FULL_PROJECTION = new String[] { CallLog.Calls._ID,
            CallLog.Calls.CACHED_NAME, CallLog.Calls.CACHED_NUMBER_LABEL, CallLog.Calls.CACHED_NUMBER_TYPE,
            CallLog.Calls.DATE, CallLog.Calls.DURATION, CallLog.Calls.NEW, CallLog.Calls.NUMBER, CallLog.Calls.TYPE,
            SipManager.CALLLOG_PROFILE_ID_FIELD, SipManager.CALLLOG_STATUS_CODE_FIELD,
            SipManager.CALLLOG_STATUS_TEXT_FIELD };

    private static final String[] MESSAGES_FULL_PROJECTION = new String[] { SipMessage.FIELD_ID,
            SipMessage.FIELD_FROM, SipMessage.FIELD_TO, SipMessage.FIELD_CONTACT, SipMessage.FIELD_BODY,
            SipMessage.FIELD_MIME_TYPE, SipMessage.FIELD_TYPE, SipMessage.FIELD_DATE, SipMessage.FIELD_STATUS,
            SipMessage.FIELD_READ, SipMessage.FIELD_FROM_FULL, };

    private static final String[] FILTERS_FULL_PROJECTION = new String[] { Filter._ID, Filter.FIELD_PRIORITY,
            Filter.FIELD_ACCOUNT, Filter.FIELD_MATCHES, Filter.FIELD_REPLACE, Filter.FIELD_ACTION, };

    private static final String THIS_FILE = "DBProvider";

    // Map active account id (id for sql settings database) with SipProfileState that contains stack id and other status infos
    private final Map<Long, ContentValues> profilesStatus = new HashMap<Long, ContentValues>();

    @Override
    public String getType(Uri uri) {
        switch (URI_MATCHER.match(uri)) {
        case ACCOUNTS:
            return SipProfile.ACCOUNT_CONTENT_TYPE;
        case ACCOUNTS_ID:
            return SipProfile.ACCOUNT_CONTENT_ITEM_TYPE;
        case ACCOUNTS_STATUS:
            return SipProfile.ACCOUNT_STATUS_CONTENT_TYPE;
        case ACCOUNTS_STATUS_ID:
            return SipProfile.ACCOUNT_STATUS_CONTENT_ITEM_TYPE;
        case CALLLOGS:
            return SipManager.CALLLOG_CONTENT_TYPE;
        case CALLLOGS_ID:
            return SipManager.CALLLOG_CONTENT_ITEM_TYPE;
        case FILTERS:
            return SipManager.FILTER_CONTENT_TYPE;
        case FILTERS_ID:
            return SipManager.FILTER_CONTENT_ITEM_TYPE;
        case MESSAGES:
            return SipMessage.MESSAGE_CONTENT_TYPE;
        case MESSAGES_ID:
            return SipMessage.MESSAGE_CONTENT_ITEM_TYPE;
        case THREADS:
            return SipMessage.MESSAGE_CONTENT_TYPE;
        case THREADS_ID:
            return SipMessage.MESSAGE_CONTENT_ITEM_TYPE;
        default:
            throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri);
        }
    }

    @Override
    public boolean onCreate() {
        mOpenHelper = new DatabaseHelper(getContext());
        // Assumes that any failures will be reported by a thrown exception.
        return true;
    }

    private static final String MESSAGES_THREAD_SELECTION = "(" + SipMessage.FIELD_FROM + "=? AND "
            + SipMessage.FIELD_TYPE + " IN (" + Integer.toString(SipMessage.MESSAGE_TYPE_INBOX) + ") )" + " OR "
            + "(" + SipMessage.FIELD_TO + "=? AND " + SipMessage.FIELD_TYPE + " IN ("
            + Integer.toString(SipMessage.MESSAGE_TYPE_QUEUED) + ", "
            + Integer.toString(SipMessage.MESSAGE_TYPE_FAILED) + ", "
            + Integer.toString(SipMessage.MESSAGE_TYPE_SENT) + ") )";

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        String finalWhere;
        int count = 0;
        int matched = URI_MATCHER.match(uri);
        Uri regUri = uri;

        List<String> possibles = getPossibleFieldsForType(matched);
        checkSelection(possibles, where);

        ArrayList<Long> oldRegistrationsAccounts = null;

        switch (matched) {
        case ACCOUNTS:
            count = db.delete(SipProfile.ACCOUNTS_TABLE_NAME, where, whereArgs);
            break;
        case ACCOUNTS_ID:
            finalWhere = DatabaseUtilsCompat
                    .concatenateWhere(SipProfile.FIELD_ID + " = " + ContentUris.parseId(uri), where);
            count = db.delete(SipProfile.ACCOUNTS_TABLE_NAME, finalWhere, whereArgs);
            break;
        case CALLLOGS:
            count = db.delete(SipManager.CALLLOGS_TABLE_NAME, where, whereArgs);
            break;
        case CALLLOGS_ID:
            finalWhere = DatabaseUtilsCompat.concatenateWhere(CallLog.Calls._ID + " = " + ContentUris.parseId(uri),
                    where);
            count = db.delete(SipManager.CALLLOGS_TABLE_NAME, finalWhere, whereArgs);
            break;
        case FILTERS:
            count = db.delete(SipManager.FILTERS_TABLE_NAME, where, whereArgs);
            break;
        case FILTERS_ID:
            finalWhere = DatabaseUtilsCompat.concatenateWhere(Filter._ID + " = " + ContentUris.parseId(uri), where);
            count = db.delete(SipManager.FILTERS_TABLE_NAME, finalWhere, whereArgs);
            break;
        case MESSAGES:
            count = db.delete(SipMessage.MESSAGES_TABLE_NAME, where, whereArgs);
            break;
        case MESSAGES_ID:
            finalWhere = DatabaseUtilsCompat
                    .concatenateWhere(SipMessage.FIELD_ID + " = " + ContentUris.parseId(uri), where);
            count = db.delete(SipMessage.MESSAGES_TABLE_NAME, finalWhere, whereArgs);
            break;
        case THREADS_ID:
            String from = uri.getLastPathSegment();
            if (!TextUtils.isEmpty(from)) {
                count = db.delete(SipMessage.MESSAGES_TABLE_NAME, MESSAGES_THREAD_SELECTION,
                        new String[] { from, from });
            } else {
                count = 0;
            }
            regUri = SipMessage.MESSAGE_URI;
            break;
        case ACCOUNTS_STATUS:
            oldRegistrationsAccounts = new ArrayList<Long>();
            synchronized (profilesStatus) {
                for (Long accId : profilesStatus.keySet()) {
                    oldRegistrationsAccounts.add(accId);
                }
                profilesStatus.clear();
            }
            break;
        case ACCOUNTS_STATUS_ID:
            long id = ContentUris.parseId(uri);
            synchronized (profilesStatus) {
                profilesStatus.remove(id);
            }
            break;
        default:
            throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri);
        }

        getContext().getContentResolver().notifyChange(regUri, null);

        if (matched == ACCOUNTS_ID || matched == ACCOUNTS_STATUS_ID) {
            long rowId = ContentUris.parseId(uri);
            if (rowId >= 0) {
                if (matched == ACCOUNTS_ID) {
                    broadcastAccountDelete(rowId);
                } else if (matched == ACCOUNTS_STATUS_ID) {
                    broadcastRegistrationChange(rowId);
                }
            }
        }
        if (matched == FILTERS || matched == FILTERS_ID) {
            Filter.resetCache();
        }
        if (matched == ACCOUNTS_STATUS && oldRegistrationsAccounts != null) {
            for (Long accId : oldRegistrationsAccounts) {
                if (accId != null) {
                    broadcastRegistrationChange(accId);
                }
            }
        }

        return count;
    }

    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        int matched = URI_MATCHER.match(uri);
        String matchedTable = null;
        Uri baseInsertedUri = null;

        switch (matched) {
        case ACCOUNTS:
        case ACCOUNTS_ID:
            matchedTable = SipProfile.ACCOUNTS_TABLE_NAME;
            baseInsertedUri = SipProfile.ACCOUNT_ID_URI_BASE;
            break;
        case CALLLOGS:
        case CALLLOGS_ID:
            matchedTable = SipManager.CALLLOGS_TABLE_NAME;
            baseInsertedUri = SipManager.CALLLOG_ID_URI_BASE;
            break;
        case FILTERS:
        case FILTERS_ID:
            matchedTable = SipManager.FILTERS_TABLE_NAME;
            baseInsertedUri = SipManager.FILTER_ID_URI_BASE;
            break;
        case MESSAGES:
        case MESSAGES_ID:
            matchedTable = SipMessage.MESSAGES_TABLE_NAME;
            baseInsertedUri = SipMessage.MESSAGE_ID_URI_BASE;
            break;
        case ACCOUNTS_STATUS_ID:
            long id = ContentUris.parseId(uri);
            synchronized (profilesStatus) {
                SipProfileState ps = new SipProfileState();
                if (profilesStatus.containsKey(id)) {
                    ContentValues currentValues = profilesStatus.get(id);
                    ps.createFromContentValue(currentValues);
                }
                ps.createFromContentValue(initialValues);
                ContentValues cv = ps.getAsContentValue();
                cv.put(SipProfileState.ACCOUNT_ID, id);
                profilesStatus.put(id, cv);
                Log.d(THIS_FILE, "Added " + cv);
            }
            getContext().getContentResolver().notifyChange(uri, null);
            return uri;
        default:
            break;
        }

        if (matchedTable == null) {
            throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri);
        }

        ContentValues values;

        if (initialValues != null) {
            values = new ContentValues(initialValues);
        } else {
            values = new ContentValues();
        }

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();

        long rowId = db.insert(matchedTable, null, values);

        // If the insert succeeded, the row ID exists.
        if (rowId >= 0) {
            // TODO : for inserted account register it here

            Uri retUri = ContentUris.withAppendedId(baseInsertedUri, rowId);
            getContext().getContentResolver().notifyChange(retUri, null);

            if (matched == ACCOUNTS || matched == ACCOUNTS_ID) {
                broadcastAccountChange(rowId);
            }
            if (matched == CALLLOGS || matched == CALLLOGS_ID) {
                db.delete(SipManager.CALLLOGS_TABLE_NAME,
                        CallLog.Calls._ID + " IN " + "(SELECT " + CallLog.Calls._ID + " FROM "
                                + SipManager.CALLLOGS_TABLE_NAME + " ORDER BY " + CallLog.Calls.DEFAULT_SORT_ORDER
                                + " LIMIT -1 OFFSET 500)",
                        null);
            }
            if (matched == ACCOUNTS_STATUS || matched == ACCOUNTS_STATUS_ID) {
                broadcastRegistrationChange(rowId);
            }
            if (matched == FILTERS || matched == FILTERS_ID) {
                Filter.resetCache();
            }

            return retUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

        // Constructs a new query builder and sets its table name
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String finalSortOrder = sortOrder;
        String[] finalSelectionArgs = selectionArgs;
        String finalGrouping = null;
        String finalHaving = null;
        int type = URI_MATCHER.match(uri);

        Uri regUri = uri;

        // Security check to avoid data retrieval from outside
        int remoteUid = Binder.getCallingUid();
        int selfUid = android.os.Process.myUid();
        if (remoteUid != selfUid) {
            if (type == ACCOUNTS || type == ACCOUNTS_ID) {
                for (String proj : projection) {
                    if (proj.toLowerCase().contains(SipProfile.FIELD_DATA) || proj.toLowerCase().contains("*")) {
                        throw new SecurityException("Password not readable from external apps");
                    }
                }
            }
        }
        // Security check to avoid project of invalid fields or lazy projection
        List<String> possibles = getPossibleFieldsForType(type);
        if (possibles == null) {
            throw new SecurityException("You are asking wrong values " + type);
        }
        checkProjection(possibles, projection);
        checkSelection(possibles, selection);

        Cursor c;
        long id;
        switch (type) {
        case ACCOUNTS:
            qb.setTables(SipProfile.ACCOUNTS_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = SipProfile.FIELD_PRIORITY + " ASC";
            }
            break;
        case ACCOUNTS_ID:
            qb.setTables(SipProfile.ACCOUNTS_TABLE_NAME);
            qb.appendWhere(SipProfile.FIELD_ID + "=?");
            finalSelectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
                    new String[] { uri.getLastPathSegment() });
            break;
        case CALLLOGS:
            qb.setTables(SipManager.CALLLOGS_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = CallLog.Calls.DATE + " DESC";
            }
            break;
        case CALLLOGS_ID:
            qb.setTables(SipManager.CALLLOGS_TABLE_NAME);
            qb.appendWhere(CallLog.Calls._ID + "=?");
            finalSelectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
                    new String[] { uri.getLastPathSegment() });
            break;
        case FILTERS:
            qb.setTables(SipManager.FILTERS_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = Filter.DEFAULT_ORDER;
            }
            break;
        case FILTERS_ID:
            qb.setTables(SipManager.FILTERS_TABLE_NAME);
            qb.appendWhere(Filter._ID + "=?");
            finalSelectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
                    new String[] { uri.getLastPathSegment() });
            break;
        case MESSAGES:
            qb.setTables(SipMessage.MESSAGES_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = SipMessage.FIELD_DATE + " DESC";
            }
            break;
        case MESSAGES_ID:
            qb.setTables(SipMessage.MESSAGES_TABLE_NAME);
            qb.appendWhere(SipMessage.FIELD_ID + "=?");
            finalSelectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
                    new String[] { uri.getLastPathSegment() });
            break;
        case THREADS:
            qb.setTables(SipMessage.MESSAGES_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = SipMessage.FIELD_DATE + " DESC";
            }
            projection = new String[] { "ROWID AS _id", SipMessage.FIELD_FROM, SipMessage.FIELD_FROM_FULL,
                    SipMessage.FIELD_TO,
                    "CASE " + "WHEN " + SipMessage.FIELD_FROM + "='SELF' THEN " + SipMessage.FIELD_TO + " WHEN "
                            + SipMessage.FIELD_FROM + "!='SELF' THEN " + SipMessage.FIELD_FROM
                            + " END AS message_ordering",
                    SipMessage.FIELD_BODY, "MAX(" + SipMessage.FIELD_DATE + ") AS " + SipMessage.FIELD_DATE,
                    "MIN(" + SipMessage.FIELD_READ + ") AS " + SipMessage.FIELD_READ,
                    //SipMessage.FIELD_READ,
                    "COUNT(" + SipMessage.FIELD_DATE + ") AS counter" };
            //qb.appendWhere(SipMessage.FIELD_TYPE + " in (" + SipMessage.MESSAGE_TYPE_INBOX
            //        + "," + SipMessage.MESSAGE_TYPE_SENT + ")");
            finalGrouping = "message_ordering";
            regUri = SipMessage.MESSAGE_URI;
            break;
        case THREADS_ID:
            qb.setTables(SipMessage.MESSAGES_TABLE_NAME);
            if (sortOrder == null) {
                finalSortOrder = SipMessage.FIELD_DATE + " DESC";
            }
            projection = new String[] { "ROWID AS _id", SipMessage.FIELD_FROM, SipMessage.FIELD_TO,
                    SipMessage.FIELD_BODY, SipMessage.FIELD_DATE, SipMessage.FIELD_MIME_TYPE, SipMessage.FIELD_TYPE,
                    SipMessage.FIELD_STATUS, SipMessage.FIELD_FROM_FULL };
            qb.appendWhere(MESSAGES_THREAD_SELECTION);
            String from = uri.getLastPathSegment();
            finalSelectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
                    new String[] { from, from });
            regUri = SipMessage.MESSAGE_URI;
            break;
        case ACCOUNTS_STATUS:
            synchronized (profilesStatus) {
                ContentValues[] cvs = new ContentValues[profilesStatus.size()];
                int i = 0;
                for (ContentValues ps : profilesStatus.values()) {
                    cvs[i] = ps;
                    i++;
                }
                c = getCursor(cvs);
            }
            if (c != null) {
                c.setNotificationUri(getContext().getContentResolver(), uri);
            }
            return c;
        case ACCOUNTS_STATUS_ID:
            id = ContentUris.parseId(uri);
            synchronized (profilesStatus) {
                ContentValues cv = profilesStatus.get(id);
                if (cv == null) {
                    return null;
                }
                c = getCursor(new ContentValues[] { cv });
            }
            c.setNotificationUri(getContext().getContentResolver(), uri);
            return c;
        default:
            throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri);
        }

        SQLiteDatabase db = mOpenHelper.getReadableDatabase();

        c = qb.query(db, projection, selection, finalSelectionArgs, finalGrouping, finalHaving, finalSortOrder);

        c.setNotificationUri(getContext().getContentResolver(), regUri);
        return c;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        String finalWhere;
        int matched = URI_MATCHER.match(uri);

        List<String> possibles = getPossibleFieldsForType(matched);
        checkSelection(possibles, where);

        switch (matched) {
        case ACCOUNTS:
            count = db.update(SipProfile.ACCOUNTS_TABLE_NAME, values, where, whereArgs);
            break;
        case ACCOUNTS_ID:
            finalWhere = DatabaseUtilsCompat
                    .concatenateWhere(SipProfile.FIELD_ID + " = " + ContentUris.parseId(uri), where);
            count = db.update(SipProfile.ACCOUNTS_TABLE_NAME, values, finalWhere, whereArgs);
            break;
        case CALLLOGS:
            count = db.update(SipManager.CALLLOGS_TABLE_NAME, values, where, whereArgs);
            break;
        case CALLLOGS_ID:
            finalWhere = DatabaseUtilsCompat.concatenateWhere(CallLog.Calls._ID + " = " + ContentUris.parseId(uri),
                    where);
            count = db.update(SipManager.CALLLOGS_TABLE_NAME, values, finalWhere, whereArgs);
            break;
        case FILTERS:
            count = db.update(SipManager.FILTERS_TABLE_NAME, values, where, whereArgs);
            break;
        case FILTERS_ID:
            finalWhere = DatabaseUtilsCompat.concatenateWhere(Filter._ID + " = " + ContentUris.parseId(uri), where);
            count = db.update(SipManager.FILTERS_TABLE_NAME, values, finalWhere, whereArgs);
            break;
        case MESSAGES:
            count = db.update(SipMessage.MESSAGES_TABLE_NAME, values, where, whereArgs);
            break;
        case MESSAGES_ID:
            finalWhere = DatabaseUtilsCompat
                    .concatenateWhere(SipMessage.FIELD_ID + " = " + ContentUris.parseId(uri), where);
            count = db.update(SipMessage.MESSAGES_TABLE_NAME, values, where, whereArgs);
            break;
        case ACCOUNTS_STATUS_ID:
            long id = ContentUris.parseId(uri);
            synchronized (profilesStatus) {
                SipProfileState ps = new SipProfileState();
                if (profilesStatus.containsKey(id)) {
                    ContentValues currentValues = profilesStatus.get(id);
                    ps.createFromContentValue(currentValues);
                }
                ps.createFromContentValue(values);
                ContentValues cv = ps.getAsContentValue();
                cv.put(SipProfileState.ACCOUNT_ID, id);
                profilesStatus.put(id, cv);
                Log.d(THIS_FILE, "Updated " + cv);
            }
            count = 1;
            break;
        default:
            throw new IllegalArgumentException(UNKNOWN_URI_LOG + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);

        long rowId = -1;
        if (matched == ACCOUNTS_ID || matched == ACCOUNTS_STATUS_ID) {
            rowId = ContentUris.parseId(uri);
        }
        if (rowId >= 0) {
            if (matched == ACCOUNTS_ID) {
                // Don't broadcast if we only changed wizard or only changed priority
                boolean doBroadcast = true;
                if (values.size() == 1) {
                    if (values.containsKey(SipProfile.FIELD_WIZARD)) {
                        doBroadcast = false;
                    } else if (values.containsKey(SipProfile.FIELD_PRIORITY)) {
                        doBroadcast = false;
                    }
                }
                if (doBroadcast) {
                    broadcastAccountChange(rowId);
                }
            } else if (matched == ACCOUNTS_STATUS_ID) {
                broadcastRegistrationChange(rowId);
            }
        }
        if (matched == FILTERS || matched == FILTERS_ID) {
            Filter.resetCache();
        }

        return count;
    }

    /**
     * Build a {@link Cursor} with a single row that contains all values
     * provided through the given {@link ContentValues}.
     */
    private Cursor getCursor(ContentValues[] contentValues) {
        if (contentValues.length > 0) {
            final Set<Entry<String, Object>> valueSet = contentValues[0].valueSet();
            int colSize = valueSet.size();
            final String[] keys = new String[colSize];

            int i = 0;
            for (Entry<String, Object> entry : valueSet) {
                keys[i] = entry.getKey();
                i++;
            }

            final MatrixCursor cursor = new MatrixCursor(keys);
            for (ContentValues cv : contentValues) {
                final Object[] values = new Object[colSize];
                i = 0;
                for (Entry<String, Object> entry : cv.valueSet()) {
                    values[i] = entry.getValue();
                    i++;
                }
                cursor.addRow(values);
            }
            return cursor;
        }
        return null;
    }

    /**
     * Broadcast the fact that account config has changed
     * @param accountId
     */
    private void broadcastAccountChange(long accountId) {
        Intent publishIntent = new Intent(SipManager.ACTION_SIP_ACCOUNT_CHANGED);
        publishIntent.putExtra(SipProfile.FIELD_ID, accountId);
        getContext().sendBroadcast(publishIntent);
        BackupWrapper.getInstance(getContext()).dataChanged();
    }

    /**
     * Broadcast the fact that account config has been deleted
     * @param accountId
     */
    private void broadcastAccountDelete(long accountId) {
        Intent publishIntent = new Intent(SipManager.ACTION_SIP_ACCOUNT_DELETED);
        publishIntent.putExtra(SipProfile.FIELD_ID, accountId);
        getContext().sendBroadcast(publishIntent);
        BackupWrapper.getInstance(getContext()).dataChanged();
    }

    /**
     * Broadcast the fact that registration / adding status changed
     * @param accountId the id of the account
     */
    private void broadcastRegistrationChange(long accountId) {
        Intent publishIntent = new Intent(SipManager.ACTION_SIP_REGISTRATION_CHANGED);
        publishIntent.putExtra(SipProfile.FIELD_ID, accountId);
        getContext().sendBroadcast(publishIntent, SipManager.PERMISSION_USE_SIP);

    }

    private static List<String> getPossibleFieldsForType(int type) {
        List<String> possibles = null;
        switch (type) {
        case ACCOUNTS:
        case ACCOUNTS_ID:
            possibles = Arrays.asList(ACCOUNT_FULL_PROJECTION);
            break;
        case CALLLOGS:
        case CALLLOGS_ID:
            possibles = Arrays.asList(CALL_LOG_FULL_PROJECTION);
            break;
        case FILTERS:
        case FILTERS_ID:
            possibles = Arrays.asList(FILTERS_FULL_PROJECTION);
            break;
        case MESSAGES:
        case MESSAGES_ID:
            possibles = Arrays.asList(MESSAGES_FULL_PROJECTION);
            break;
        case THREADS:
        case THREADS_ID:
            possibles = new ArrayList<String>();
            break;
        case ACCOUNTS_STATUS:
        case ACCOUNTS_STATUS_ID:
            possibles = new ArrayList<String>();
        default:
        }
        return possibles;
    }

    private static void checkSelection(List<String> possibles, String selection) {
        if (selection != null) {
            String cleanSelection = selection.toLowerCase();
            for (String field : possibles) {
                cleanSelection = cleanSelection.replace(field, "");
            }
            cleanSelection = cleanSelection.replaceAll(" in \\([0-9 ,]+\\)", "");
            cleanSelection = cleanSelection.replaceAll(" and ", "");
            cleanSelection = cleanSelection.replaceAll(" or ", "");
            cleanSelection = cleanSelection.replaceAll("[0-9]+", "");
            cleanSelection = cleanSelection.replaceAll("[=? ]", "");
            if (cleanSelection.length() > 0) {
                throw new SecurityException("You are selecting wrong thing " + cleanSelection);
            }
        }
    }

    private static void checkProjection(List<String> possibles, String[] projection) {
        if (projection != null) {
            // Ensure projection is valid
            for (String proj : projection) {
                proj = proj.replaceAll(" AS [a-zA-Z0-9_]+$", "");
                if (!possibles.contains(proj)) {
                    throw new SecurityException("You are asking wrong values " + proj);
                }
            }
        }
    }

}