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