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