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