diff options
author | Debashish Chatterjee <debashishc@google.com> | 2011-06-13 16:20:30 +0100 |
---|---|---|
committer | Debashish Chatterjee <debashishc@google.com> | 2011-06-15 15:57:09 +0100 |
commit | 52e8d24f8492116f0b49b147576ce13a5f913aa2 (patch) | |
tree | b79b1d8c38b062b545fabb0ac6b15eb9798ca2d5 /src/com/android/providers | |
parent | 59f6477e10203617f504857f7e9aee2fda393f4f (diff) | |
download | packages_providers_ContactsProvider-52e8d24f8492116f0b49b147576ce13a5f913aa2.tar.gz packages_providers_ContactsProvider-52e8d24f8492116f0b49b147576ce13a5f913aa2.tar.bz2 packages_providers_ContactsProvider-52e8d24f8492116f0b49b147576ce13a5f913aa2.zip |
Voicemail provider implementation within ContactsProvider.
- New voicemail provider class
- all voicemail operations restricted to only 'voicemail' call type.
- new voicemail permissions (currently defined in the manifest file
itself)
Change-Id: I32b916c5b4a53e93bafbecf7df7bee3f5e27fee6
Diffstat (limited to 'src/com/android/providers')
8 files changed, 819 insertions, 1 deletions
diff --git a/src/com/android/providers/contacts/ProjectionMap.java b/src/com/android/providers/contacts/ProjectionMap.java index 56198b84..f4c76d61 100644 --- a/src/com/android/providers/contacts/ProjectionMap.java +++ b/src/com/android/providers/contacts/ProjectionMap.java @@ -56,7 +56,7 @@ public class ProjectionMap extends HashMap<String, String> { } - public String[] mColumns; + private String[] mColumns; public static Builder builder() { return new Builder(); diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java new file mode 100644 index 00000000..d5597583 --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailContentProvider.java @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts; + +import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; +import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.provider.CallLog.Calls; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Voicemails; +import android.util.Log; + +import com.android.providers.contacts.ContactsDatabaseHelper.Tables; +import com.android.providers.contacts.ContactsDatabaseHelper.Views; +import com.android.providers.contacts.util.CloseUtils; +import com.android.providers.contacts.util.TypedUriMatcherImpl; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// TODO: Restrict access to only voicemail columns (i.e no access to call_log +// specific fields) +// TODO: Port unit tests from perforce. +/** + * An implementation of the Voicemail content provider. + */ +public class VoicemailContentProvider extends ContentProvider { + private static final String TAG = "VoicemailContentProvider"; + + /** The private directory in which to store the data associated with the voicemail. */ + private static final String DATA_DIRECTORY = "voicemail-data"; + + private static final String[] MIME_TYPE_ONLY_PROJECTION = new String[] { Voicemails.MIME_TYPE }; + private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA }; + private static final String VOICEMAILS_TABLE_NAME = Tables.CALLS; + + // Voicemail projection map + private static final ProjectionMap sVoicemailProjectionMap = new ProjectionMap.Builder() + .add(Voicemails._ID) + .add(Voicemails.NUMBER) + .add(Voicemails.DATE) + .add(Voicemails.DURATION) + .add(Voicemails.NEW) + .add(Voicemails.STATE) + .add(Voicemails.SOURCE_DATA) + .add(Voicemails.SOURCE_PACKAGE) + .add(Voicemails.HAS_CONTENT) + .add(Voicemails.MIME_TYPE) + .add(Voicemails._DATA) + .build(); + private ContentResolver mContentResolver; + private ContactsDatabaseHelper mDbHelper; + + @Override + public boolean onCreate() { + Context context = context(); + + mContentResolver = context.getContentResolver(); + mDbHelper = ContactsDatabaseHelper.getInstance(context); + + return true; + } + + /*package for testing*/ Context context() { + return getContext(); + } + + @Override + public String getType(Uri uri) { + UriData uriData = null; + try { + uriData = createUriData(uri); + } catch (IllegalArgumentException ignored) { + // Special case: for illegal URIs, we return null rather than thrown an exception. + return null; + } + // TODO: DB lookup for the mime type may cause strict mode exception for the callers of + // getType(). See if this could be avoided. + if (uriData.hasId()) { + // An individual voicemail - so lookup the MIME type in the db. + return lookupMimeType(uriData); + } + // Not an individual voicemail - must be a directory listing type. + return VoicemailContract.DIR_TYPE; + } + + /** Query the db for the MIME type of the given URI, called only from {@link #getType(Uri)}. */ + private String lookupMimeType(UriData uriData) { + Cursor cursor = null; + try { + // Use queryInternal, bypassing provider permission check. This is needed because + // getType() can be called from any application context (even without voicemail + // permissions) to know the MIME type of the URI. There is no security issue here as we + // do not expose any sensitive data through this interface. + cursor = queryInternal(uriData, MIME_TYPE_ONLY_PROJECTION, null, null, null); + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndex(Voicemails.MIME_TYPE)); + } + } finally { + CloseUtils.closeQuietly(cursor); + } + return null; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + checkHasOwnPermission(); + UriData uriData = createUriData(uri); + checkPackagePermission(uriData); + return queryInternal(uriData, projection, + concatenateClauses(selection, getPackageRestrictionClause()), selectionArgs, + sortOrder); + } + + /** + * Internal version of query(), that does not apply any provider restriction and lets the query + * flow through without such checks. + * <p> + * This is useful for internal queries when we do not worry about access permissions. + */ + private Cursor queryInternal(UriData uriData, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(Tables.CALLS); + qb.setProjectionMap(sVoicemailProjectionMap); + qb.setStrict(true); + + String combinedClause = concatenateClauses(selection, getWhereClause(uriData), + getCallTypeClause()); + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder); + if (c != null) { + c.setNotificationUri(mContentResolver, VoicemailContract.CONTENT_URI); + } + return c; + } + + private String getWhereClause(UriData uriData) { + return concatenateClauses( + (uriData.hasId() ? + getEqualityClause(Voicemails._ID, uriData.getId()) + : null), + (uriData.hasSourcePackage() ? + getEqualityClause(Voicemails.SOURCE_PACKAGE, uriData.getSourcePackage()) + : null)); + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] valuesArray) { + checkHasOwnPermission(); + // TODO: There is scope to optimize this method further. At the least we can avoid doing the + // extra work related to the calling provider and checking permissions. + UriData uriData = createUriData(uri); + int numInserted = 0; + for (ContentValues values : valuesArray) { + if (insertInternal(uriData, values, false) != null) { + numInserted++; + } + } + if (numInserted > 0) { + notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); + } + return numInserted; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + checkHasOwnPermission(); + return insertInternal(createUriData(uri), values, true); + } + + private Uri insertInternal(UriData uriData, ContentValues values, + boolean sendProviderChangedNotification) { + checkInsertSupported(uriData); + checkAndAddSourcePackageIntoValues(uriData, values); + + // "_data" column is used by base ContentProvider's openFileHelper() to determine filename + // when Input/Output stream is requested to be opened. + values.put(Voicemails._DATA, generateDataFile()); + + // call type is always voicemail. + values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE); + + SQLiteDatabase db = mDbHelper.getWritableDatabase(); + long rowId = db.insert(VOICEMAILS_TABLE_NAME, null, values); + if (rowId > 0) { + Uri newUri = ContentUris.withAppendedId( + Uri.withAppendedPath(VoicemailContract.CONTENT_URI_SOURCE, + values.getAsString(Voicemails.SOURCE_PACKAGE)), rowId); + + if (sendProviderChangedNotification) { + notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL, + Intent.ACTION_PROVIDER_CHANGED); + } else { + notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL); + } + // Populate the 'voicemail_uri' field to be used by the call_log provider. + updateVoicemailUri(newUri); + return newUri; + } + return null; + } + + private void updateVoicemailUri(Uri newUri) { + ContentValues values = new ContentValues(); + values.put(Calls.VOICEMAIL_URI, newUri.toString()); + update(newUri, values, null, null); + } + + private void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) { + // If content values don't contain the provider, calculate the right provider to use. + if (!values.containsKey(Voicemails.SOURCE_PACKAGE)) { + String provider = uriData.hasSourcePackage() ? + uriData.getSourcePackage() : getCallingPackage(); + values.put(Voicemails.SOURCE_PACKAGE, provider); + } + // If you put a provider in the URI and in the values, they must match. + if (uriData.hasSourcePackage() && values.containsKey(Voicemails.SOURCE_PACKAGE)) { + if (!uriData.getSourcePackage().equals(values.get(Voicemails.SOURCE_PACKAGE))) { + throw new IllegalArgumentException( + "Provider in URI was " + uriData.getSourcePackage() + + " but doesn't match provider in ContentValues which was " + + values.get(Voicemails.SOURCE_PACKAGE)); + } + } + // You must have access to the provider given in values. + if (!hasFullPermission(getCallingPackage())) { + checkPackagesMatch(getCallingPackage(), values.getAsString(Voicemails.SOURCE_PACKAGE), + uriData.getUri()); + } + } + + /** + * Checks that the callingProvider is same as voicemailProvider. Throws {@link + * SecurityException} if they don't match. + */ + private final void checkPackagesMatch(String callingProvider, String voicemailProvider, + Uri uri) { + if (!voicemailProvider.equals(callingProvider)) { + String errorMsg = String.format("Permission denied for URI: %s\n. " + + "Provider %s cannot perform this operation for %s. Requires %s permission.", + uri, callingProvider, voicemailProvider, + Manifest.permission.READ_WRITE_ALL_VOICEMAIL); + throw new SecurityException(errorMsg); + } + } + + private void checkInsertSupported(UriData uriData) { + if (uriData.hasId()) { + throw new UnsupportedOperationException(String.format( + "Cannot insert URI: %s. Inserted URIs should not contain an id.", + uriData.getUri())); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + checkHasOwnPermission(); + UriData uriData = createUriData(uri); + checkUpdateSupported(uriData); + checkPackagePermission(uriData); + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); + // TODO: This implementation does not allow bulk update because it only accepts + // URI that include message Id. I think we do want to support bulk update. + String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(), + getWhereClause(uriData), getCallTypeClause()); + int count = db.update(VOICEMAILS_TABLE_NAME, values, combinedClause, selectionArgs); + if (count > 0) { + notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); + } + return count; + } + + private void checkUpdateSupported(UriData uriData) { + if (!uriData.hasId()) { + throw new UnsupportedOperationException(String.format( + "Cannot update URI: %s. Bulk update not supported", uriData.getUri())); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + checkHasOwnPermission(); + UriData uriData = createUriData(uri); + checkPackagePermission(uriData); + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); + String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(), + getWhereClause(uriData), getCallTypeClause()); + + // Delete all the files associated with this query. Once we've deleted the rows, there will + // be no way left to get hold of the files. + Cursor cursor = null; + try { + cursor = queryInternal(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs, + null); + while (cursor.moveToNext()) { + File file = new File(cursor.getString(0)); + if (file.exists()) { + boolean success = file.delete(); + if (!success) { + Log.e(TAG, "Failed to delete file: " + file.getAbsolutePath()); + } + } + } + } finally { + CloseUtils.closeQuietly(cursor); + } + + // Now delete the rows themselves. + int count = db.delete(VOICEMAILS_TABLE_NAME, combinedClause, selectionArgs); + if (count > 0) { + notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); + } + return count; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + checkHasOwnPermission(); + UriData uriData = createUriData(uri); + checkPackagePermission(uriData); + + // This relies on "_data" column to be populated with the file path. + ParcelFileDescriptor openFileHelper = openFileHelper(uri, mode); + + // If the open succeeded, then update the file exists bit in the table. + if (mode.contains("w")) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.HAS_CONTENT, 1); + update(uri, contentValues, null, null); + } + + return openFileHelper; + } + + /** + * Notifies the content resolver and fires required broadcast intent(s) to notify about the + * change. + * + * @param notificationUri The URI that got impacted due to the change. This is the URI that is + * included in content resolver and broadcast intent notification. + * @param intentActions List of intent actions that needs to be fired. A separate intent is + * fired for each intent action. + */ + private void notifyChange(Uri notificationUri, String... intentActions) { + // Notify the observers. + mContentResolver.notifyChange(notificationUri, null, true); + // Fire notification intents. + for (String intentAction : intentActions) { + // TODO: We can possibly be more intelligent here and send targeted intents based on + // what voicemail permission the package has. If possible, here is what we would like to + // do for a given broadcast intent - + // 1) Send it to all packages that have READ_WRITE_ALL_VOICEMAIL permission. + // 2) Send it to only the owner package that has just READ_WRITE_OWN_VOICEMAIL, if not + // already sent in (1). + Intent intent = new Intent(intentAction, notificationUri); + intent.putExtra(VoicemailContract.EXTRA_CHANGED_BY, getCallingPackage()); + context().sendOrderedBroadcast(intent, Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + } + + /** Generates a random file for storing audio data. */ + private String generateDataFile() { + try { + File dataDirectory = context().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE); + File voicemailFile = File.createTempFile("voicemail", "", dataDirectory); + return voicemailFile.getAbsolutePath(); + } catch (IOException e) { + // If we are unable to create a temporary file, something went horribly wrong. + throw new RuntimeException("unable to create temp file", e); + } + } + + /** + * Decorates a URI by providing methods to get various properties from the URI. + */ + private static class UriData { + private final Uri mUri; + private final String mId; + private final String mSourcePackage; + + public UriData(Uri uri, String id, String sourcePackage) { + mUri = uri; + mId = id; + mSourcePackage = sourcePackage; + } + + /** Gets the original URI to which this {@link UriData} corresponds. */ + public final Uri getUri() { + return mUri; + } + + /** Tells us if our URI has an individual voicemail id. */ + public final boolean hasId() { + return mId != null; + } + + /** Gets the ID for the voicemail. */ + public final String getId() { + return mId; + } + + /** Tells us if our URI has a source package string. */ + public final boolean hasSourcePackage() { + return mSourcePackage != null; + } + + /** Gets the source package. */ + public final String getSourcePackage() { + return mSourcePackage; + } + } + + /** + * Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the + * READ_WRITE_OWN_VOICEMAIL permission and is using a URI that matches + * /voicemail/source/[source-package] where [source-package] is the same as the calling + * package. + * + * @throws SecurityException if the check fails. + */ + private void checkPackagePermission(UriData uriData) { + if (!hasFullPermission(getCallingPackage())) { + if (!uriData.hasSourcePackage()) { + // You cannot have a match if this is not a provider uri. + throw new SecurityException(String.format( + "Provider %s does not have %s permission." + + "\nPlease use /voicemail/provider/ query path instead.\nURI: %s", + getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL, + uriData.getUri())); + } + checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri()); + } + } + + private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() { + return new TypedUriMatcherImpl<VoicemailUriType>( + VoicemailContract.AUTHORITY, VoicemailUriType.values()); + } + + /** Get a {@link UriData} corresponding to a given uri. */ + private UriData createUriData(Uri uri) { + List<String> segments = uri.getPathSegments(); + switch (createUriMatcher().match(uri)) { + case VOICEMAILS: + return new UriData(uri, null, null); + case VOICEMAILS_ID: + return new UriData(uri, segments.get(1), null); + case VOICEMAILS_SOURCE: + return new UriData(uri, null, segments.get(2)); + case VOICEMAILS_SOURCE_ID: + return new UriData(uri, segments.get(3), segments.get(2)); + case NO_MATCH: + throw new IllegalArgumentException("Invalid URI: " + uri); + default: + throw new IllegalStateException("Impossible, all cases are covered"); + } + } + + /** + * Gets the name of the calling package. + * <p> + * It's possible (though unlikely) for there to be more than one calling package (requires that + * your manifest say you want to share process ids) in which case we will return an arbitrary + * package name. It's also possible (though very unlikely) for us to be unable to work out what + * your calling package is, in which case we will return null. + */ + /* package for test */String getCallingPackage() { + int caller = Binder.getCallingUid(); + if (caller == 0) { + return null; + } + String[] callerPackages = context().getPackageManager().getPackagesForUid(caller); + if (callerPackages == null || callerPackages.length == 0) { + return null; + } + if (callerPackages.length == 1) { + return callerPackages[0]; + } + // If we have more than one caller package, which is very unlikely, let's return the one + // with the highest permissions. If more than one has the same permission, we don't care + // which one we return. + String bestSoFar = callerPackages[0]; + for (String callerPackage : callerPackages) { + if (hasFullPermission(callerPackage)) { + // Full always wins, we can return early. + return callerPackage; + } + if (hasOwnPermission(callerPackage)) { + bestSoFar = callerPackage; + } + } + return bestSoFar; + } + + /** + * This check is made once only at every entry-point into this class from outside. + * + * @throws SecurityException if the caller does not have the voicemail source permission. + */ + private void checkHasOwnPermission() { + if (!hasOwnPermission(getCallingPackage())) { + throw new SecurityException("The caller must have permission: " + + Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + } + + /** Tells us if the given package has the source permission. */ + private boolean hasOwnPermission(String packageName) { + return hasPermission(packageName, Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + + /** + * Tells us if the given package has the full permission and the source + * permission. + */ + private boolean hasFullPermission(String packageName) { + return hasOwnPermission(packageName) && + hasPermission(packageName, Manifest.permission.READ_WRITE_ALL_VOICEMAIL); + } + + /** Tells us if the given package has the given permission. */ + /* package for test */boolean hasPermission(String packageName, String permission) { + return context().getPackageManager().checkPermission(permission, packageName) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Creates a clause to restrict the selection to the calling provider or null if the caller has + * access to all data. + */ + private String getPackageRestrictionClause() { + if (hasFullPermission(getCallingPackage())) { + return null; + } + return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage()); + } + + + /** Creates a clause to restrict the selection to only voicemail call type.*/ + private String getCallTypeClause() { + return getEqualityClause(Calls.TYPE, String.valueOf(Calls.VOICEMAIL_TYPE)); + } + +} diff --git a/src/com/android/providers/contacts/VoicemailUriType.java b/src/com/android/providers/contacts/VoicemailUriType.java new file mode 100644 index 00000000..9e728a28 --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailUriType.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts; + +import com.android.providers.contacts.util.UriType; + +/** + * Defines the different URIs handled by the voicemail content provider. + */ +enum VoicemailUriType implements UriType { + NO_MATCH(null), + VOICEMAILS("voicemail"), + VOICEMAILS_ID("voicemail/#"), + VOICEMAILS_SOURCE("voicemail/source/*"), + VOICEMAILS_SOURCE_ID("voicemail/source/*/#"); + + private final String path; + + private VoicemailUriType(String path) { + this.path = path; + } + + @Override + public String path() { + return path; + } +} diff --git a/src/com/android/providers/contacts/util/CloseUtils.java b/src/com/android/providers/contacts/util/CloseUtils.java new file mode 100644 index 00000000..c5753a77 --- /dev/null +++ b/src/com/android/providers/contacts/util/CloseUtils.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts.util; + +import android.database.Cursor; + +/** + * Utility methods for closing database cursors. + */ +public class CloseUtils { + private CloseUtils() { + } + + /** If the argument is non-null, close the cursor. */ + public static void closeQuietly(Cursor cursor) { + if (cursor != null) { + cursor.close(); + } + } +} diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java new file mode 100644 index 00000000..6db077f4 --- /dev/null +++ b/src/com/android/providers/contacts/util/DbQueryUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts.util; + +import android.database.DatabaseUtils; +import android.text.TextUtils; + +/** + * Static methods for helping us build database query selection strings. + */ +public class DbQueryUtils { + // Static class with helper methods, so private constructor. + private DbQueryUtils() { + } + + /** Returns a WHERE clause asserting equality of a field to a value. */ + public static String getEqualityClause(String field, String value) { + StringBuilder clause = new StringBuilder(); + clause.append("("); + clause.append(field); + clause.append(" = "); + DatabaseUtils.appendEscapedSQLString(clause, value); + clause.append(")"); + return clause.toString(); + } + + /** Concatenates any number of clauses using "AND". */ + public static String concatenateClauses(String... clauses) { + StringBuilder builder = new StringBuilder(); + for (String clause : clauses) { + if (!TextUtils.isEmpty(clause)) { + if (builder.length() > 0) { + builder.append(" AND "); + } + builder.append("("); + builder.append(clause); + builder.append(")"); + } + } + return builder.toString(); + } +} diff --git a/src/com/android/providers/contacts/util/TypedUriMatcher.java b/src/com/android/providers/contacts/util/TypedUriMatcher.java new file mode 100644 index 00000000..e9a5f4bf --- /dev/null +++ b/src/com/android/providers/contacts/util/TypedUriMatcher.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts.util; + +import android.net.Uri; + +/** + * Maps a {@link Uri} into an enum type. + * + * @param <T> the type of the URI + */ +public interface TypedUriMatcher<T extends UriType> { + public T match(Uri uri); +} diff --git a/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java b/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java new file mode 100644 index 00000000..6378adf7 --- /dev/null +++ b/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts.util; + +import android.content.UriMatcher; +import android.net.Uri; + +/** + * Implementation of {@link TypedUriMatcher}. + * + * @param <T> the type of the URI + */ +public class TypedUriMatcherImpl<T extends UriType> implements TypedUriMatcher<T> { + private final String mAuthority; + private final T[] mValues; + private final T mNoMatchUriType; + private final UriMatcher mUriMatcher; + + public TypedUriMatcherImpl(String authority, T[] values) { + mAuthority = authority; + mValues = values; + mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + T candidateNoMatchUriType = null; + for (T value : values) { + String path = value.path(); + if (path != null) { + addUriType(path, value); + } else { + candidateNoMatchUriType = value; + } + } + this.mNoMatchUriType = candidateNoMatchUriType; + } + + private void addUriType(String path, T value) { + mUriMatcher.addURI(mAuthority, path, value.ordinal()); + } + + @Override + public T match(Uri uri) { + int match = mUriMatcher.match(uri); + if (match == UriMatcher.NO_MATCH) { + return mNoMatchUriType; + } + return mValues[match]; + } +} diff --git a/src/com/android/providers/contacts/util/UriType.java b/src/com/android/providers/contacts/util/UriType.java new file mode 100644 index 00000000..e0e0b21c --- /dev/null +++ b/src/com/android/providers/contacts/util/UriType.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 The Android Open Source 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 com.android.providers.contacts.util; + +/** + * URI type used by {@link TypedUriMatcher}. + */ +public interface UriType { + /** Returns the path associated with this URI type. */ + public String path(); + + /** Returns the ordinal associated with this URI type. */ + public int ordinal(); +} |