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.phone.vvm.omtp.sync; 17 18 import android.content.Context; 19 import android.net.Network; 20 import android.net.Uri; 21 import android.provider.VoicemailContract; 22 import android.telecom.PhoneAccountHandle; 23 import android.telecom.Voicemail; 24 import android.text.TextUtils; 25 import com.android.phone.Assert; 26 import com.android.phone.PhoneUtils; 27 import com.android.phone.VoicemailStatus; 28 import com.android.phone.settings.VisualVoicemailSettingsUtil; 29 import com.android.phone.vvm.omtp.ActivationTask; 30 import com.android.phone.vvm.omtp.OmtpEvents; 31 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 32 import com.android.phone.vvm.omtp.VvmLog; 33 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback; 34 import com.android.phone.vvm.omtp.imap.ImapHelper; 35 import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException; 36 import com.android.phone.vvm.omtp.scheduling.BaseTask; 37 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.NetworkWrapper; 38 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.RequestFailedException; 39 import com.android.phone.vvm.omtp.utils.PhoneAccountHandleConverter; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * Sync OMTP visual voicemail. 46 */ 47 public class OmtpVvmSyncService { 48 49 private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); 50 51 /** 52 * Signifies a sync with both uploading to the server and downloading from the server. 53 */ 54 public static final String SYNC_FULL_SYNC = "full_sync"; 55 /** 56 * Only upload to the server. 57 */ 58 public static final String SYNC_UPLOAD_ONLY = "upload_only"; 59 /** 60 * Only download from the server. 61 */ 62 public static final String SYNC_DOWNLOAD_ONLY = "download_only"; 63 /** 64 * Only download single voicemail transcription. 65 */ 66 public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = 67 "download_one_transcription"; 68 69 private final Context mContext; 70 71 // Record the timestamp of the last full sync so that duplicate syncs can be reduced. 72 private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp"; 73 // Constant indicating that there has never been a full sync. 74 public static final long NO_PRIOR_FULL_SYNC = -1; 75 76 private VoicemailsQueryHelper mQueryHelper; 77 78 public OmtpVvmSyncService(Context context) { 79 mContext = context; 80 mQueryHelper = new VoicemailsQueryHelper(mContext); 81 } 82 83 public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount, 84 Voicemail voicemail, VoicemailStatus.Editor status) { 85 Assert.isTrue(phoneAccount != null); 86 VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); 87 setupAndSendRequest(task, phoneAccount, voicemail, action, status); 88 } 89 90 private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount, 91 Voicemail voicemail, String action, VoicemailStatus.Editor status) { 92 if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) { 93 VvmLog.v(TAG, "Sync requested for disabled account"); 94 return; 95 } 96 int subId = PhoneAccountHandleConverter.toSubId(phoneAccount); 97 if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) { 98 ActivationTask.start(mContext, subId, null); 99 return; 100 } 101 102 OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, subId); 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(VoicemailStatus.edit(mContext, phoneAccount), 107 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(BaseTask task, Network network, PhoneAccountHandle phoneAccount, 122 Voicemail voicemail, String action, VoicemailStatus.Editor status) { 123 try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) { 124 boolean success; 125 if (voicemail == null) { 126 success = syncAll(action, imapHelper, phoneAccount); 127 } else { 128 success = syncOne(imapHelper, voicemail, phoneAccount); 129 } 130 if (success) { 131 // TODO: b/30569269 failure should interrupt all subsequent task via exceptions 132 imapHelper.updateQuota(); 133 imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED); 134 } else { 135 task.fail(); 136 } 137 } catch (InitializingException e) { 138 VvmLog.w(TAG, "Can't retrieve Imap credentials.", e); 139 return; 140 } 141 } 142 143 private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) { 144 boolean uploadSuccess = true; 145 boolean downloadSuccess = true; 146 147 if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { 148 uploadSuccess = upload(imapHelper); 149 } 150 if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { 151 downloadSuccess = download(imapHelper, account); 152 } 153 154 VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess) 155 + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]"); 156 157 return uploadSuccess && downloadSuccess; 158 } 159 160 private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, 161 PhoneAccountHandle account) { 162 if (shouldPerformPrefetch(account, imapHelper)) { 163 VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext, 164 voicemail.getUri(), account); 165 imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData()); 166 } 167 168 return imapHelper.fetchTranscription( 169 new TranscriptionFetchedCallback(mContext, voicemail), 170 voicemail.getSourceData()); 171 } 172 173 private boolean upload(ImapHelper imapHelper) { 174 List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(); 175 List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(); 176 177 boolean success = true; 178 179 if (deletedVoicemails.size() > 0) { 180 if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { 181 // We want to delete selectively instead of all the voicemails for this provider 182 // in case the state changed since the IMAP query was completed. 183 mQueryHelper.deleteFromDatabase(deletedVoicemails); 184 } else { 185 success = false; 186 } 187 } 188 189 if (readVoicemails.size() > 0) { 190 if (imapHelper.markMessagesAsRead(readVoicemails)) { 191 mQueryHelper.markCleanInDatabase(readVoicemails); 192 } else { 193 success = false; 194 } 195 } 196 197 return success; 198 } 199 200 private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) { 201 List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails(); 202 List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(); 203 204 if (localVoicemails == null || serverVoicemails == null) { 205 // Null value means the query failed. 206 return false; 207 } 208 209 Map<String, Voicemail> remoteMap = buildMap(serverVoicemails); 210 211 // Go through all the local voicemails and check if they are on the server. 212 // They may be read or deleted on the server but not locally. Perform the 213 // appropriate local operation if the status differs from the server. Remove 214 // the messages that exist both locally and on the server to know which server 215 // messages to insert locally. 216 for (int i = 0; i < localVoicemails.size(); i++) { 217 Voicemail localVoicemail = localVoicemails.get(i); 218 Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); 219 if (remoteVoicemail == null) { 220 mQueryHelper.deleteFromDatabase(localVoicemail); 221 } else { 222 if (remoteVoicemail.isRead() != localVoicemail.isRead()) { 223 mQueryHelper.markReadInDatabase(localVoicemail); 224 } 225 226 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) && 227 TextUtils.isEmpty(localVoicemail.getTranscription())) { 228 mQueryHelper.updateWithTranscription(localVoicemail, 229 remoteVoicemail.getTranscription()); 230 } 231 } 232 } 233 234 // The leftover messages are messages that exist on the server but not locally. 235 boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper); 236 for (Voicemail remoteVoicemail : remoteMap.values()) { 237 Uri uri = VoicemailContract.Voicemails.insert(mContext, remoteVoicemail); 238 if (prefetchEnabled) { 239 VoicemailFetchedCallback fetchedCallback = 240 new VoicemailFetchedCallback(mContext, uri, account); 241 imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData()); 242 } 243 } 244 245 return true; 246 } 247 248 private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) { 249 OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper( 250 mContext, PhoneUtils.getSubIdForPhoneAccountHandle(account)); 251 return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming(); 252 } 253 254 /** 255 * Builds a map from provider data to message for the given collection of voicemails. 256 */ 257 private Map<String, Voicemail> buildMap(List<Voicemail> messages) { 258 Map<String, Voicemail> map = new HashMap<String, Voicemail>(); 259 for (Voicemail message : messages) { 260 map.put(message.getSourceData(), message); 261 } 262 return map; 263 } 264 265 public class TranscriptionFetchedCallback { 266 267 private Context mContext; 268 private Voicemail mVoicemail; 269 270 public TranscriptionFetchedCallback(Context context, Voicemail voicemail) { 271 mContext = context; 272 mVoicemail = voicemail; 273 } 274 275 public void setVoicemailTranscription(String transcription) { 276 VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); 277 queryHelper.updateWithTranscription(mVoicemail, transcription); 278 } 279 } 280 } 281