1 /* 2 * Copyright (C) 2015 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 package com.android.voicemail.impl.sync; 17 18 import android.annotation.TargetApi; 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.Build.VERSION_CODES; 26 import android.provider.VoicemailContract; 27 import android.provider.VoicemailContract.Voicemails; 28 import android.support.annotation.NonNull; 29 import android.telecom.PhoneAccountHandle; 30 import com.android.dialer.common.Assert; 31 import com.android.voicemail.impl.Voicemail; 32 import java.util.ArrayList; 33 import java.util.List; 34 35 /** Construct queries to interact with the voicemails table. */ 36 public class VoicemailsQueryHelper { 37 static final String[] PROJECTION = 38 new String[] { 39 Voicemails._ID, // 0 40 Voicemails.SOURCE_DATA, // 1 41 Voicemails.IS_READ, // 2 42 Voicemails.DELETED, // 3 43 Voicemails.TRANSCRIPTION // 4 44 }; 45 46 public static final int _ID = 0; 47 public static final int SOURCE_DATA = 1; 48 public static final int IS_READ = 2; 49 public static final int DELETED = 3; 50 public static final int TRANSCRIPTION = 4; 51 52 static final String DELETED_SELECTION = Voicemails.DELETED + "=1"; 53 static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0"; 54 55 private Context context; 56 private ContentResolver contentResolver; 57 private Uri sourceUri; 58 59 public VoicemailsQueryHelper(Context context) { 60 this.context = context; 61 contentResolver = context.getContentResolver(); 62 sourceUri = VoicemailContract.Voicemails.buildSourceUri(this.context.getPackageName()); 63 } 64 65 /** 66 * Get all the locally deleted voicemails that have not been synced to the server. 67 * 68 * @return A list of deleted voicemails. 69 */ 70 public List<Voicemail> getDeletedVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) { 71 return getLocalVoicemails(phoneAccountHandle, DELETED_SELECTION); 72 } 73 74 /** 75 * Get all voicemails locally stored. 76 * 77 * @return A list of all locally stored voicemails. 78 */ 79 public List<Voicemail> getAllVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) { 80 return getLocalVoicemails(phoneAccountHandle, null); 81 } 82 83 /** 84 * Utility method to make queries to the voicemail database. 85 * 86 * <p>TODO(a bug) add PhoneAccountHandle filtering back 87 * 88 * @param selection A filter declaring which rows to return. {@code null} returns all rows. 89 * @return A list of voicemails according to the selection statement. 90 */ 91 private List<Voicemail> getLocalVoicemails( 92 @NonNull PhoneAccountHandle unusedPhoneAccountHandle, String selection) { 93 Cursor cursor = contentResolver.query(sourceUri, PROJECTION, selection, null, null); 94 if (cursor == null) { 95 return null; 96 } 97 try { 98 List<Voicemail> voicemails = new ArrayList<Voicemail>(); 99 while (cursor.moveToNext()) { 100 final long id = cursor.getLong(_ID); 101 final String sourceData = cursor.getString(SOURCE_DATA); 102 final boolean isRead = cursor.getInt(IS_READ) == 1; 103 final String transcription = cursor.getString(TRANSCRIPTION); 104 Voicemail voicemail = 105 Voicemail.createForUpdate(id, sourceData) 106 .setIsRead(isRead) 107 .setTranscription(transcription) 108 .build(); 109 voicemails.add(voicemail); 110 } 111 return voicemails; 112 } finally { 113 cursor.close(); 114 } 115 } 116 117 /** 118 * Deletes a list of voicemails from the voicemail content provider. 119 * 120 * @param voicemails The list of voicemails to delete 121 * @return The number of voicemails deleted 122 */ 123 public int deleteFromDatabase(List<Voicemail> voicemails) { 124 int count = voicemails.size(); 125 if (count == 0) { 126 return 0; 127 } 128 129 StringBuilder sb = new StringBuilder(); 130 for (int i = 0; i < count; i++) { 131 if (i > 0) { 132 sb.append(","); 133 } 134 sb.append(voicemails.get(i).getId()); 135 } 136 137 String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString()); 138 return contentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null); 139 } 140 141 /** Utility method to delete a single voicemail that is not archived. */ 142 public void deleteNonArchivedFromDatabase(Voicemail voicemail) { 143 contentResolver.delete( 144 Voicemails.CONTENT_URI, 145 Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0", 146 new String[] {Long.toString(voicemail.getId())}); 147 } 148 149 public int markReadInDatabase(List<Voicemail> voicemails) { 150 int count = voicemails.size(); 151 for (int i = 0; i < count; i++) { 152 markReadInDatabase(voicemails.get(i)); 153 } 154 return count; 155 } 156 157 /** Utility method to mark single message as read. */ 158 public void markReadInDatabase(Voicemail voicemail) { 159 Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId()); 160 ContentValues contentValues = new ContentValues(); 161 contentValues.put(Voicemails.IS_READ, "1"); 162 contentResolver.update(uri, contentValues, null, null); 163 } 164 165 /** 166 * Sends an update command to the voicemail content provider for a list of voicemails. From the 167 * view of the provider, since the updater is the owner of the entry, a blank "update" means that 168 * the voicemail source is indicating that the server has up-to-date information on the voicemail. 169 * This flips the "dirty" bit to "0". 170 * 171 * @param voicemails The list of voicemails to update 172 * @return The number of voicemails updated 173 */ 174 public int markCleanInDatabase(List<Voicemail> voicemails) { 175 int count = voicemails.size(); 176 for (int i = 0; i < count; i++) { 177 markCleanInDatabase(voicemails.get(i)); 178 } 179 return count; 180 } 181 182 /** Utility method to mark single message as clean. */ 183 public void markCleanInDatabase(Voicemail voicemail) { 184 Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId()); 185 ContentValues contentValues = new ContentValues(); 186 contentResolver.update(uri, contentValues, null, null); 187 } 188 189 /** Utility method to add a transcription to the voicemail. */ 190 public void updateWithTranscription(Voicemail voicemail, String transcription) { 191 Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId()); 192 ContentValues contentValues = new ContentValues(); 193 contentValues.put(Voicemails.TRANSCRIPTION, transcription); 194 contentResolver.update(uri, contentValues, null, null); 195 } 196 197 /** 198 * Voicemail is unique if the tuple of (phone account component name, phone account id, source 199 * data) is unique. If the phone account is missing, we also consider this unique since it's 200 * simply an "unknown" account. 201 * 202 * @param voicemail The voicemail to check if it is unique. 203 * @return {@code true} if the voicemail is unique, {@code false} otherwise. 204 */ 205 public boolean isVoicemailUnique(Voicemail voicemail) { 206 Cursor cursor = null; 207 PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount(); 208 if (phoneAccount != null) { 209 String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString(); 210 String phoneAccountId = phoneAccount.getId(); 211 String sourceData = voicemail.getSourceData(); 212 if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) { 213 return true; 214 } 215 try { 216 String whereClause = 217 Voicemails.PHONE_ACCOUNT_COMPONENT_NAME 218 + "=? AND " 219 + Voicemails.PHONE_ACCOUNT_ID 220 + "=? AND " 221 + Voicemails.SOURCE_DATA 222 + "=?"; 223 String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData}; 224 cursor = contentResolver.query(sourceUri, PROJECTION, whereClause, whereArgs, null); 225 if (cursor.getCount() == 0) { 226 return true; 227 } else { 228 return false; 229 } 230 } finally { 231 if (cursor != null) { 232 cursor.close(); 233 } 234 } 235 } 236 return true; 237 } 238 239 /** 240 * Marks voicemails in the local database as archived. This indicates that the voicemails from the 241 * server were removed automatically to make space for new voicemails, and are stored locally on 242 * the users devices, without a corresponding server copy. 243 */ 244 public void markArchivedInDatabase(List<Voicemail> voicemails) { 245 for (Voicemail voicemail : voicemails) { 246 markArchiveInDatabase(voicemail); 247 } 248 } 249 250 /** Utility method to mark single voicemail as archived. */ 251 public void markArchiveInDatabase(Voicemail voicemail) { 252 Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId()); 253 ContentValues contentValues = new ContentValues(); 254 contentValues.put(Voicemails.ARCHIVED, "1"); 255 contentResolver.update(uri, contentValues, null, null); 256 } 257 258 /** Find the oldest voicemails that are on the device, and also on the server. */ 259 @TargetApi(VERSION_CODES.M) // used for try with resources 260 public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) { 261 if (numVoicemails <= 0) { 262 Assert.fail("Query for remote voicemails cannot be <= 0"); 263 } 264 265 String sortAndLimit = "date ASC limit " + numVoicemails; 266 267 try (Cursor cursor = 268 contentResolver.query(sourceUri, PROJECTION, ARCHIVED_SELECTION, null, sortAndLimit)) { 269 270 Assert.isNotNull(cursor); 271 272 List<Voicemail> voicemails = new ArrayList<>(); 273 while (cursor.moveToNext()) { 274 final long id = cursor.getLong(_ID); 275 final String sourceData = cursor.getString(SOURCE_DATA); 276 Voicemail voicemail = Voicemail.createForUpdate(id, sourceData).build(); 277 voicemails.add(voicemail); 278 } 279 280 if (voicemails.size() != numVoicemails) { 281 Assert.fail( 282 String.format( 283 "voicemail count (%d) doesn't matched expected (%d)", 284 voicemails.size(), numVoicemails)); 285 } 286 return voicemails; 287 } 288 } 289 } 290