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 
17 package com.example.android.voicemail.common.core;
18 
19 import com.example.android.voicemail.common.logging.Logger;
20 import com.example.android.voicemail.common.utils.CloseUtils;
21 import com.example.android.voicemail.common.utils.DbQueryUtils;
22 
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.provider.VoicemailContract;
30 import android.provider.VoicemailContract.Voicemails;
31 
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * Implementation of the {@link VoicemailProviderHelper} interface.
40  */
41 public final class VoicemailProviderHelpers implements VoicemailProviderHelper {
42     private static final Logger logger = Logger.getLogger(VoicemailProviderHelpers.class);
43 
44     /** Full projection on the voicemail table, giving us all the columns. */
45     private static final String[] FULL_PROJECTION = new String[] {
46             Voicemails._ID,
47             Voicemails.HAS_CONTENT,
48             Voicemails.NUMBER,
49             Voicemails.DURATION,
50             Voicemails.DATE,
51             Voicemails.SOURCE_PACKAGE,
52             Voicemails.SOURCE_DATA,
53             Voicemails.IS_READ
54     };
55 
56     private final ContentResolver mContentResolver;
57     private final Uri mBaseUri;
58 
59     /**
60      * Creates an instance of {@link VoicemailProviderHelpers} that wraps the supplied content
61      * provider.
62      *
63      * @param contentResolver the ContentResolver used for opening the output stream to read and
64      *            write to the file
65      */
VoicemailProviderHelpers(Uri baseUri, ContentResolver contentResolver)66     private VoicemailProviderHelpers(Uri baseUri, ContentResolver contentResolver) {
67         mContentResolver = contentResolver;
68         mBaseUri = baseUri;
69     }
70 
71     /**
72      * Constructs a VoicemailProviderHelper with full access to all voicemails.
73      * <p>
74      * Requires the manifest permissions
75      * <code>com.android.providers.voicemail.permission.READ_WRITE_ALL_VOICEMAIL</code> and
76      * <code>com.android.providers.voicemail.permission.READ_WRITE_OWN_VOICEMAIL</code>.
77      */
createFullVoicemailProvider(Context context)78     public static VoicemailProviderHelper createFullVoicemailProvider(Context context) {
79         return new VoicemailProviderHelpers(Voicemails.CONTENT_URI, context.getContentResolver());
80     }
81 
82     /**
83      * Constructs a VoicemailProviderHelper with limited access to voicemails created by this
84      * source.
85      * <p>
86      * Requires the manifest permission
87      * <code>com.android.providers.voicemail.permission.READ_WRITE_OWN_VOICEMAIL</code>.
88      */
createPackageScopedVoicemailProvider(Context context)89     public static VoicemailProviderHelper createPackageScopedVoicemailProvider(Context context) {
90         return new VoicemailProviderHelpers(Voicemails.buildSourceUri(context.getPackageName()),
91                 context.getContentResolver());
92     }
93 
94     @Override
insert(Voicemail voicemail)95     public Uri insert(Voicemail voicemail) {
96         check(!voicemail.hasId(), "Inserted voicemails must not have an id", voicemail);
97         check(voicemail.hasTimestampMillis(), "Inserted voicemails must have a timestamp",
98                 voicemail);
99         check(voicemail.hasNumber(), "Inserted voicemails must have a number", voicemail);
100         logger.d(String.format("Inserting new voicemail: %s", voicemail));
101         ContentValues contentValues = getContentValues(voicemail);
102         if (!voicemail.hasRead()) {
103             // If is_read is not set then set it to false as default value.
104             contentValues.put(Voicemails.IS_READ, 0);
105         }
106         return mContentResolver.insert(mBaseUri, contentValues);
107     }
108 
109     @Override
update(Uri uri, Voicemail voicemail)110     public int update(Uri uri, Voicemail voicemail) {
111         check(!voicemail.hasUri(), "Can't update the Uri of a voicemail", voicemail);
112         logger.d("Updating voicemail: " + voicemail + " for uri: " + uri);
113         ContentValues values = getContentValues(voicemail);
114         return mContentResolver.update(uri, values, null, null);
115     }
116 
117     @Override
setVoicemailContent(Uri voicemailUri, InputStream inputStream, String mimeType)118     public void setVoicemailContent(Uri voicemailUri, InputStream inputStream, String mimeType)
119             throws IOException {
120         setVoicemailContent(voicemailUri, null, inputStream, mimeType);
121     }
122 
123     @Override
setVoicemailContent(Uri voicemailUri, byte[] inputBytes, String mimeType)124     public void setVoicemailContent(Uri voicemailUri, byte[] inputBytes, String mimeType)
125             throws IOException {
126         setVoicemailContent(voicemailUri, inputBytes, null, mimeType);
127     }
128 
setVoicemailContent(Uri voicemailUri, byte[] inputBytes, InputStream inputStream, String mimeType)129     private void setVoicemailContent(Uri voicemailUri, byte[] inputBytes, InputStream inputStream,
130             String mimeType) throws IOException {
131         if (inputBytes != null && inputStream != null) {
132             throw new IllegalArgumentException("Both inputBytes & inputStream non-null. Don't" +
133                     " know which one to use.");
134         }
135 
136         logger.d(String.format("Writing new voicemail content: %s", voicemailUri));
137         OutputStream outputStream = null;
138         try {
139             outputStream = mContentResolver.openOutputStream(voicemailUri);
140             if (inputBytes != null) {
141                 outputStream.write(inputBytes);
142             } else if (inputStream != null) {
143                 copyStreamData(inputStream, outputStream);
144             }
145         } finally {
146             CloseUtils.closeQuietly(outputStream);
147         }
148         // Update mime_type & has_content after we are done with file update.
149         ContentValues values = new ContentValues();
150         values.put(Voicemails.MIME_TYPE, mimeType);
151         values.put(Voicemails.HAS_CONTENT, true);
152         int updatedCount = mContentResolver.update(voicemailUri, values, null, null);
153         if (updatedCount != 1) {
154             throw new IOException("Updating voicemail should have updated 1 row, was: "
155                     + updatedCount);
156         }
157     }
158 
159     @Override
findVoicemailBySourceData(String sourceData)160     public Voicemail findVoicemailBySourceData(String sourceData) {
161         Cursor cursor = null;
162         try {
163             cursor = mContentResolver.query(mBaseUri, FULL_PROJECTION,
164                     DbQueryUtils.getEqualityClause(Voicemails.SOURCE_DATA, sourceData),
165                     null, null);
166             if (cursor.getCount() != 1) {
167                 logger.w("Expected 1 voicemail matching sourceData " + sourceData + ", got " +
168                         cursor.getCount());
169                 return null;
170             }
171             cursor.moveToFirst();
172             return getVoicemailFromCursor(cursor);
173         } finally {
174             CloseUtils.closeQuietly(cursor);
175         }
176     }
177 
178     @Override
findVoicemailByUri(Uri uri)179     public Voicemail findVoicemailByUri(Uri uri) {
180         Cursor cursor = null;
181         try {
182             cursor = mContentResolver.query(uri, FULL_PROJECTION, null, null, null);
183             if (cursor.getCount() != 1) {
184                 logger.w("Expected 1 voicemail matching uri " + uri + ", got " + cursor.getCount());
185                 return null;
186             }
187             cursor.moveToFirst();
188             Voicemail voicemail = getVoicemailFromCursor(cursor);
189             // Make sure this is an exact match.
190             if (voicemail.getUri().equals(uri)) {
191                 return voicemail;
192             } else {
193                 logger.w("Queried uri: " + uri + " do not represent a unique voicemail record.");
194                 return null;
195             }
196         } finally {
197             CloseUtils.closeQuietly(cursor);
198         }
199     }
200 
201     @Override
getUriForVoicemailWithId(long id)202     public Uri getUriForVoicemailWithId(long id) {
203         return ContentUris.withAppendedId(mBaseUri, id);
204     }
205 
206     /**
207      * Checks that an assertion is true.
208      *
209      * @throws IllegalArgumentException if the assertion is false, along with a suitable message
210      *             including a toString() representation of the voicemail
211      */
check(boolean assertion, String message, Voicemail voicemail)212     private void check(boolean assertion, String message, Voicemail voicemail) {
213         if (!assertion) {
214             throw new IllegalArgumentException(message + ": " + voicemail);
215         }
216     }
217 
218     @Override
deleteAll()219     public int deleteAll() {
220         logger.i(String.format("Deleting all voicemails"));
221         return mContentResolver.delete(mBaseUri, "", new String[0]);
222     }
223 
224     @Override
getAllVoicemails()225     public List<Voicemail> getAllVoicemails() {
226         return getAllVoicemails(null, null, SortOrder.DEFAULT);
227     }
228 
229     @Override
getAllVoicemails(VoicemailFilter filter, String sortColumn, SortOrder sortOrder)230     public List<Voicemail> getAllVoicemails(VoicemailFilter filter,
231             String sortColumn, SortOrder sortOrder) {
232         logger.i(String.format("Fetching all voicemails"));
233         Cursor cursor = null;
234         try {
235             cursor = mContentResolver.query(mBaseUri, FULL_PROJECTION,
236                     filter != null ? filter.getWhereClause() : null,
237                     null, getSortBy(sortColumn, sortOrder));
238             List<Voicemail> results = new ArrayList<Voicemail>(cursor.getCount());
239             while (cursor.moveToNext()) {
240                 // A performance optimisation is possible here.
241                 // The helper method extracts the column indices once every time it is called,
242                 // whilst
243                 // we could extract them all up front (without the benefit of the re-use of the
244                 // helper
245                 // method code).
246                 // At the moment I'm pretty sure the benefits outweigh the costs, so leaving as-is.
247                 results.add(getVoicemailFromCursor(cursor));
248             }
249             return results;
250         } finally {
251             CloseUtils.closeQuietly(cursor);
252         }
253     }
254 
getSortBy(String column, SortOrder sortOrder)255     private String getSortBy(String column, SortOrder sortOrder) {
256         if (column == null) {
257             return null;
258         }
259         switch (sortOrder) {
260             case ASCENDING:
261                 return column + " ASC";
262             case DESCENDING:
263                 return column + " DESC";
264             case DEFAULT:
265                 return column;
266         }
267         // Should never reach here.
268         return null;
269     }
270 
getVoicemailFromCursor(Cursor cursor)271     private VoicemailImpl getVoicemailFromCursor(Cursor cursor) {
272         long id = cursor.getLong(cursor.getColumnIndexOrThrow(Voicemails._ID));
273         String sourcePackage = cursor.getString(
274                 cursor.getColumnIndexOrThrow(Voicemails.SOURCE_PACKAGE));
275         VoicemailImpl voicemail = VoicemailImpl
276                 .createEmptyBuilder()
277                 .setTimestamp(cursor.getLong(cursor.getColumnIndexOrThrow(Voicemails.DATE)))
278                 .setNumber(cursor.getString(cursor.getColumnIndexOrThrow(Voicemails.NUMBER)))
279                 .setId(id)
280                 .setDuration(cursor.getLong(cursor.getColumnIndexOrThrow(Voicemails.DURATION)))
281                 .setSourcePackage(sourcePackage)
282                 .setSourceData(cursor.getString(
283                         cursor.getColumnIndexOrThrow(Voicemails.SOURCE_DATA)))
284                 .setUri(buildUriWithSourcePackage(id, sourcePackage))
285                 .setHasContent(cursor.getInt(
286                         cursor.getColumnIndexOrThrow(Voicemails.HAS_CONTENT)) == 1)
287                 .setIsRead(cursor.getInt(cursor.getColumnIndexOrThrow(Voicemails.IS_READ)) == 1)
288                 .build();
289         return voicemail;
290     }
291 
buildUriWithSourcePackage(long id, String sourcePackage)292     private Uri buildUriWithSourcePackage(long id, String sourcePackage) {
293         return ContentUris.withAppendedId(Voicemails.buildSourceUri(sourcePackage), id);
294     }
295 
296     /**
297      * Maps structured {@link Voicemail} to {@link ContentValues} understood by content provider.
298      */
getContentValues(Voicemail voicemail)299     private ContentValues getContentValues(Voicemail voicemail) {
300         ContentValues contentValues = new ContentValues();
301         if (voicemail.hasTimestampMillis()) {
302             contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
303         }
304         if (voicemail.hasNumber()) {
305             contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
306         }
307         if (voicemail.hasDuration()) {
308             contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
309         }
310         if (voicemail.hasSourcePackage()) {
311             contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
312         }
313         if (voicemail.hasSourceData()) {
314             contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
315         }
316         if (voicemail.hasRead()) {
317             contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
318         }
319         return contentValues;
320     }
321 
copyStreamData(InputStream in, OutputStream out)322     private void copyStreamData(InputStream in, OutputStream out) throws IOException {
323         byte[] data = new byte[8 * 1024];
324         int numBytes;
325         while ((numBytes = in.read(data)) > 0) {
326             out.write(data, 0, numBytes);
327         }
328 
329     }
330 }
331