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