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.Context; 20 import android.net.Network; 21 import android.net.Uri; 22 import android.os.Build.VERSION_CODES; 23 import android.support.v4.os.BuildCompat; 24 import android.telecom.PhoneAccountHandle; 25 import android.text.TextUtils; 26 import android.util.ArrayMap; 27 import com.android.dialer.logging.DialerImpression; 28 import com.android.voicemail.VoicemailComponent; 29 import com.android.voicemail.impl.ActivationTask; 30 import com.android.voicemail.impl.Assert; 31 import com.android.voicemail.impl.OmtpEvents; 32 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper; 33 import com.android.voicemail.impl.Voicemail; 34 import com.android.voicemail.impl.VoicemailStatus; 35 import com.android.voicemail.impl.VvmLog; 36 import com.android.voicemail.impl.fetch.VoicemailFetchedCallback; 37 import com.android.voicemail.impl.imap.ImapHelper; 38 import com.android.voicemail.impl.imap.ImapHelper.InitializingException; 39 import com.android.voicemail.impl.mail.store.ImapFolder.Quota; 40 import com.android.voicemail.impl.scheduling.BaseTask; 41 import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; 42 import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper; 43 import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException; 44 import com.android.voicemail.impl.utils.LoggerUtils; 45 import com.android.voicemail.impl.utils.VoicemailDatabaseUtil; 46 import java.util.List; 47 import java.util.Map; 48 49 /** Sync OMTP visual voicemail. */ 50 @TargetApi(VERSION_CODES.O) 51 public class OmtpVvmSyncService { 52 53 private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); 54 55 /** Signifies a sync with both uploading to the server and downloading from the server. */ 56 public static final String SYNC_FULL_SYNC = "full_sync"; 57 /** Only upload to the server. */ 58 public static final String SYNC_UPLOAD_ONLY = "upload_only"; 59 /** Only download from the server. */ 60 public static final String SYNC_DOWNLOAD_ONLY = "download_only"; 61 /** Only download single voicemail transcription. */ 62 public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription"; 63 /** Threshold for whether we should archive and delete voicemails from the remote VM server. */ 64 private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f; 65 66 private final Context mContext; 67 68 private VoicemailsQueryHelper mQueryHelper; 69 70 public OmtpVvmSyncService(Context context) { 71 mContext = context; 72 mQueryHelper = new VoicemailsQueryHelper(mContext); 73 } 74 75 public void sync( 76 BaseTask task, 77 String action, 78 PhoneAccountHandle phoneAccount, 79 Voicemail voicemail, 80 VoicemailStatus.Editor status) { 81 Assert.isTrue(phoneAccount != null); 82 VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); 83 setupAndSendRequest(task, phoneAccount, voicemail, action, status); 84 } 85 86 private void setupAndSendRequest( 87 BaseTask task, 88 PhoneAccountHandle phoneAccount, 89 Voicemail voicemail, 90 String action, 91 VoicemailStatus.Editor status) { 92 if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) { 93 VvmLog.v(TAG, "Sync requested for disabled account"); 94 return; 95 } 96 if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) { 97 ActivationTask.start(mContext, phoneAccount, null); 98 return; 99 } 100 101 OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount); 102 LoggerUtils.logImpressionOnMainThread(mContext, DialerImpression.Type.VVM_SYNC_STARTED); 103 // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data 104 // channel errors, which should happen when the task starts, not when it ends. It is the 105 // "Sync in progress..." status. 106 config.handleEvent( 107 VoicemailStatus.edit(mContext, phoneAccount), OmtpEvents.DATA_IMAP_OPERATION_STARTED); 108 try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) { 109 if (network == null) { 110 VvmLog.e(TAG, "unable to acquire network"); 111 task.fail(); 112 return; 113 } 114 doSync(task, network.get(), phoneAccount, voicemail, action, status); 115 } catch (RequestFailedException e) { 116 config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); 117 task.fail(); 118 } 119 } 120 121 private void doSync( 122 BaseTask task, 123 Network network, 124 PhoneAccountHandle phoneAccount, 125 Voicemail voicemail, 126 String action, 127 VoicemailStatus.Editor status) { 128 try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) { 129 boolean success; 130 if (voicemail == null) { 131 success = syncAll(action, imapHelper, phoneAccount); 132 } else { 133 success = syncOne(imapHelper, voicemail, phoneAccount); 134 } 135 if (success) { 136 // TODO: b/30569269 failure should interrupt all subsequent task via exceptions 137 imapHelper.updateQuota(); 138 autoDeleteAndArchiveVM(imapHelper, phoneAccount); 139 imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED); 140 LoggerUtils.logImpressionOnMainThread(mContext, DialerImpression.Type.VVM_SYNC_COMPLETED); 141 } else { 142 task.fail(); 143 } 144 } catch (InitializingException e) { 145 VvmLog.w(TAG, "Can't retrieve Imap credentials.", e); 146 return; 147 } 148 } 149 150 /** 151 * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs 152 * and delete them from the server to ensure new VMs can be received. 153 */ 154 private void autoDeleteAndArchiveVM( 155 ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) { 156 if (!isArchiveAllowedAndEnabled(mContext, phoneAccountHandle)) { 157 VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off"); 158 LoggerUtils.logImpressionOnMainThread( 159 mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF); 160 return; 161 } 162 Quota quotaOnServer = imapHelper.getQuota(); 163 if (quotaOnServer == null) { 164 LoggerUtils.logImpressionOnMainThread( 165 mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_FAILED_DUE_TO_FAILED_QUOTA_CHECK); 166 VvmLog.e(TAG, "autoDeleteAndArchiveVM failed - Can't retrieve Imap quota."); 167 return; 168 } 169 170 if ((float) quotaOnServer.occupied / (float) quotaOnServer.total 171 > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) { 172 deleteAndArchiveVM(imapHelper, quotaOnServer); 173 imapHelper.updateQuota(); 174 LoggerUtils.logImpressionOnMainThread( 175 mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER); 176 } else { 177 VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold"); 178 } 179 } 180 181 private static boolean isArchiveAllowedAndEnabled( 182 Context context, PhoneAccountHandle phoneAccountHandle) { 183 184 if (!VoicemailComponent.get(context) 185 .getVoicemailClient() 186 .isVoicemailArchiveAvailable(context)) { 187 VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is not available"); 188 return false; 189 } 190 if (!VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)) { 191 VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is turned off"); 192 return false; 193 } 194 if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle)) { 195 VvmLog.i("isArchiveAllowedAndEnabled", "voicemail is turned off"); 196 return false; 197 } 198 return true; 199 } 200 201 private void deleteAndArchiveVM(ImapHelper imapHelper, Quota quotaOnServer) { 202 // Archive column should only be used for 0 and above 203 Assert.isTrue(BuildCompat.isAtLeastO()); 204 205 // The number of voicemails that exceed our threshold and should be deleted from the server 206 int numVoicemails = 207 quotaOnServer.occupied - (int) (AUTO_DELETE_ARCHIVE_VM_THRESHOLD * quotaOnServer.total); 208 List<Voicemail> oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails); 209 VvmLog.w(TAG, "number of voicemails to delete " + numVoicemails); 210 if (!oldestVoicemails.isEmpty()) { 211 mQueryHelper.markArchivedInDatabase(oldestVoicemails); 212 imapHelper.markMessagesAsDeleted(oldestVoicemails); 213 VvmLog.i( 214 TAG, 215 String.format( 216 "successfully archived and deleted %d voicemails", oldestVoicemails.size())); 217 } else { 218 VvmLog.w(TAG, "remote voicemail server is empty"); 219 } 220 } 221 222 private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) { 223 boolean uploadSuccess = true; 224 boolean downloadSuccess = true; 225 226 if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { 227 uploadSuccess = upload(account, imapHelper); 228 } 229 if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { 230 downloadSuccess = download(imapHelper, account); 231 } 232 233 VvmLog.v( 234 TAG, 235 "upload succeeded: [" 236 + String.valueOf(uploadSuccess) 237 + "] download succeeded: [" 238 + String.valueOf(downloadSuccess) 239 + "]"); 240 241 return uploadSuccess && downloadSuccess; 242 } 243 244 private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) { 245 if (shouldPerformPrefetch(account, imapHelper)) { 246 VoicemailFetchedCallback callback = 247 new VoicemailFetchedCallback(mContext, voicemail.getUri(), account); 248 imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData()); 249 } 250 251 return imapHelper.fetchTranscription( 252 new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData()); 253 } 254 255 private boolean upload(PhoneAccountHandle phoneAccountHandle, ImapHelper imapHelper) { 256 List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(phoneAccountHandle); 257 List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(phoneAccountHandle); 258 259 boolean success = true; 260 261 if (deletedVoicemails.size() > 0) { 262 if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { 263 // We want to delete selectively instead of all the voicemails for this provider 264 // in case the state changed since the IMAP query was completed. 265 mQueryHelper.deleteFromDatabase(deletedVoicemails); 266 } else { 267 success = false; 268 } 269 } 270 271 if (readVoicemails.size() > 0) { 272 VvmLog.i(TAG, "Marking voicemails as read"); 273 if (imapHelper.markMessagesAsRead(readVoicemails)) { 274 VvmLog.i(TAG, "Marking voicemails as clean"); 275 mQueryHelper.markCleanInDatabase(readVoicemails); 276 } else { 277 success = false; 278 } 279 } 280 281 return success; 282 } 283 284 private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) { 285 List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails(); 286 List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(account); 287 288 if (localVoicemails == null || serverVoicemails == null) { 289 // Null value means the query failed. 290 return false; 291 } 292 293 Map<String, Voicemail> remoteMap = buildMap(serverVoicemails); 294 295 // Go through all the local voicemails and check if they are on the server. 296 // They may be read or deleted on the server but not locally. Perform the 297 // appropriate local operation if the status differs from the server. Remove 298 // the messages that exist both locally and on the server to know which server 299 // messages to insert locally. 300 // Voicemails that were removed automatically from the server, are marked as 301 // archived and are stored locally. We do not delete them, as they were removed from the server 302 // by design (to make space). 303 for (int i = 0; i < localVoicemails.size(); i++) { 304 Voicemail localVoicemail = localVoicemails.get(i); 305 Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); 306 307 // Do not delete voicemails that are archived marked as archived. 308 if (remoteVoicemail == null) { 309 mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail); 310 } else { 311 if (remoteVoicemail.isRead() && !localVoicemail.isRead()) { 312 mQueryHelper.markReadInDatabase(localVoicemail); 313 } 314 315 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) 316 && TextUtils.isEmpty(localVoicemail.getTranscription())) { 317 LoggerUtils.logImpressionOnMainThread( 318 mContext, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED); 319 mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription()); 320 } 321 } 322 } 323 324 // The leftover messages are messages that exist on the server but not locally. 325 boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper); 326 for (Voicemail remoteVoicemail : remoteMap.values()) { 327 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())) { 328 LoggerUtils.logImpressionOnMainThread( 329 mContext, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED); 330 } 331 Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail); 332 if (prefetchEnabled) { 333 VoicemailFetchedCallback fetchedCallback = 334 new VoicemailFetchedCallback(mContext, uri, account); 335 imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData()); 336 } 337 } 338 339 return true; 340 } 341 342 private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) { 343 OmtpVvmCarrierConfigHelper carrierConfigHelper = 344 new OmtpVvmCarrierConfigHelper(mContext, account); 345 return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming(); 346 } 347 348 /** Builds a map from provider data to message for the given collection of voicemails. */ 349 private Map<String, Voicemail> buildMap(List<Voicemail> messages) { 350 Map<String, Voicemail> map = new ArrayMap<String, Voicemail>(); 351 for (Voicemail message : messages) { 352 map.put(message.getSourceData(), message); 353 } 354 return map; 355 } 356 357 /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */ 358 public static class TranscriptionFetchedCallback { 359 360 private Context mContext; 361 private Voicemail mVoicemail; 362 363 public TranscriptionFetchedCallback(Context context, Voicemail voicemail) { 364 mContext = context; 365 mVoicemail = voicemail; 366 } 367 368 public void setVoicemailTranscription(String transcription) { 369 VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); 370 queryHelper.updateWithTranscription(mVoicemail, transcription); 371 } 372 } 373 } 374