/**
* 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.models;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.SparseIntArray;
import com.csipsimple.R;
import com.csipsimple.api.SipManager;
import com.csipsimple.utils.Log;
import com.csipsimple.utils.bluetooth.BluetoothWrapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class Filter {
public static final String _ID = "_id";
public static final String FIELD_PRIORITY = "priority";
public static final String FIELD_ACCOUNT = "account";
public static final String FIELD_MATCHES = "matches";
public static final String FIELD_REPLACE = "replace";
public static final String FIELD_ACTION = "action";
public static final int ACTION_CAN_CALL = 0;
public static final int ACTION_CANT_CALL = 1;
public static final int ACTION_REPLACE = 2;
public static final int ACTION_DIRECTLY_CALL = 3;
public static final int ACTION_AUTO_ANSWER = 4;
public static final int MATCHER_STARTS = 0;
public static final int MATCHER_HAS_N_DIGIT = 1;
public static final int MATCHER_HAS_MORE_N_DIGIT = 2;
public static final int MATCHER_IS_EXACTLY = 3;
public static final int MATCHER_REGEXP = 4;
public static final int MATCHER_ENDS = 5;
public static final int MATCHER_ALL = 6;
public static final int MATCHER_CONTAINS = 7;
public static final int MATCHER_BLUETOOTH = 8;
public static final int MATCHER_CALLINFO_AUTOREPLY = 9;
public static final int REPLACE_PREFIX = 0;
public static final int REPLACE_MATCH_BY = 1;
public static final int REPLACE_ALL_BY = 2;
public static final int REPLACE_REGEXP = 3;
public static final int REPLACE_SUFFIX = 4;
public static final String[] FULL_PROJ = {
_ID,
FIELD_PRIORITY,
FIELD_MATCHES,
FIELD_REPLACE,
FIELD_ACTION
};
public static final Class<?>[] FULL_PROJ_TYPES = {
Integer.class,
Integer.class,
String.class,
String.class,
Integer.class
};
public static final String DEFAULT_ORDER = FIELD_PRIORITY + " asc";
private static final String BLUETOOTH_MATCHER_KEY = "###BLUETOOTH###";
private static final String CALLINFO_AUTOREPLY_MATCHER_KEY = "###CALLINFO_AUTOREPLY###";
private static final String THIS_FILE = "Filter";
public Integer id;
public Integer priority;
public Integer account;
public String matchPattern;
public Integer matchType;
public String replacePattern;
public Integer action;
public Filter() {
// Nothing to do
}
public Filter(Cursor c) {
super();
createFromDb(c);
}
public void createFromDb(Cursor c) {
ContentValues args = new ContentValues();
DatabaseUtils.cursorRowToContentValues(c, args);
createFromContentValue(args);
}
public void createFromContentValue(ContentValues args) {
Integer tmp_i;
String tmp_s;
tmp_i = args.getAsInteger(_ID);
if (tmp_i != null) {
id = tmp_i;
}
tmp_i = args.getAsInteger(FIELD_PRIORITY);
if (tmp_i != null) {
priority = tmp_i;
}
tmp_i = args.getAsInteger(FIELD_ACTION);
if (tmp_i != null) {
action = tmp_i;
}
tmp_s = args.getAsString(FIELD_MATCHES);
if (tmp_s != null) {
matchPattern = tmp_s;
}
tmp_s = args.getAsString(FIELD_REPLACE);
if (tmp_s != null) {
replacePattern = tmp_s;
}
tmp_i = args.getAsInteger(FIELD_ACCOUNT);
if(tmp_i != null) {
account = tmp_i;
}
}
public ContentValues getDbContentValues() {
ContentValues args = new ContentValues();
if(id != null){
args.put(_ID, id);
}
args.put(FIELD_ACCOUNT, account);
args.put(FIELD_MATCHES, matchPattern);
args.put(FIELD_REPLACE, replacePattern);
args.put(FIELD_ACTION, action);
args.put(FIELD_PRIORITY, priority);
return args;
}
public String getRepresentation(Context context) {
String[] matches_array = context.getResources().getStringArray(R.array.filters_type);
String[] replace_array = context.getResources().getStringArray(R.array.replace_type);
RegExpRepresentation m = getRepresentationForMatcher();
StringBuffer reprBuf = new StringBuffer();
reprBuf.append(matches_array[getPositionForMatcher(m.type)]);
if(m.type != MATCHER_BLUETOOTH &&
m.type != MATCHER_CALLINFO_AUTOREPLY &&
m.type != MATCHER_ALL) {
reprBuf.append(' ');
reprBuf.append(m.fieldContent);
}
if(!TextUtils.isEmpty(replacePattern) && action == ACTION_REPLACE) {
m = getRepresentationForReplace();
reprBuf.append('\n');
reprBuf.append(replace_array[getPositionForReplace(m.type)]);
reprBuf.append(' ');
reprBuf.append(m.fieldContent);
}
return reprBuf.toString();
}
private void logInvalidPattern(PatternSyntaxException e) {
Log.e(THIS_FILE, "Invalid pattern ", e);
}
private boolean patternMatches(Context ctxt, String number, Bundle extraHdr, boolean defaultValue) {
if(CALLINFO_AUTOREPLY_MATCHER_KEY.equals(matchPattern)) {
if(extraHdr != null &&
extraHdr.containsKey("Call-Info")) {
String hdrValue = extraHdr.getString("Call-Info");
if(hdrValue != null) {
hdrValue = hdrValue.trim();
}
if(!TextUtils.isEmpty(hdrValue) &&
"answer-after=0".equalsIgnoreCase(hdrValue)){
return true;
}
}
}else if(BLUETOOTH_MATCHER_KEY.equals(matchPattern)) {
return BluetoothWrapper.getInstance(ctxt).isBTHeadsetConnected();
}else {
try {
return Pattern.matches(matchPattern, number);
}catch(PatternSyntaxException e) {
logInvalidPattern(e);
}
}
return defaultValue;
}
/**
* Does the filter allows to call ?
* @param ctxt Application context
* @param number number to test
* @return true if we can call this number
*/
public boolean canCall(Context ctxt, String number) {
if(action == ACTION_CANT_CALL) {
return !patternMatches(ctxt, number, null, false);
}
return true;
}
/**
* Does the filter force to call ?
* @param ctxt Application context
* @param number number to test
* @return true if we must call this number
*/
public boolean mustCall(Context ctxt, String number) {
if(action == ACTION_DIRECTLY_CALL) {
return patternMatches(ctxt, number, null, false);
}
return false;
}
/**
* Should the filter avoid next filters ?
* @param ctxt Application context
* @param number number to test
* @return true if we should not process next filters
*/
public boolean stopProcessing(Context ctxt, String number) {
if(action == ACTION_CAN_CALL || action == ACTION_DIRECTLY_CALL) {
return patternMatches(ctxt, number, null, false);
}
return false;
}
/**
* Does the filter auto answer a call ?
* @param ctxt Application context
* @param number number to test
* @return true if the call should be auto-answered
*/
public boolean autoAnswer(Context ctxt, String number, Bundle extraHdr) {
if(action == ACTION_AUTO_ANSWER) {
return patternMatches(ctxt, number, extraHdr, false);
}
return false;
}
/**
* Rewrite the number with this filter rule
* @param number the number to rewrite
* @return the rewritten number
*/
public String rewrite(String number) {
if(action == ACTION_REPLACE) {
try {
Pattern pattern = Pattern.compile(matchPattern);
Matcher matcher = pattern.matcher(number);
return matcher.replaceAll(replacePattern);
}catch(PatternSyntaxException e) {
logInvalidPattern(e);
}catch(ArrayIndexOutOfBoundsException e) {
Log.e(THIS_FILE, "Out of bounds ", e);
}
}
return number;
}
//Utilities functions
private static int getForPosition(SparseIntArray positions, Integer key) {
return positions.get(key);
}
private static int getPositionFor(SparseIntArray positions, Integer value) {
if(value != null) {
int pos = positions.indexOfValue(value);
if(pos >= 0) {
return pos;
}
}
return 0;
}
/**
* Available actions
*/
private final static SparseIntArray FILTER_ACTION_POS = new SparseIntArray();
static {
FILTER_ACTION_POS.put(0, ACTION_CANT_CALL);
FILTER_ACTION_POS.put(1, ACTION_REPLACE);
FILTER_ACTION_POS.put(2, ACTION_CAN_CALL);
FILTER_ACTION_POS.put(3, ACTION_DIRECTLY_CALL);
FILTER_ACTION_POS.put(4, ACTION_AUTO_ANSWER);
};
public static int getActionForPosition(Integer selectedItemPosition) {
return getForPosition(FILTER_ACTION_POS, selectedItemPosition);
}
public static int getPositionForAction(Integer selectedAction) {
return getPositionFor(FILTER_ACTION_POS, selectedAction);
}
/**
* Available matches patterns
*/
private final static SparseIntArray MATCHER_TYPE_POS = new SparseIntArray();
static {
MATCHER_TYPE_POS.put(0, MATCHER_STARTS);
MATCHER_TYPE_POS.put(1, MATCHER_ENDS);
MATCHER_TYPE_POS.put(2, MATCHER_CONTAINS);
MATCHER_TYPE_POS.put(3, MATCHER_ALL);
MATCHER_TYPE_POS.put(4, MATCHER_HAS_N_DIGIT);
MATCHER_TYPE_POS.put(5, MATCHER_HAS_MORE_N_DIGIT);
MATCHER_TYPE_POS.put(6, MATCHER_IS_EXACTLY);
MATCHER_TYPE_POS.put(7, MATCHER_REGEXP);
MATCHER_TYPE_POS.put(8, MATCHER_BLUETOOTH);
MATCHER_TYPE_POS.put(9, MATCHER_CALLINFO_AUTOREPLY);
};
public static int getMatcherForPosition(Integer selectedItemPosition) {
return getForPosition(MATCHER_TYPE_POS, selectedItemPosition);
}
public static int getPositionForMatcher(Integer selectedAction) {
return getPositionFor(MATCHER_TYPE_POS, selectedAction);
}
private final static SparseIntArray REPLACE_TYPE_POS = new SparseIntArray();
static {
REPLACE_TYPE_POS.put(0, REPLACE_PREFIX);
REPLACE_TYPE_POS.put(1, REPLACE_SUFFIX);
REPLACE_TYPE_POS.put(2, REPLACE_MATCH_BY);
REPLACE_TYPE_POS.put(3, REPLACE_ALL_BY);
REPLACE_TYPE_POS.put(4, REPLACE_REGEXP);
};
public static int getReplaceForPosition(Integer selectedItemPosition) {
return getForPosition(REPLACE_TYPE_POS, selectedItemPosition);
}
public static int getPositionForReplace(Integer selectedAction) {
return getPositionFor(REPLACE_TYPE_POS, selectedAction);
}
/**
* Represent a typed regexp
* Utility for visualisation of regexp (typed, for example start with, number of digit etc etc)
* @author r3gis3r
*
*/
public static final class RegExpRepresentation {
public Integer type;
public String fieldContent;
}
/**
* Set matches field according to a RegExpRepresentation (for UI display)
* @param representation the regexp representation
*/
public void setMatcherRepresentation(RegExpRepresentation representation) {
matchType = representation.type;
switch(representation.type) {
case MATCHER_STARTS:
matchPattern = "^"+Pattern.quote(representation.fieldContent)+"(.*)$";
break;
case MATCHER_ENDS:
matchPattern = "^(.*)"+Pattern.quote(representation.fieldContent)+"$";
break;
case MATCHER_CONTAINS:
matchPattern = "^(.*)"+Pattern.quote(representation.fieldContent)+"(.*)$";
break;
case MATCHER_ALL:
matchPattern = "^(.*)$";
break;
case MATCHER_HAS_N_DIGIT:
//TODO ... we should probably test the fieldContent type to ensure it's well digits...
matchPattern = "^(\\d{"+representation.fieldContent+"})$";
break;
case MATCHER_HAS_MORE_N_DIGIT:
//TODO ... we should probably test the fieldContent type to ensure it's well digits...
matchPattern = "^(\\d{"+representation.fieldContent+",})$";
break;
case MATCHER_IS_EXACTLY:
matchPattern = "^("+Pattern.quote(representation.fieldContent)+")$";
break;
case MATCHER_BLUETOOTH:
matchPattern = BLUETOOTH_MATCHER_KEY;
break;
case MATCHER_CALLINFO_AUTOREPLY:
matchPattern = CALLINFO_AUTOREPLY_MATCHER_KEY;
break;
case MATCHER_REGEXP:
default:
matchType = MATCHER_REGEXP; // In case hit default:
matchPattern = representation.fieldContent;
break;
}
}
/**
* Get the representation for current matcher
* @return RegExpReprestation object with type of matcher and content for matcher
* (content that should be shown in a text field for user)
*/
public RegExpRepresentation getRepresentationForMatcher() {
RegExpRepresentation repr = new RegExpRepresentation();
repr.type = matchType = MATCHER_REGEXP;
if(matchPattern == null) {
repr.type = matchType = MATCHER_STARTS;
repr.fieldContent = "";
return repr;
}else {
repr.fieldContent = matchPattern;
if( TextUtils.isEmpty(repr.fieldContent) ) {
repr.type = matchType = MATCHER_STARTS;
return repr;
}
}
if(matchPattern.equals(BLUETOOTH_MATCHER_KEY)) {
repr.type = matchType = MATCHER_BLUETOOTH;
}else if(matchPattern.equalsIgnoreCase(CALLINFO_AUTOREPLY_MATCHER_KEY)) {
repr.type = matchType = MATCHER_CALLINFO_AUTOREPLY;
}
Matcher matcher = null;
//Well... here we are... Some awful regexp matcher to test a regexp... Isn't it nice?
matcher = Pattern.compile("^\\^\\\\Q(.+)\\\\E\\(\\.\\*\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_STARTS;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\^\\(\\.\\*\\)\\\\Q(.+)\\\\E\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_ENDS;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\^\\(\\.\\*\\)\\\\Q(.+)\\\\E\\(\\.\\*\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_CONTAINS;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\^\\(\\.\\*\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_ALL;
repr.fieldContent = "";
return repr;
}
matcher = Pattern.compile("^\\^\\(\\\\d\\{([0-9]+)\\}\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_HAS_N_DIGIT;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\^\\(\\\\d\\{([0-9]+),\\}\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_HAS_MORE_N_DIGIT;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\^\\(\\\\Q(.+)\\\\E\\)\\$$").matcher(matchPattern);
if(matcher.matches()) {
repr.type = matchType = MATCHER_IS_EXACTLY;
repr.fieldContent = matcher.group(1);
return repr;
}
return repr;
}
public void setReplaceRepresentation(RegExpRepresentation representation){
switch(representation.type) {
case REPLACE_PREFIX:
replacePattern = representation.fieldContent+"$0";
break;
case REPLACE_SUFFIX:
replacePattern = "$0"+representation.fieldContent;
break;
case REPLACE_MATCH_BY:
switch (matchType)
{
case MATCHER_STARTS:
replacePattern = representation.fieldContent+"$1";
break;
case MATCHER_ENDS:
replacePattern = "$1"+representation.fieldContent;
break;
case MATCHER_CONTAINS:
replacePattern = "$1"+representation.fieldContent+"$2";
break;
default:
// Other types match the entire input
replacePattern = representation.fieldContent;
break;
}
break;
case REPLACE_ALL_BY:
//If $ is inside... well, next time will be considered as a regexp
replacePattern = representation.fieldContent;
break;
case REPLACE_REGEXP:
default:
replacePattern = representation.fieldContent;
break;
}
}
public RegExpRepresentation getRepresentationForReplace() {
RegExpRepresentation repr = new RegExpRepresentation();
repr.type = REPLACE_REGEXP;
if(replacePattern == null) {
repr.type = REPLACE_MATCH_BY;
repr.fieldContent = "";
if(action != null && action == ACTION_AUTO_ANSWER) {
repr.fieldContent = replacePattern;
}
return repr;
}else {
repr.fieldContent = replacePattern;
if( TextUtils.isEmpty(repr.fieldContent) ) {
repr.type = REPLACE_MATCH_BY;
return repr;
}
}
Matcher matcher = null;
matcher = Pattern.compile("^(.+)\\$0$").matcher(replacePattern);
if(matcher.matches()) {
repr.type = REPLACE_PREFIX;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^\\$0(.+)$").matcher(replacePattern);
if(matcher.matches()) {
repr.type = REPLACE_SUFFIX;
repr.fieldContent = matcher.group(1);
return repr;
}
switch (matchType)
{
case MATCHER_STARTS:
matcher = Pattern.compile("^(.*)\\$1$").matcher(replacePattern);
break;
case MATCHER_ENDS:
matcher = Pattern.compile("^\\$1(.*)$").matcher(replacePattern);
break;
case MATCHER_CONTAINS:
matcher = Pattern.compile("^\\$1(.*)\\$2$").matcher(replacePattern);
break;
default:
// Other types match the entire input
matcher = Pattern.compile("^(.*)$").matcher(replacePattern);
break;
}
if(matcher.matches()) {
repr.type = REPLACE_MATCH_BY;
repr.fieldContent = matcher.group(1);
return repr;
}
matcher = Pattern.compile("^([^\\$]+)$").matcher(replacePattern);
if(matcher.matches()) {
repr.type = REPLACE_ALL_BY;
repr.fieldContent = matcher.group(1);
return repr;
}
return repr;
}
//Static utility method
public static boolean isCallableNumber(Context ctxt, long accountId, String number) {
boolean canCall = true;
List<Filter> filterList = getFiltersForAccount(ctxt, accountId);
for (Filter f : filterList) {
Log.d(THIS_FILE, "Test filter " + f.matchPattern);
canCall &= f.canCall(ctxt, number);
// Stop processing & rewrite
if (f.stopProcessing(ctxt, number)) {
return canCall;
}
number = f.rewrite(number);
}
return canCall;
}
public static boolean isMustCallNumber(Context ctxt, long accountId, String number) {
List<Filter> filterList = getFiltersForAccount(ctxt, accountId);
for (Filter f : filterList) {
if(f.mustCall(ctxt, number)) {
return true;
}
//Stop processing & rewrite
if(f.stopProcessing(ctxt, number)) {
return false;
}
number = f.rewrite(number);
}
return false;
}
/**
* Rewrite a phone number for use with an account.
*
* @param ctxt The application context.
* @param accountId The account id to use for outgoing call
* @param number The number to rewrite
* @return Rewritten number
*/
public static String rewritePhoneNumber(Context ctxt, long accountId, String number) {
List<Filter> filterList = getFiltersForAccount(ctxt, accountId);
for (Filter f : filterList) {
//Log.d(THIS_FILE, "RW > Test filter "+f.matches);
number = f.rewrite(number);
if(f.stopProcessing(ctxt, number)) {
return number;
}
}
return number;
}
public static int isAutoAnswerNumber(Context ctxt, long accountId, String number, Bundle extraHdr) {
List<Filter> filterList = getFiltersForAccount(ctxt, accountId);
for (Filter f : filterList) {
if (f.autoAnswer(ctxt, number, extraHdr)) {
if (TextUtils.isEmpty(f.replacePattern)) {
return 200;
}
try {
return Integer.parseInt(f.replacePattern);
} catch (NumberFormatException e) {
Log.e(THIS_FILE, "Invalid autoanswer code : " + f.replacePattern);
}
return 200;
}
// Stop processing & rewrite
if (f.stopProcessing(ctxt, number)) {
return 0;
}
number = f.rewrite(number);
}
return 0;
}
// Helpers static factory
public static Filter getFilterFromDbId(Context ctxt, long filterId, String[] projection) {
Filter filter = new Filter();
if(filterId >= 0) {
Cursor c = ctxt.getContentResolver().query(ContentUris.withAppendedId(SipManager.FILTER_ID_URI_BASE, filterId),
projection, null, null, null);
if(c != null) {
try {
if(c.getCount() > 0) {
c.moveToFirst();
filter = new Filter(c);
}
}catch(Exception e) {
Log.e(THIS_FILE, "Something went wrong while retrieving the account", e);
} finally {
c.close();
}
}
}
return filter;
}
private static Map<Long, List<Filter>> FILTERS_PER_ACCOUNT = new HashMap<Long, List<Filter>>();
private static List<Filter> getFiltersForAccount(Context ctxt, long accountId){
if (!FILTERS_PER_ACCOUNT.containsKey(accountId)) {
ArrayList<Filter> aList = new ArrayList<Filter>();
Cursor c = getFiltersCursorForAccount(ctxt, accountId);
if (c != null) {
try {
if (c.moveToFirst()) {
do {
aList.add(new Filter(c));
} while (c.moveToNext());
}
} catch (Exception e) {
Log.e(THIS_FILE, "Error on looping over sip profiles", e);
} finally {
c.close();
}
}
FILTERS_PER_ACCOUNT.put(accountId, aList);
}
return FILTERS_PER_ACCOUNT.get(accountId);
}
public static void resetCache() {
FILTERS_PER_ACCOUNT = new HashMap<Long, List<Filter>>();
}
public static Cursor getFiltersCursorForAccount(Context ctxt, long accountId) {
return ctxt.getContentResolver().query(SipManager.FILTER_URI, FULL_PROJ, FIELD_ACCOUNT+"=?", new String[]{Long.toString(accountId)}, DEFAULT_ORDER);
}
}