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