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