/* * Copyright (C) 2013 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dev.dworks.apps.asecure.blacklist; import android.app.backup.BackupManager; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import dev.dworks.apps.asecure.blacklist.Telephony.Blacklist; public class BlacklistProvider extends ContentProvider { private static final String TAG = "BlacklistProvider"; private static final boolean DEBUG = true; private static final String DATABASE_NAME = "blacklist.db"; private static final int DATABASE_VERSION = 2; private static final String BLACKLIST_TABLE = "blacklist"; private static final String COLUMN_NORMALIZED = "normalized_number"; private static final int BL_ALL = 0; private static final int BL_ID = 1; private static final int BL_NUMBER = 2; private static final int BL_PHONE = 3; private static final int BL_MESSAGE = 4; private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sURIMatcher.addURI("blacklist", null, BL_ALL); sURIMatcher.addURI("blacklist", "#", BL_ID); sURIMatcher.addURI("blacklist", "bynumber/*", BL_NUMBER); sURIMatcher.addURI("blacklist", "phone", BL_PHONE); sURIMatcher.addURI("blacklist", "message", BL_MESSAGE); } private static class DatabaseHelper extends SQLiteOpenHelper { // Context to access resources with private Context mContext; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { // Set up the database schema db.execSQL("CREATE TABLE " + BLACKLIST_TABLE + "(_id INTEGER PRIMARY KEY," + "number TEXT," + "normalized_number TEXT," + "is_regex INTEGER," + "phone INTEGER DEFAULT 0," + "message INTEGER DEFAULT 0);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { // drop the uniqueness constraint that was present on the DB in V1 db.execSQL("ALTER TABLE " + BLACKLIST_TABLE + " RENAME TO " + BLACKLIST_TABLE + "_old;"); onCreate(db); db.execSQL("INSERT INTO " + BLACKLIST_TABLE + " SELECT * FROM " + BLACKLIST_TABLE + "_old;"); } } } private DatabaseHelper mOpenHelper; private BackupManager mBackupManager; @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); mBackupManager = new BackupManager(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(BLACKLIST_TABLE); // Generate the body of the query. int match = sURIMatcher.match(uri); if (DEBUG) { Log.v(TAG, "Query uri=" + uri + ", match=" + match); } switch (match) { case BL_ALL: break; case BL_ID: qb.appendWhere(Blacklist._ID + " = " + uri.getLastPathSegment()); break; case BL_NUMBER: { String number = normalizeNumber(uri.getLastPathSegment()); boolean regex = uri.getBooleanQueryParameter(Blacklist.REGEX_KEY, false); if (regex) { qb.appendWhere("\"" + number + "\" like " + COLUMN_NORMALIZED); } else { qb.appendWhere(COLUMN_NORMALIZED + " = \"" + number + "\""); } break; } case BL_PHONE: qb.appendWhere(Blacklist.PHONE_MODE + " != 0"); break; case BL_MESSAGE: qb.appendWhere(Blacklist.MESSAGE_MODE + " != 0"); break; default: Log.e(TAG, "query: invalid request: " + uri); return null; } if (TextUtils.isEmpty(sortOrder)) { sortOrder = Blacklist.DEFAULT_SORT_ORDER; } SQLiteDatabase db = mOpenHelper.getReadableDatabase(); Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); ret.setNotificationUri(getContext().getContentResolver(), uri); return ret; } @Override public String getType(Uri uri) { switch (sURIMatcher.match(uri)) { case BL_ALL: case BL_PHONE: case BL_MESSAGE: return "vnd.android.cursor.dir/blacklist-entry"; case BL_ID: case BL_NUMBER: return "vnd.android.cursor.item/blacklist-entry"; default: throw new IllegalArgumentException("Unknown URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { int match = sURIMatcher.match(uri); if (DEBUG) { Log.v(TAG, "Insert uri=" + uri + ", match=" + match); } if (match != BL_ALL) { return null; } ContentValues values = validateAndPrepareContentValues(initialValues, null); if (values == null) { Log.e(TAG, "Invalid insert values " + initialValues); return null; } SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long rowID = db.insert(BLACKLIST_TABLE, null, values); if (rowID <= 0) { return null; } if (DEBUG) Log.d(TAG, "inserted " + values + " rowID = " + rowID); notifyChange(); return ContentUris.withAppendedId(Blacklist.CONTENT_URI, rowID); } @Override public int delete(Uri uri, String where, String[] whereArgs) { int count = 0; int match = sURIMatcher.match(uri); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); if (DEBUG) { Log.v(TAG, "Delete uri=" + uri + ", match=" + match); } switch (match) { case BL_ALL: break; case BL_ID: if (where != null || whereArgs != null) { throw new UnsupportedOperationException( "Cannot delete URI " + uri + " with a where clause"); } where = Blacklist._ID + " = ?"; whereArgs = new String[] { uri.getLastPathSegment() }; break; case BL_NUMBER: if (where != null || whereArgs != null) { throw new UnsupportedOperationException( "Cannot delete URI " + uri + " with a where clause"); } where = COLUMN_NORMALIZED + " = ?"; whereArgs = new String[] { normalizeNumber(uri.getLastPathSegment()) }; break; default: throw new UnsupportedOperationException("Cannot delete that URI: " + uri); } count = db.delete(BLACKLIST_TABLE, where, whereArgs); if (DEBUG) Log.d(TAG, "delete result count " + count); if (count > 0) { notifyChange(); } return count; } @Override public int update(Uri uri, ContentValues initialValues, String where, String[] whereArgs) { int count = 0; int match = sURIMatcher.match(uri); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); if (DEBUG) { Log.v(TAG, "Update uri=" + uri + ", match=" + match); } String uriNumber = match == BL_NUMBER ? uri.getLastPathSegment() : null; ContentValues values = validateAndPrepareContentValues(initialValues, uriNumber); if (values == null) { Log.e(TAG, "Invalid update values " + initialValues); return 0; } switch (match) { case BL_ALL: count = db.update(BLACKLIST_TABLE, values, where, whereArgs); break; case BL_NUMBER: if (where != null || whereArgs != null) { throw new UnsupportedOperationException( "Cannot update URI " + uri + " with a where clause"); } db.beginTransaction(); try { count = db.update(BLACKLIST_TABLE, values, COLUMN_NORMALIZED + " = ?", new String[] { normalizeNumber(uriNumber) }); if (count == 0) { // convenience: fall back to insert if number wasn't present if (db.insert(BLACKLIST_TABLE, null, values) > 0) { count = 1; } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } break; case BL_ID: if (where != null || whereArgs != null) { throw new UnsupportedOperationException( "Cannot update URI " + uri + " with a where clause"); } count = db.update(BLACKLIST_TABLE, values, Blacklist._ID + " = ?", new String[] { uri.getLastPathSegment() }); break; default: throw new UnsupportedOperationException("Cannot update that URI: " + uri); } if (DEBUG) Log.d(TAG, "Update result count " + count); if (count > 0) { notifyChange(); } return count; } private ContentValues validateAndPrepareContentValues( ContentValues initialValues, String uriNumber) { ContentValues values = new ContentValues(initialValues); // apps are not supposed to mess with the normalized number or the regex state values.remove(COLUMN_NORMALIZED); values.remove(Blacklist.IS_REGEX); // on 'upsert', insert the number passed via URI if no other number was specified if (uriNumber != null && !values.containsKey(Blacklist.NUMBER)) { values.put(Blacklist.NUMBER, uriNumber); } if (values.containsKey(Telephony.Blacklist.NUMBER)) { String number = values.getAsString(Blacklist.NUMBER); if (TextUtils.isEmpty(number)) { return null; } String normalizedNumber = normalizeNumber(number); boolean isRegex = normalizedNumber.indexOf('%') >= 0 || normalizedNumber.indexOf('_') >= 0; values.put(COLUMN_NORMALIZED, normalizedNumber); values.put(Blacklist.IS_REGEX, isRegex ? 1 : 0); } return values; } private void notifyChange() { getContext().getContentResolver().notifyChange(Blacklist.CONTENT_URI, null); mBackupManager.dataChanged(); } // mostly a copy of PhoneNumberUtils.normalizeNumber, with the exception of // not converting characters and support for regex characters private String normalizeNumber(String number) { int len = number.length(); StringBuilder ret = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = number.charAt(i); // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) int digit = Character.digit(c, 10); if (digit != -1) { ret.append(digit); } else if (i == 0 && c == '+') { ret.append(c); } else if (c == '*') { // replace regex match-multiple character by SQL equivalent ret.append('%'); } else if (c == '.') { // replace regex-match-single character by SQL equivalent ret.append('_'); } } return ret.toString(); } }