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