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