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