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