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