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.app.AlarmManager; 19 import android.app.IntentService; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.net.Network; 24 import android.net.NetworkInfo; 25 import android.net.Uri; 26 import android.provider.VoicemailContract; 27 import android.provider.VoicemailContract.Status; 28 import android.telecom.PhoneAccountHandle; 29 import android.telecom.Voicemail; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import com.android.phone.PhoneUtils; 34 import com.android.phone.VoicemailUtils; 35 import com.android.phone.settings.VisualVoicemailSettingsUtil; 36 import com.android.phone.vvm.omtp.LocalLogHelper; 37 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 38 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback; 39 import com.android.phone.vvm.omtp.imap.ImapHelper; 40 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 46 /** 47 * Sync OMTP visual voicemail. 48 */ 49 public class OmtpVvmSyncService extends IntentService { 50 51 private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); 52 53 // Number of retries 54 private static final int NETWORK_RETRY_COUNT = 3; 55 56 /** 57 * Signifies a sync with both uploading to the server and downloading from the server. 58 */ 59 public static final String SYNC_FULL_SYNC = "full_sync"; 60 /** 61 * Only upload to the server. 62 */ 63 public static final String SYNC_UPLOAD_ONLY = "upload_only"; 64 /** 65 * Only download from the server. 66 */ 67 public static final String SYNC_DOWNLOAD_ONLY = "download_only"; 68 /** 69 * Only download single voicemail transcription. 70 */ 71 public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = 72 "download_one_transcription"; 73 /** 74 * The account to sync. 75 */ 76 public static final String EXTRA_PHONE_ACCOUNT = "phone_account"; 77 /** 78 * The voicemail to fetch. 79 */ 80 public static final String EXTRA_VOICEMAIL = "voicemail"; 81 /** 82 * The sync request is initiated by the user, should allow shorter sync interval. 83 */ 84 public static final String EXTRA_IS_MANUAL_SYNC = "is_manual_sync"; 85 // Minimum time allowed between full syncs 86 private static final int MINIMUM_FULL_SYNC_INTERVAL_MILLIS = 60 * 1000; 87 88 // Minimum time allowed between manual syncs 89 private static final int MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS = 3 * 1000; 90 91 private VoicemailsQueryHelper mQueryHelper; 92 OmtpVvmSyncService()93 public OmtpVvmSyncService() { 94 super("OmtpVvmSyncService"); 95 } 96 getSyncIntent(Context context, String action, PhoneAccountHandle phoneAccount, boolean firstAttempt)97 public static Intent getSyncIntent(Context context, String action, 98 PhoneAccountHandle phoneAccount, boolean firstAttempt) { 99 return getSyncIntent(context, action, phoneAccount, null, firstAttempt); 100 } 101 getSyncIntent(Context context, String action, PhoneAccountHandle phoneAccount, Voicemail voicemail, boolean firstAttempt)102 public static Intent getSyncIntent(Context context, String action, 103 PhoneAccountHandle phoneAccount, Voicemail voicemail, boolean firstAttempt) { 104 if (firstAttempt) { 105 if (phoneAccount != null) { 106 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, 107 phoneAccount); 108 } else { 109 OmtpVvmSourceManager vvmSourceManager = 110 OmtpVvmSourceManager.getInstance(context); 111 Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources(); 112 for (PhoneAccountHandle source : sources) { 113 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, source); 114 } 115 } 116 } 117 118 Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class); 119 serviceIntent.setAction(action); 120 if (phoneAccount != null) { 121 serviceIntent.putExtra(EXTRA_PHONE_ACCOUNT, phoneAccount); 122 } 123 if (voicemail != null) { 124 serviceIntent.putExtra(EXTRA_VOICEMAIL, voicemail); 125 } 126 127 cancelRetriesForIntent(context, serviceIntent); 128 return serviceIntent; 129 } 130 131 /** 132 * Cancel all retry syncs for an account. 133 * 134 * @param context The context the service runs in. 135 * @param phoneAccount The phone account for which to cancel syncs. 136 */ cancelAllRetries(Context context, PhoneAccountHandle phoneAccount)137 public static void cancelAllRetries(Context context, PhoneAccountHandle phoneAccount) { 138 cancelRetriesForIntent(context, getSyncIntent(context, SYNC_FULL_SYNC, phoneAccount, 139 false)); 140 } 141 142 /** 143 * A helper method to cancel all pending alarms for intents that would be identical to the given 144 * intent. 145 * 146 * @param context The context the service runs in. 147 * @param intent The intent to search and cancel. 148 */ cancelRetriesForIntent(Context context, Intent intent)149 private static void cancelRetriesForIntent(Context context, Intent intent) { 150 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 151 alarmManager.cancel(PendingIntent.getService(context, 0, intent, 0)); 152 153 Intent copyIntent = new Intent(intent); 154 if (SYNC_FULL_SYNC.equals(copyIntent.getAction())) { 155 // A full sync action should also cancel both of the other types of syncs 156 copyIntent.setAction(SYNC_DOWNLOAD_ONLY); 157 alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0)); 158 copyIntent.setAction(SYNC_UPLOAD_ONLY); 159 alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0)); 160 } 161 } 162 163 @Override onCreate()164 public void onCreate() { 165 super.onCreate(); 166 mQueryHelper = new VoicemailsQueryHelper(this); 167 } 168 169 @Override onHandleIntent(Intent intent)170 protected void onHandleIntent(Intent intent) { 171 if (intent == null) { 172 Log.d(TAG, "onHandleIntent: could not handle null intent"); 173 return; 174 } 175 String action = intent.getAction(); 176 PhoneAccountHandle phoneAccount = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT); 177 LocalLogHelper.log(TAG, "Sync requested: " + action + 178 " for all accounts: " + String.valueOf(phoneAccount == null)); 179 180 boolean isManualSync = intent.getBooleanExtra(EXTRA_IS_MANUAL_SYNC, false); 181 Voicemail voicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL); 182 if (phoneAccount != null) { 183 Log.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); 184 setupAndSendRequest(phoneAccount, voicemail, action, isManualSync); 185 } else { 186 Log.v(TAG, "Sync requested: " + action + " - for all accounts"); 187 OmtpVvmSourceManager vvmSourceManager = 188 OmtpVvmSourceManager.getInstance(this); 189 Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources(); 190 for (PhoneAccountHandle source : sources) { 191 setupAndSendRequest(source, null, action, isManualSync); 192 } 193 } 194 } 195 setupAndSendRequest(PhoneAccountHandle phoneAccount, Voicemail voicemail, String action, boolean isManualSync)196 private void setupAndSendRequest(PhoneAccountHandle phoneAccount, Voicemail voicemail, 197 String action, boolean isManualSync) { 198 if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount)) { 199 Log.v(TAG, "Sync requested for disabled account"); 200 return; 201 } 202 203 if (SYNC_FULL_SYNC.equals(action)) { 204 long lastSyncTime = VisualVoicemailSettingsUtil.getVisualVoicemailLastFullSyncTime( 205 this, phoneAccount); 206 long currentTime = System.currentTimeMillis(); 207 int minimumInterval = isManualSync ? MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS 208 : MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS; 209 if (currentTime - lastSyncTime < minimumInterval) { 210 // If it's been less than a minute since the last sync, bail. 211 Log.v(TAG, "Avoiding duplicate full sync: synced recently for " 212 + phoneAccount.getId()); 213 214 /** 215 * Perform a NOOP change to the database so the sender can observe the sync is 216 * completed. 217 * TODO: Instead of this hack, refactor the sync to be synchronous so the sender 218 * can use sendOrderedBroadcast() to register a callback once all syncs are 219 * finished 220 * b/26937720 221 */ 222 Status.setStatus(this, phoneAccount, 223 Status.CONFIGURATION_STATE_IGNORE, 224 Status.DATA_CHANNEL_STATE_IGNORE, 225 Status.NOTIFICATION_CHANNEL_STATE_IGNORE); 226 return; 227 } 228 VisualVoicemailSettingsUtil.setVisualVoicemailLastFullSyncTime( 229 this, phoneAccount, currentTime); 230 } 231 232 VvmNetworkRequestCallback networkCallback = new SyncNetworkRequestCallback(this, 233 phoneAccount, voicemail, action); 234 networkCallback.requestNetwork(); 235 } 236 doSync(Network network, VvmNetworkRequestCallback callback, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action)237 private void doSync(Network network, VvmNetworkRequestCallback callback, 238 PhoneAccountHandle phoneAccount, Voicemail voicemail, String action) { 239 int retryCount = NETWORK_RETRY_COUNT; 240 try { 241 while (retryCount > 0) { 242 ImapHelper imapHelper = new ImapHelper(this, phoneAccount, network); 243 if (!imapHelper.isSuccessfullyInitialized()) { 244 Log.w(TAG, "Can't retrieve Imap credentials."); 245 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this, 246 phoneAccount); 247 return; 248 } 249 250 boolean success = true; 251 if (voicemail == null) { 252 success = syncAll(action, imapHelper, phoneAccount); 253 } else { 254 success = syncOne(imapHelper, voicemail, phoneAccount); 255 } 256 imapHelper.updateQuota(); 257 258 // Need to check again for whether visual voicemail is enabled because it could have 259 // been disabled while waiting for the response from the network. 260 if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount) && 261 !success) { 262 retryCount--; 263 Log.v(TAG, "Retrying " + action); 264 } else { 265 // Nothing more to do here, just exit. 266 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this, 267 phoneAccount); 268 VoicemailUtils.setDataChannelState( 269 this, phoneAccount, Status.DATA_CHANNEL_STATE_OK); 270 return; 271 } 272 } 273 } finally { 274 if (callback != null) { 275 callback.releaseNetwork(); 276 } 277 } 278 } 279 syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account)280 private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) { 281 boolean uploadSuccess = true; 282 boolean downloadSuccess = true; 283 284 if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { 285 uploadSuccess = upload(imapHelper); 286 } 287 if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { 288 downloadSuccess = download(imapHelper, account); 289 } 290 291 Log.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess) 292 + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]"); 293 294 boolean success = uploadSuccess && downloadSuccess; 295 if (!uploadSuccess || !downloadSuccess) { 296 if (uploadSuccess) { 297 action = SYNC_DOWNLOAD_ONLY; 298 } else if (downloadSuccess) { 299 action = SYNC_UPLOAD_ONLY; 300 } 301 } 302 303 return success; 304 } 305 syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account)306 private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, 307 PhoneAccountHandle account) { 308 if (shouldPerformPrefetch(account, imapHelper)) { 309 VoicemailFetchedCallback callback = new VoicemailFetchedCallback(this, 310 voicemail.getUri()); 311 imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData()); 312 } 313 314 return imapHelper.fetchTranscription( 315 new TranscriptionFetchedCallback(this, voicemail), 316 voicemail.getSourceData()); 317 } 318 319 private class SyncNetworkRequestCallback extends VvmNetworkRequestCallback { 320 321 Voicemail mVoicemail; 322 private String mAction; 323 SyncNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action)324 public SyncNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount, 325 Voicemail voicemail, String action) { 326 super(context, phoneAccount); 327 mAction = action; 328 mVoicemail = voicemail; 329 } 330 331 @Override onAvailable(Network network)332 public void onAvailable(Network network) { 333 super.onAvailable(network); 334 NetworkInfo info = getConnectivityManager().getNetworkInfo(network); 335 if (info == null) { 336 Log.d(TAG, "Network Type: Unknown"); 337 } else { 338 Log.d(TAG, "Network Type: " + info.getTypeName()); 339 } 340 341 doSync(network, this, mPhoneAccount, mVoicemail, mAction); 342 } 343 344 } 345 upload(ImapHelper imapHelper)346 private boolean upload(ImapHelper imapHelper) { 347 List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(); 348 List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(); 349 350 boolean success = true; 351 352 if (deletedVoicemails.size() > 0) { 353 if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { 354 // We want to delete selectively instead of all the voicemails for this provider 355 // in case the state changed since the IMAP query was completed. 356 mQueryHelper.deleteFromDatabase(deletedVoicemails); 357 } else { 358 success = false; 359 } 360 } 361 362 if (readVoicemails.size() > 0) { 363 if (imapHelper.markMessagesAsRead(readVoicemails)) { 364 mQueryHelper.markReadInDatabase(readVoicemails); 365 } else { 366 success = false; 367 } 368 } 369 370 return success; 371 } 372 download(ImapHelper imapHelper, PhoneAccountHandle account)373 private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) { 374 List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails(); 375 List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(); 376 377 if (localVoicemails == null || serverVoicemails == null) { 378 // Null value means the query failed. 379 return false; 380 } 381 382 Map<String, Voicemail> remoteMap = buildMap(serverVoicemails); 383 384 // Go through all the local voicemails and check if they are on the server. 385 // They may be read or deleted on the server but not locally. Perform the 386 // appropriate local operation if the status differs from the server. Remove 387 // the messages that exist both locally and on the server to know which server 388 // messages to insert locally. 389 for (int i = 0; i < localVoicemails.size(); i++) { 390 Voicemail localVoicemail = localVoicemails.get(i); 391 Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); 392 if (remoteVoicemail == null) { 393 mQueryHelper.deleteFromDatabase(localVoicemail); 394 } else { 395 if (remoteVoicemail.isRead() != localVoicemail.isRead()) { 396 mQueryHelper.markReadInDatabase(localVoicemail); 397 } 398 399 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) && 400 TextUtils.isEmpty(localVoicemail.getTranscription())) { 401 mQueryHelper.updateWithTranscription(localVoicemail, 402 remoteVoicemail.getTranscription()); 403 } 404 } 405 } 406 407 // The leftover messages are messages that exist on the server but not locally. 408 boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper); 409 for (Voicemail remoteVoicemail : remoteMap.values()) { 410 Uri uri = VoicemailContract.Voicemails.insert(this, remoteVoicemail); 411 if (prefetchEnabled) { 412 VoicemailFetchedCallback fetchedCallback = new VoicemailFetchedCallback(this, uri); 413 imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData()); 414 } 415 } 416 417 return true; 418 } 419 shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper)420 private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) { 421 OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper( 422 this, PhoneUtils.getSubIdForPhoneAccountHandle(account)); 423 return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming(); 424 } 425 setRetryAlarm(PhoneAccountHandle phoneAccount, String action)426 protected void setRetryAlarm(PhoneAccountHandle phoneAccount, String action) { 427 Intent serviceIntent = new Intent(this, OmtpVvmSyncService.class); 428 serviceIntent.setAction(action); 429 serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_PHONE_ACCOUNT, phoneAccount); 430 PendingIntent pendingIntent = PendingIntent.getService(this, 0, serviceIntent, 0); 431 long retryInterval = VisualVoicemailSettingsUtil.getVisualVoicemailRetryInterval(this, 432 phoneAccount); 433 434 Log.v(TAG, "Retrying " + action + " in " + retryInterval + "ms"); 435 436 AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); 437 alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + retryInterval, 438 pendingIntent); 439 440 VisualVoicemailSettingsUtil.setVisualVoicemailRetryInterval(this, phoneAccount, 441 retryInterval * 2); 442 } 443 444 /** 445 * Builds a map from provider data to message for the given collection of voicemails. 446 */ buildMap(List<Voicemail> messages)447 private Map<String, Voicemail> buildMap(List<Voicemail> messages) { 448 Map<String, Voicemail> map = new HashMap<String, Voicemail>(); 449 for (Voicemail message : messages) { 450 map.put(message.getSourceData(), message); 451 } 452 return map; 453 } 454 455 public class TranscriptionFetchedCallback { 456 457 private Context mContext; 458 private Voicemail mVoicemail; 459 TranscriptionFetchedCallback(Context context, Voicemail voicemail)460 public TranscriptionFetchedCallback(Context context, Voicemail voicemail) { 461 mContext = context; 462 mVoicemail = voicemail; 463 } 464 setVoicemailTranscription(String transcription)465 public void setVoicemailTranscription(String transcription) { 466 VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); 467 queryHelper.updateWithTranscription(mVoicemail, transcription); 468 } 469 } 470 } 471