/* * 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 }; private static final ImmutableSet ALLOWED_COLUMNS = new ImmutableSet.Builder() .add(Voicemails._ID) .add(Voicemails.NUMBER) .add(Voicemails.DATE) .add(Voicemails.DURATION) .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 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.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. *

* 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) { 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 = getDatabaseModifier(db).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 getDatabaseModifier(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 getDatabaseModifier(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 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 getDatabaseModifier(SQLiteDatabase db) { return new DbModifierWithNotification(mTableName, db, mContext); } }