1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16package com.android.providers.contacts; 17 18import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; 19import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; 20import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; 21 22import android.content.ContentUris; 23import android.content.ContentValues; 24import android.content.Context; 25import android.database.Cursor; 26import android.database.DatabaseUtils; 27import android.database.sqlite.SQLiteDatabase; 28import android.database.sqlite.SQLiteQueryBuilder; 29import android.net.Uri; 30import android.os.ParcelFileDescriptor; 31import android.provider.CallLog.Calls; 32import android.provider.OpenableColumns; 33import android.provider.VoicemailContract.Voicemails; 34import android.util.Log; 35import com.android.common.content.ProjectionMap; 36import com.android.providers.contacts.VoicemailContentProvider.UriData; 37import com.android.providers.contacts.util.CloseUtils; 38import com.google.common.collect.ImmutableSet; 39import java.io.File; 40import java.io.FileNotFoundException; 41import java.io.IOException; 42 43/** 44 * Implementation of {@link VoicemailTable.Delegate} for the voicemail content table. 45 */ 46public class VoicemailContentTable implements VoicemailTable.Delegate { 47 48 private static final String TAG = "VmContentProvider"; 49 private final ProjectionMap mVoicemailProjectionMap; 50 51 /** The private directory in which to store the data associated with the voicemail. */ 52 private static final String DATA_DIRECTORY = "voicemail-data"; 53 54 private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA }; 55 56 private static final ImmutableSet<String> ALLOWED_COLUMNS = new ImmutableSet.Builder<String>() 57 .add(Voicemails._ID) 58 .add(Voicemails.NUMBER) 59 .add(Voicemails.DATE) 60 .add(Voicemails.DURATION) 61 .add(Voicemails.IS_READ) 62 .add(Voicemails.TRANSCRIPTION) 63 .add(Voicemails.STATE) 64 .add(Voicemails.SOURCE_DATA) 65 .add(Voicemails.SOURCE_PACKAGE) 66 .add(Voicemails.HAS_CONTENT) 67 .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME) 68 .add(Voicemails.PHONE_ACCOUNT_ID) 69 .add(Voicemails.MIME_TYPE) 70 .add(Voicemails.DIRTY) 71 .add(Voicemails.DELETED) 72 .add(Voicemails.LAST_MODIFIED) 73 .add(Voicemails.BACKED_UP) 74 .add(Voicemails.RESTORED) 75 .add(Voicemails.ARCHIVED) 76 .add(Voicemails.IS_OMTP_VOICEMAIL) 77 .add(OpenableColumns.DISPLAY_NAME) 78 .add(OpenableColumns.SIZE) 79 .build(); 80 81 private final String mTableName; 82 private final CallLogDatabaseHelper mDbHelper; 83 private final Context mContext; 84 private final VoicemailTable.DelegateHelper mDelegateHelper; 85 private final CallLogInsertionHelper mCallLogInsertionHelper; 86 87 public VoicemailContentTable(String tableName, Context context, CallLogDatabaseHelper dbHelper, 88 VoicemailTable.DelegateHelper contentProviderHelper, 89 CallLogInsertionHelper callLogInsertionHelper) { 90 mTableName = tableName; 91 mContext = context; 92 mDbHelper = dbHelper; 93 mDelegateHelper = contentProviderHelper; 94 mVoicemailProjectionMap = new ProjectionMap.Builder() 95 .add(Voicemails._ID) 96 .add(Voicemails.NUMBER) 97 .add(Voicemails.DATE) 98 .add(Voicemails.DURATION) 99 .add(Voicemails.IS_READ) 100 .add(Voicemails.TRANSCRIPTION) 101 .add(Voicemails.STATE) 102 .add(Voicemails.SOURCE_DATA) 103 .add(Voicemails.SOURCE_PACKAGE) 104 .add(Voicemails.HAS_CONTENT) 105 .add(Voicemails.MIME_TYPE) 106 .add(Voicemails._DATA) 107 .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME) 108 .add(Voicemails.PHONE_ACCOUNT_ID) 109 .add(Voicemails.DIRTY) 110 .add(Voicemails.DELETED) 111 .add(Voicemails.LAST_MODIFIED) 112 .add(Voicemails.BACKED_UP) 113 .add(Voicemails.RESTORED) 114 .add(Voicemails.ARCHIVED) 115 .add(Voicemails.IS_OMTP_VOICEMAIL) 116 .add(OpenableColumns.DISPLAY_NAME, createDisplayName(context)) 117 .add(OpenableColumns.SIZE, "NULL") 118 .build(); 119 mCallLogInsertionHelper = callLogInsertionHelper; 120 } 121 122 /** 123 * Calculate a suitable value for the display name column. 124 * <p> 125 * This is a bit of a hack, it uses a suitably localized string and uses SQL to combine this 126 * with the number column. 127 */ 128 private static String createDisplayName(Context context) { 129 String prefix = context.getString(R.string.voicemail_from_column); 130 return DatabaseUtils.sqlEscapeString(prefix) + " || " + Voicemails.NUMBER; 131 } 132 133 @Override 134 public Uri insert(UriData uriData, ContentValues values) { 135 checkForSupportedColumns(mVoicemailProjectionMap, values); 136 ContentValues copiedValues = new ContentValues(values); 137 checkInsertSupported(uriData); 138 mDelegateHelper.checkAndAddSourcePackageIntoValues(uriData, copiedValues); 139 140 // Add the computed fields to the copied values. 141 mCallLogInsertionHelper.addComputedValues(copiedValues); 142 143 // "_data" column is used by base ContentProvider's openFileHelper() to determine filename 144 // when Input/Output stream is requested to be opened. 145 copiedValues.put(Voicemails._DATA, generateDataFile()); 146 147 // call type is always voicemail. 148 copiedValues.put(Calls.TYPE, Calls.VOICEMAIL_TYPE); 149 // A voicemail is marked as new unless it is marked as read or explicitly overridden. 150 boolean isRead = values.containsKey(Calls.IS_READ) ? 151 values.getAsBoolean(Calls.IS_READ) : false; 152 if (!values.containsKey(Calls.NEW)) { 153 copiedValues.put(Calls.NEW, !isRead); 154 } 155 156 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 157 long rowId = getDatabaseModifier(db).insert(mTableName, null, copiedValues); 158 if (rowId > 0) { 159 Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId); 160 // Populate the 'voicemail_uri' field to be used by the call_log provider. 161 updateVoicemailUri(db, newUri); 162 return newUri; 163 } 164 return null; 165 } 166 167 private void checkInsertSupported(UriData uriData) { 168 if (uriData.hasId()) { 169 throw new UnsupportedOperationException(String.format( 170 "Cannot insert URI: %s. Inserted URIs should not contain an id.", 171 uriData.getUri())); 172 } 173 } 174 175 /** Generates a random file for storing audio data. */ 176 private String generateDataFile() { 177 try { 178 File dataDirectory = mContext.getDir(DATA_DIRECTORY, Context.MODE_PRIVATE); 179 File voicemailFile = File.createTempFile("voicemail", "", dataDirectory); 180 return voicemailFile.getAbsolutePath(); 181 } catch (IOException e) { 182 // If we are unable to create a temporary file, something went horribly wrong. 183 throw new RuntimeException("unable to create temp file", e); 184 } 185 } 186 private void updateVoicemailUri(SQLiteDatabase db, Uri newUri) { 187 ContentValues values = new ContentValues(); 188 values.put(Calls.VOICEMAIL_URI, newUri.toString()); 189 // Directly update the db because we cannot update voicemail_uri through external 190 // update() due to projectionMap check. This also avoids unnecessary permission 191 // checks that are already done as part of insert request. 192 db.update(mTableName, values, UriData.createUriData(newUri).getWhereClause(), null); 193 } 194 195 @Override 196 public int delete(UriData uriData, String selection, String[] selectionArgs) { 197 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 198 String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), 199 getCallTypeClause()); 200 201 // Delete all the files associated with this query. Once we've deleted the rows, there will 202 // be no way left to get hold of the files. 203 Cursor cursor = null; 204 try { 205 cursor = query(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs, null); 206 while (cursor.moveToNext()) { 207 String filename = cursor.getString(0); 208 if (filename == null) { 209 Log.w(TAG, "No filename for uri " + uriData.getUri() + ", cannot delete file"); 210 continue; 211 } 212 File file = new File(filename); 213 if (file.exists()) { 214 boolean success = file.delete(); 215 if (!success) { 216 Log.e(TAG, "Failed to delete file: " + file.getAbsolutePath()); 217 } 218 } 219 } 220 } finally { 221 CloseUtils.closeQuietly(cursor); 222 } 223 224 // Now delete the rows themselves. 225 return getDatabaseModifier(db).delete(mTableName, combinedClause, 226 selectionArgs); 227 } 228 229 @Override 230 public Cursor query(UriData uriData, String[] projection, String selection, 231 String[] selectionArgs, String sortOrder) { 232 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 233 qb.setTables(mTableName); 234 qb.setProjectionMap(mVoicemailProjectionMap); 235 qb.setStrict(true); 236 237 String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), 238 getCallTypeClause()); 239 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 240 Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder); 241 if (c != null) { 242 c.setNotificationUri(mContext.getContentResolver(), Voicemails.CONTENT_URI); 243 } 244 return c; 245 } 246 247 @Override 248 public int update(UriData uriData, ContentValues values, String selection, 249 String[] selectionArgs) { 250 251 checkForSupportedColumns(ALLOWED_COLUMNS, values, "Updates are not allowed."); 252 checkUpdateSupported(uriData); 253 254 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 255 // TODO: This implementation does not allow bulk update because it only accepts 256 // URI that include message Id. I think we do want to support bulk update. 257 String combinedClause = concatenateClauses(selection, uriData.getWhereClause(), 258 getCallTypeClause()); 259 return getDatabaseModifier(db).update(uriData.getUri(), mTableName, values, combinedClause, 260 selectionArgs); 261 } 262 263 private void checkUpdateSupported(UriData uriData) { 264 if (!uriData.hasId()) { 265 throw new UnsupportedOperationException(String.format( 266 "Cannot update URI: %s. Bulk update not supported", uriData.getUri())); 267 } 268 } 269 270 @Override 271 public String getType(UriData uriData) { 272 if (uriData.hasId()) { 273 return Voicemails.ITEM_TYPE; 274 } else { 275 return Voicemails.DIR_TYPE; 276 } 277 } 278 279 @Override 280 public ParcelFileDescriptor openFile(UriData uriData, String mode) 281 throws FileNotFoundException { 282 return mDelegateHelper.openDataFile(uriData, mode); 283 } 284 285 /** Creates a clause to restrict the selection to only voicemail call type.*/ 286 private String getCallTypeClause() { 287 return getEqualityClause(Calls.TYPE, Calls.VOICEMAIL_TYPE); 288 } 289 290 private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { 291 return new DbModifierWithNotification(mTableName, db, mContext); 292 } 293 294} 295