1 /* 2 * Copyright (C) 2016 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.android.dialer.voicemail; 18 19 import com.android.contacts.common.testing.NeededForTesting; 20 import com.android.dialer.calllog.CallLogQuery; 21 import com.android.dialer.database.VoicemailArchiveContract; 22 import com.android.dialer.util.AsyncTaskExecutor; 23 import com.android.dialer.util.AsyncTaskExecutors; 24 import com.google.common.base.Preconditions; 25 import com.google.common.io.ByteStreams; 26 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.database.Cursor; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.provider.CallLog; 34 import android.provider.VoicemailContract; 35 import android.util.Log; 36 import com.android.common.io.MoreCloseables; 37 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.OutputStream; 41 42 import javax.annotation.Nullable; 43 44 /** 45 * Class containing asynchronous tasks for voicemails. 46 */ 47 @NeededForTesting 48 public class VoicemailAsyncTaskUtil { 49 private static final String TAG = "VoicemailAsyncTaskUtil"; 50 51 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 52 public enum Tasks { 53 GET_VOICEMAIL_FILE_PATH, 54 SET_VOICEMAIL_ARCHIVE_STATUS, 55 ARCHIVE_VOICEMAIL_CONTENT 56 } 57 58 @NeededForTesting 59 public interface OnArchiveVoicemailListener { 60 /** 61 * Called after the voicemail has been archived. 62 * 63 * @param archivedVoicemailUri the URI of the archived voicemail 64 */ 65 void onArchiveVoicemail(@Nullable Uri archivedVoicemailUri); 66 } 67 68 @NeededForTesting 69 public interface OnSetVoicemailArchiveStatusListener { 70 /** 71 * Called after the voicemail archived_by_user column is updated. 72 * 73 * @param success whether the update was successful or not 74 */ 75 void onSetVoicemailArchiveStatus(boolean success); 76 } 77 78 @NeededForTesting 79 public interface OnGetArchivedVoicemailFilePathListener { 80 /** 81 * Called after the voicemail file path is obtained. 82 * 83 * @param filePath the file path of the archived voicemail 84 */ 85 void onGetArchivedVoicemailFilePath(@Nullable String filePath); 86 } 87 88 private final ContentResolver mResolver; 89 private final AsyncTaskExecutor mAsyncTaskExecutor; 90 91 @NeededForTesting 92 public VoicemailAsyncTaskUtil(ContentResolver contentResolver) { 93 mResolver = Preconditions.checkNotNull(contentResolver); 94 mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); 95 } 96 97 /** 98 * Returns the archived voicemail file path. 99 */ 100 @NeededForTesting 101 public void getVoicemailFilePath( 102 final OnGetArchivedVoicemailFilePathListener listener, 103 final Uri voicemailUri) { 104 Preconditions.checkNotNull(listener); 105 Preconditions.checkNotNull(voicemailUri); 106 mAsyncTaskExecutor.submit(Tasks.GET_VOICEMAIL_FILE_PATH, 107 new AsyncTask<Void, Void, String>() { 108 @Nullable 109 @Override 110 protected String doInBackground(Void... params) { 111 try (Cursor cursor = mResolver.query(voicemailUri, 112 new String[]{VoicemailArchiveContract.VoicemailArchive._DATA}, 113 null, null, null)) { 114 if (hasContent(cursor)) { 115 return cursor.getString(cursor.getColumnIndex( 116 VoicemailArchiveContract.VoicemailArchive._DATA)); 117 } 118 } 119 return null; 120 } 121 122 @Override 123 protected void onPostExecute(String filePath) { 124 listener.onGetArchivedVoicemailFilePath(filePath); 125 } 126 }); 127 } 128 129 /** 130 * Updates the archived_by_user flag of the archived voicemail. 131 */ 132 @NeededForTesting 133 public void setVoicemailArchiveStatus( 134 final OnSetVoicemailArchiveStatusListener listener, 135 final Uri voicemailUri, 136 final boolean archivedByUser) { 137 Preconditions.checkNotNull(listener); 138 Preconditions.checkNotNull(voicemailUri); 139 mAsyncTaskExecutor.submit(Tasks.SET_VOICEMAIL_ARCHIVE_STATUS, 140 new AsyncTask<Void, Void, Boolean>() { 141 @Override 142 protected Boolean doInBackground(Void... params) { 143 ContentValues values = new ContentValues(1); 144 values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, 145 archivedByUser); 146 return mResolver.update(voicemailUri, values, null, null) > 0; 147 } 148 149 @Override 150 protected void onPostExecute(Boolean success) { 151 listener.onSetVoicemailArchiveStatus(success); 152 } 153 }); 154 } 155 156 /** 157 * Checks if a voicemail has already been archived, if so, return the previously archived URI. 158 * Otherwise, copy the voicemail information to the local dialer database. If archive was 159 * successful, archived voicemail URI is returned to listener, otherwise null. 160 */ 161 @NeededForTesting 162 public void archiveVoicemailContent( 163 final OnArchiveVoicemailListener listener, 164 final Uri voicemailUri) { 165 Preconditions.checkNotNull(listener); 166 Preconditions.checkNotNull(voicemailUri); 167 mAsyncTaskExecutor.submit(Tasks.ARCHIVE_VOICEMAIL_CONTENT, 168 new AsyncTask<Void, Void, Uri>() { 169 @Nullable 170 @Override 171 protected Uri doInBackground(Void... params) { 172 Uri archivedVoicemailUri = getArchivedVoicemailUri(voicemailUri); 173 174 // If previously archived, return uri, otherwise archive everything. 175 if (archivedVoicemailUri != null) { 176 return archivedVoicemailUri; 177 } 178 179 // Combine call log and voicemail content info. 180 ContentValues values = getVoicemailContentValues(voicemailUri); 181 if (values == null) { 182 return null; 183 } 184 185 Uri insertedVoicemailUri = mResolver.insert( 186 VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, values); 187 if (insertedVoicemailUri == null) { 188 return null; 189 } 190 191 // Copy voicemail content to a new file. 192 boolean copiedFile = false; 193 try (InputStream inputStream = mResolver.openInputStream(voicemailUri); 194 OutputStream outputStream = 195 mResolver.openOutputStream(insertedVoicemailUri)) { 196 if (inputStream != null && outputStream != null) { 197 ByteStreams.copy(inputStream, outputStream); 198 copiedFile = true; 199 return insertedVoicemailUri; 200 } 201 } catch (IOException e) { 202 Log.w(TAG, "Failed to copy voicemail content to new file: " 203 + e.toString()); 204 } finally { 205 if (!copiedFile) { 206 // Roll back insert if the voicemail content was not copied. 207 mResolver.delete(insertedVoicemailUri, null, null); 208 } 209 } 210 return null; 211 } 212 213 @Override 214 protected void onPostExecute(Uri archivedVoicemailUri) { 215 listener.onArchiveVoicemail(archivedVoicemailUri); 216 } 217 }); 218 } 219 220 /** 221 * Helper method to get the archived URI of a voicemail. 222 * 223 * @param voicemailUri a {@link android.provider.VoicemailContract.Voicemails#CONTENT_URI} URI. 224 * @return the URI of the archived voicemail or {@code null} 225 */ 226 @Nullable 227 private Uri getArchivedVoicemailUri(Uri voicemailUri) { 228 try (Cursor cursor = getArchiveExistsCursor(voicemailUri)) { 229 if (hasContent(cursor)) { 230 return VoicemailArchiveContract.VoicemailArchive 231 .buildWithId(cursor.getInt(cursor.getColumnIndex( 232 VoicemailArchiveContract.VoicemailArchive._ID))); 233 } 234 } 235 return null; 236 } 237 238 /** 239 * Helper method to make a copy of all the values needed to display a voicemail. 240 * 241 * @param voicemailUri a {@link VoicemailContract.Voicemails#CONTENT_URI} URI. 242 * @return the combined call log and voicemail values for the given URI, or {@code null} 243 */ 244 @Nullable 245 private ContentValues getVoicemailContentValues(Uri voicemailUri) { 246 try (Cursor callLogInfo = getCallLogInfoCursor(voicemailUri); 247 Cursor contentInfo = getContentInfoCursor(voicemailUri)) { 248 249 if (hasContent(callLogInfo) && hasContent(contentInfo)) { 250 // Create values to insert into database. 251 ContentValues values = new ContentValues(); 252 253 // Insert voicemail call log info. 254 values.put(VoicemailArchiveContract.VoicemailArchive.COUNTRY_ISO, 255 callLogInfo.getString(CallLogQuery.COUNTRY_ISO)); 256 values.put(VoicemailArchiveContract.VoicemailArchive.GEOCODED_LOCATION, 257 callLogInfo.getString(CallLogQuery.GEOCODED_LOCATION)); 258 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NAME, 259 callLogInfo.getString(CallLogQuery.CACHED_NAME)); 260 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_TYPE, 261 callLogInfo.getInt(CallLogQuery.CACHED_NUMBER_TYPE)); 262 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_LABEL, 263 callLogInfo.getString(CallLogQuery.CACHED_NUMBER_LABEL)); 264 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_LOOKUP_URI, 265 callLogInfo.getString(CallLogQuery.CACHED_LOOKUP_URI)); 266 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_MATCHED_NUMBER, 267 callLogInfo.getString(CallLogQuery.CACHED_MATCHED_NUMBER)); 268 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NORMALIZED_NUMBER, 269 callLogInfo.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER)); 270 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_FORMATTED_NUMBER, 271 callLogInfo.getString(CallLogQuery.CACHED_FORMATTED_NUMBER)); 272 values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER_PRESENTATION, 273 callLogInfo.getInt(CallLogQuery.NUMBER_PRESENTATION)); 274 values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_COMPONENT_NAME, 275 callLogInfo.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME)); 276 values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_ID, 277 callLogInfo.getString(CallLogQuery.ACCOUNT_ID)); 278 values.put(VoicemailArchiveContract.VoicemailArchive.FEATURES, 279 callLogInfo.getInt(CallLogQuery.FEATURES)); 280 values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_PHOTO_URI, 281 callLogInfo.getString(CallLogQuery.CACHED_PHOTO_URI)); 282 283 // Insert voicemail content info. 284 values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID, 285 contentInfo.getInt(contentInfo.getColumnIndex( 286 VoicemailContract.Voicemails._ID))); 287 values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER, 288 contentInfo.getString(contentInfo.getColumnIndex( 289 VoicemailContract.Voicemails.NUMBER))); 290 values.put(VoicemailArchiveContract.VoicemailArchive.DATE, 291 contentInfo.getLong(contentInfo.getColumnIndex( 292 VoicemailContract.Voicemails.DATE))); 293 values.put(VoicemailArchiveContract.VoicemailArchive.DURATION, 294 contentInfo.getLong(contentInfo.getColumnIndex( 295 VoicemailContract.Voicemails.DURATION))); 296 values.put(VoicemailArchiveContract.VoicemailArchive.MIME_TYPE, 297 contentInfo.getString(contentInfo.getColumnIndex( 298 VoicemailContract.Voicemails.MIME_TYPE))); 299 values.put(VoicemailArchiveContract.VoicemailArchive.TRANSCRIPTION, 300 contentInfo.getString(contentInfo.getColumnIndex( 301 VoicemailContract.Voicemails.TRANSCRIPTION))); 302 303 // Achived is false by default because it is updated after insertion. 304 values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, false); 305 306 return values; 307 } 308 } 309 return null; 310 } 311 312 private boolean hasContent(@Nullable Cursor cursor) { 313 return cursor != null && cursor.moveToFirst(); 314 } 315 316 @Nullable 317 private Cursor getCallLogInfoCursor(Uri voicemailUri) { 318 return mResolver.query( 319 ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, 320 ContentUris.parseId(voicemailUri)), 321 CallLogQuery._PROJECTION, null, null, null); 322 } 323 324 @Nullable 325 private Cursor getContentInfoCursor(Uri voicemailUri) { 326 return mResolver.query(voicemailUri, 327 new String[] { 328 VoicemailContract.Voicemails._ID, 329 VoicemailContract.Voicemails.NUMBER, 330 VoicemailContract.Voicemails.DATE, 331 VoicemailContract.Voicemails.DURATION, 332 VoicemailContract.Voicemails.MIME_TYPE, 333 VoicemailContract.Voicemails.TRANSCRIPTION, 334 }, null, null, null); 335 } 336 337 @Nullable 338 private Cursor getArchiveExistsCursor(Uri voicemailUri) { 339 return mResolver.query(VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, 340 new String[] {VoicemailArchiveContract.VoicemailArchive._ID}, 341 VoicemailArchiveContract.VoicemailArchive.SERVER_ID + "=" 342 + ContentUris.parseId(voicemailUri), 343 null, 344 null); 345 } 346 } 347