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