| /* |
| * 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.checkForSupportedColumns; |
| import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; |
| import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; |
| |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.DatabaseUtils; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteQueryBuilder; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.CallLog.Calls; |
| import android.provider.OpenableColumns; |
| import android.provider.VoicemailContract.Voicemails; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import com.android.common.content.ProjectionMap; |
| import com.android.providers.contacts.VoicemailContentProvider.UriData; |
| import com.android.providers.contacts.util.CloseUtils; |
| |
| import com.google.common.collect.ImmutableSet; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| |
| /** |
| * Implementation of {@link VoicemailTable.Delegate} for the voicemail content table. |
| */ |
| public class VoicemailContentTable implements VoicemailTable.Delegate { |
| |
| private static final String TAG = "VmContentProvider"; |
| private final ProjectionMap mVoicemailProjectionMap; |
| |
| /** |
| * 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[] FILENAME_ONLY_PROJECTION = new String[] {Voicemails._DATA}; |
| |
| public static final ImmutableSet<String> ALLOWED_COLUMNS = new ImmutableSet.Builder<String>() |
| .add(Voicemails._ID) |
| .add(Voicemails.NUMBER) |
| .add(Voicemails.DATE) |
| .add(Voicemails.DURATION) |
| .add(Voicemails.NEW) |
| .add(Voicemails.IS_READ) |
| .add(Voicemails.TRANSCRIPTION) |
| .add(Voicemails.TRANSCRIPTION_STATE) |
| .add(Voicemails.STATE) |
| .add(Voicemails.SOURCE_DATA) |
| .add(Voicemails.SOURCE_PACKAGE) |
| .add(Voicemails.HAS_CONTENT) |
| .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME) |
| .add(Voicemails.PHONE_ACCOUNT_ID) |
| .add(Voicemails.MIME_TYPE) |
| .add(Voicemails.DIRTY) |
| .add(Voicemails.DELETED) |
| .add(Voicemails.LAST_MODIFIED) |
| .add(Voicemails.BACKED_UP) |
| .add(Voicemails.RESTORED) |
| .add(Voicemails.ARCHIVED) |
| .add(Voicemails.IS_OMTP_VOICEMAIL) |
| .add(OpenableColumns.DISPLAY_NAME) |
| .add(OpenableColumns.SIZE) |
| .build(); |
| |
| private static final int BULK_INSERTS_PER_YIELD_POINT = 50; |
| |
| private final String mTableName; |
| private final CallLogDatabaseHelper mDbHelper; |
| private final Context mContext; |
| private final VoicemailTable.DelegateHelper mDelegateHelper; |
| private final CallLogInsertionHelper mCallLogInsertionHelper; |
| |
| public VoicemailContentTable(String tableName, Context context, CallLogDatabaseHelper dbHelper, |
| VoicemailTable.DelegateHelper contentProviderHelper, |
| CallLogInsertionHelper callLogInsertionHelper) { |
| mTableName = tableName; |
| mContext = context; |
| mDbHelper = dbHelper; |
| mDelegateHelper = contentProviderHelper; |
| mVoicemailProjectionMap = new ProjectionMap.Builder() |
| .add(Voicemails._ID) |
| .add(Voicemails.NUMBER) |
| .add(Voicemails.DATE) |
| .add(Voicemails.DURATION) |
| .add(Voicemails.NEW) |
| .add(Voicemails.IS_READ) |
| .add(Voicemails.TRANSCRIPTION) |
| .add(Voicemails.TRANSCRIPTION_STATE) |
| .add(Voicemails.STATE) |
| .add(Voicemails.SOURCE_DATA) |
| .add(Voicemails.SOURCE_PACKAGE) |
| .add(Voicemails.HAS_CONTENT) |
| .add(Voicemails.MIME_TYPE) |
| .add(Voicemails._DATA) |
| .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME) |
| .add(Voicemails.PHONE_ACCOUNT_ID) |
| .add(Voicemails.DIRTY) |
| .add(Voicemails.DELETED) |
| .add(Voicemails.LAST_MODIFIED) |
| .add(Voicemails.BACKED_UP) |
| .add(Voicemails.RESTORED) |
| .add(Voicemails.ARCHIVED) |
| .add(Voicemails.IS_OMTP_VOICEMAIL) |
| .add(OpenableColumns.DISPLAY_NAME, createDisplayName(context)) |
| .add(OpenableColumns.SIZE, "NULL") |
| .build(); |
| mCallLogInsertionHelper = callLogInsertionHelper; |
| } |
| |
| /** |
| * Calculate a suitable value for the display name column. |
| * <p> |
| * This is a bit of a hack, it uses a suitably localized string and uses SQL to combine this |
| * with the number column. |
| */ |
| private static String createDisplayName(Context context) { |
| String prefix = context.getString(R.string.voicemail_from_column); |
| return DatabaseUtils.sqlEscapeString(prefix) + " || " + Voicemails.NUMBER; |
| } |
| |
| @Override |
| public Uri insert(UriData uriData, ContentValues values) { |
| DatabaseModifier modifier = createDatabaseModifier(mDbHelper.getWritableDatabase()); |
| Uri uri = insertRow(modifier, uriData, values); |
| return uri; |
| } |
| |
| @Override |
| public int bulkInsert(UriData uriData, ContentValues[] values) { |
| DatabaseModifier modifier = createDatabaseModifier(mDbHelper.getWritableDatabase()); |
| modifier.startBulkOperation(); |
| int count = 0; |
| for (ContentValues value : values) { |
| Uri uri = insertRow(modifier, uriData, value); |
| if (uri != null) { |
| count++; |
| } |
| if((count % BULK_INSERTS_PER_YIELD_POINT) == 0){ |
| modifier.yieldBulkOperation(); |
| } |
| } |
| modifier.finishBulkOperation(); |
| return count; |
| } |
| |
| private Uri insertRow(DatabaseModifier modifier, UriData uriData, ContentValues values) { |
| checkForSupportedColumns(mVoicemailProjectionMap, values); |
| ContentValues copiedValues = new ContentValues(values); |
| checkInsertSupported(uriData); |
| mDelegateHelper.checkAndAddSourcePackageIntoValues(uriData, copiedValues); |
| |
| // Add the computed fields to the copied values. |
| mCallLogInsertionHelper.addComputedValues(copiedValues); |
| |
| // "_data" column is used by base ContentProvider's openFileHelper() to determine filename |
| // when Input/Output stream is requested to be opened. |
| copiedValues.put(Voicemails._DATA, generateDataFile()); |
| |
| // call type is always voicemail. |
| copiedValues.put(Calls.TYPE, Calls.VOICEMAIL_TYPE); |
| // A voicemail is marked as new unless it is marked as read or explicitly overridden. |
| boolean isRead = values.containsKey(Calls.IS_READ) ? |
| values.getAsBoolean(Calls.IS_READ) : false; |
| if (!values.containsKey(Calls.NEW)) { |
| copiedValues.put(Calls.NEW, !isRead); |
| } |
| |
| SQLiteDatabase db = mDbHelper.getWritableDatabase(); |
| long rowId = modifier.insert(mTableName, null, copiedValues); |
| if (rowId > 0) { |
| Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId); |
| // Populate the 'voicemail_uri' field to be used by the call_log provider. |
| updateVoicemailUri(db, newUri); |
| return newUri; |
| } |
| return null; |
| } |
| |
| 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())); |
| } |
| } |
| |
| /** Generates a random file for storing audio data. */ |
| private String generateDataFile() { |
| try { |
| File dataDirectory = mContext.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); |
| } |
| } |
| private void updateVoicemailUri(SQLiteDatabase db, Uri newUri) { |
| ContentValues values = new ContentValues(); |
| values.put(Calls.VOICEMAIL_URI, newUri.toString()); |
| // Directly update the db because we cannot update voicemail_uri through external |
| // update() due to projectionMap check. This also avoids unnecessary permission |
| // checks that are already done as part of insert request. |
| db.update(mTableName, values, UriData.createUriData(newUri).getWhereClause(), null); |
| } |
| |
| @Override |
| public int delete(UriData uriData, String selection, String[] selectionArgs) { |
| final SQLiteDatabase db = mDbHelper.getWritableDatabase(); |
| String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), |
| 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 = query(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs, null); |
| while (cursor.moveToNext()) { |
| String filename = cursor.getString(0); |
| if (filename == null) { |
| Log.w(TAG, "No filename for uri " + uriData.getUri() + ", cannot delete file"); |
| continue; |
| } |
| File file = new File(filename); |
| 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. |
| return createDatabaseModifier(db).delete(mTableName, combinedClause, |
| selectionArgs); |
| } |
| |
| @Override |
| public Cursor query(UriData uriData, String[] projection, String selection, |
| String[] selectionArgs, String sortOrder) { |
| SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); |
| qb.setTables(mTableName); |
| qb.setProjectionMap(mVoicemailProjectionMap); |
| qb.setStrict(true); |
| |
| String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), |
| getCallTypeClause()); |
| SQLiteDatabase db = mDbHelper.getReadableDatabase(); |
| Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder); |
| if (c != null) { |
| c.setNotificationUri(mContext.getContentResolver(), Voicemails.CONTENT_URI); |
| } |
| return c; |
| } |
| |
| @Override |
| public int update(UriData uriData, ContentValues values, String selection, |
| String[] selectionArgs) { |
| |
| checkForSupportedColumns(ALLOWED_COLUMNS, values, "Updates are not allowed."); |
| checkUpdateSupported(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, uriData.getWhereClause(), |
| getCallTypeClause()); |
| return createDatabaseModifier(db).update(uriData.getUri(), mTableName, values, combinedClause, |
| selectionArgs); |
| } |
| |
| 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 String getType(UriData uriData) { |
| if (uriData.hasId()) { |
| return Voicemails.ITEM_TYPE; |
| } else { |
| return Voicemails.DIR_TYPE; |
| } |
| } |
| |
| @Override |
| public ParcelFileDescriptor openFile(UriData uriData, String mode) |
| throws FileNotFoundException { |
| return mDelegateHelper.openDataFile(uriData, mode); |
| } |
| |
| @Override |
| public ArraySet<String> getSourcePackages() { |
| return mDbHelper.selectDistinctColumn(mTableName, Voicemails.SOURCE_PACKAGE); |
| } |
| |
| /** Creates a clause to restrict the selection to only voicemail call type.*/ |
| private String getCallTypeClause() { |
| return getEqualityClause(Calls.TYPE, Calls.VOICEMAIL_TYPE); |
| } |
| |
| private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { |
| return new DbModifierWithNotification(mTableName, db, mContext); |
| } |
| |
| } |