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