/**
 * 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.voiceblue.phone.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.voiceblue.phone.api.SipManager;
import com.voiceblue.phone.api.SipMessage;
import com.voiceblue.phone.api.SipProfile;
import com.voiceblue.phone.api.SipProfileState;
import com.voiceblue.phone.db.DBAdapter.DatabaseHelper;
import com.voiceblue.phone.models.Filter;
import com.voiceblue.phone.utils.Log;
import com.voiceblue.phone.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);
                }
            }
        }
	}
	
}
