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