Home | History | Annotate | Download | only in contacts
      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