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