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.phone.vvm.omtp.sync; 17 18import android.app.AlarmManager; 19import android.app.IntentService; 20import android.app.PendingIntent; 21import android.content.Context; 22import android.content.Intent; 23import android.net.ConnectivityManager; 24import android.net.Network; 25import android.net.ConnectivityManager.NetworkCallback; 26import android.net.NetworkCapabilities; 27import android.net.NetworkRequest; 28import android.provider.VoicemailContract; 29import android.telecom.PhoneAccountHandle; 30import android.telecom.Voicemail; 31import android.telephony.TelephonyManager; 32import android.util.Log; 33 34import com.android.phone.PhoneUtils; 35import com.android.phone.settings.VisualVoicemailSettingsUtil; 36import com.android.phone.vvm.omtp.LocalLogHelper; 37import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 38import com.android.phone.vvm.omtp.imap.ImapHelper; 39 40import java.util.HashMap; 41import java.util.List; 42import java.util.Map; 43import java.util.Set; 44 45/** 46 * Sync OMTP visual voicemail. 47 */ 48public class OmtpVvmSyncService extends IntentService { 49 private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); 50 51 /** Signifies a sync with both uploading to the server and downloading from the server. */ 52 public static final String SYNC_FULL_SYNC = "full_sync"; 53 /** Only upload to the server. */ 54 public static final String SYNC_UPLOAD_ONLY = "upload_only"; 55 /** Only download from the server. */ 56 public static final String SYNC_DOWNLOAD_ONLY = "download_only"; 57 /** The account to sync. */ 58 public static final String EXTRA_PHONE_ACCOUNT = "phone_account"; 59 60 // Timeout used to call ConnectivityManager.requestNetwork 61 private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000; 62 63 // Minimum time allowed between full syncs 64 private static final int MINIMUM_FULL_SYNC_INTERVAL_MILLIS = 60 * 1000; 65 66 // Number of retries 67 private static final int NETWORK_RETRY_COUNT = 6; 68 69 private VoicemailsQueryHelper mQueryHelper; 70 private ConnectivityManager mConnectivityManager; 71 72 public OmtpVvmSyncService() { 73 super("OmtpVvmSyncService"); 74 } 75 76 public static Intent getSyncIntent(Context context, String action, 77 PhoneAccountHandle phoneAccount, boolean firstAttempt) { 78 if (firstAttempt) { 79 if (phoneAccount != null) { 80 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, 81 phoneAccount); 82 } else { 83 OmtpVvmSourceManager vvmSourceManager = 84 OmtpVvmSourceManager.getInstance(context); 85 Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources(); 86 for (PhoneAccountHandle source : sources) { 87 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, source); 88 } 89 } 90 } 91 92 Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class); 93 serviceIntent.setAction(action); 94 if (phoneAccount != null) { 95 serviceIntent.putExtra(EXTRA_PHONE_ACCOUNT, phoneAccount); 96 } 97 98 cancelRetriesForIntent(context, serviceIntent); 99 return serviceIntent; 100 } 101 102 /** 103 * Cancel all retry syncs for an account. 104 * @param context The context the service runs in. 105 * @param phoneAccount The phone account for which to cancel syncs. 106 */ 107 public static void cancelAllRetries(Context context, PhoneAccountHandle phoneAccount) { 108 cancelRetriesForIntent(context, getSyncIntent(context, SYNC_FULL_SYNC, phoneAccount, 109 false)); 110 } 111 112 /** 113 * A helper method to cancel all pending alarms for intents that would be identical to the given 114 * intent. 115 * @param context The context the service runs in. 116 * @param intent The intent to search and cancel. 117 */ 118 private static void cancelRetriesForIntent(Context context, Intent intent) { 119 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 120 alarmManager.cancel(PendingIntent.getService(context, 0, intent, 0)); 121 122 Intent copyIntent = new Intent(intent); 123 if (SYNC_FULL_SYNC.equals(copyIntent.getAction())) { 124 // A full sync action should also cancel both of the other types of syncs 125 copyIntent.setAction(SYNC_DOWNLOAD_ONLY); 126 alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0)); 127 copyIntent.setAction(SYNC_UPLOAD_ONLY); 128 alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0)); 129 } 130 } 131 132 @Override 133 public void onCreate() { 134 super.onCreate(); 135 mQueryHelper = new VoicemailsQueryHelper(this); 136 } 137 138 @Override 139 protected void onHandleIntent(Intent intent) { 140 if (intent == null) { 141 Log.d(TAG, "onHandleIntent: could not handle null intent"); 142 return; 143 } 144 145 String action = intent.getAction(); 146 147 PhoneAccountHandle phoneAccount = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT); 148 149 LocalLogHelper.log(TAG, "Sync requested: " + action + 150 " for all accounts: " + String.valueOf(phoneAccount == null)); 151 152 if (phoneAccount != null) { 153 Log.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); 154 setupAndSendRequest(phoneAccount, action); 155 } else { 156 Log.v(TAG, "Sync requested: " + action + " - for all accounts"); 157 OmtpVvmSourceManager vvmSourceManager = 158 OmtpVvmSourceManager.getInstance(this); 159 Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources(); 160 for (PhoneAccountHandle source : sources) { 161 setupAndSendRequest(source, action); 162 } 163 } 164 } 165 166 private void setupAndSendRequest(PhoneAccountHandle phoneAccount, String action) { 167 if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount)) { 168 Log.v(TAG, "Sync requested for disabled account"); 169 return; 170 } 171 172 if (SYNC_FULL_SYNC.equals(action)) { 173 long lastSyncTime = VisualVoicemailSettingsUtil.getVisualVoicemailLastFullSyncTime( 174 this, phoneAccount); 175 long currentTime = System.currentTimeMillis(); 176 if (currentTime - lastSyncTime < MINIMUM_FULL_SYNC_INTERVAL_MILLIS) { 177 // If it's been less than a minute since the last sync, bail. 178 Log.v(TAG, "Avoiding duplicate full sync: synced recently for " 179 + phoneAccount.getId()); 180 return; 181 } 182 VisualVoicemailSettingsUtil.setVisualVoicemailLastFullSyncTime( 183 this, phoneAccount, currentTime); 184 } 185 186 int subId = PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount); 187 OmtpVvmCarrierConfigHelper carrierConfigHelper = 188 new OmtpVvmCarrierConfigHelper(this, subId); 189 190 if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) { 191 doSync(null, null, phoneAccount, action); 192 } else { 193 OmtpVvmNetworkRequestCallback networkCallback = new OmtpVvmNetworkRequestCallback( 194 phoneAccount, action); 195 requestNetwork(networkCallback); 196 } 197 } 198 199 private class OmtpVvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback { 200 PhoneAccountHandle mPhoneAccount; 201 String mAction; 202 NetworkRequest mNetworkRequest; 203 204 public OmtpVvmNetworkRequestCallback(PhoneAccountHandle phoneAccount, 205 String action) { 206 mPhoneAccount = phoneAccount; 207 mAction = action; 208 mNetworkRequest = new NetworkRequest.Builder() 209 .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) 210 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 211 .setNetworkSpecifier( 212 Integer.toString( 213 PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount))) 214 .build(); 215 } 216 217 public NetworkRequest getNetworkRequest() { 218 return mNetworkRequest; 219 } 220 221 @Override 222 public void onAvailable(final Network network) { 223 doSync(network, this, mPhoneAccount, mAction); 224 } 225 226 @Override 227 public void onLost(Network network) { 228 releaseNetwork(this); 229 } 230 231 @Override 232 public void onUnavailable() { 233 releaseNetwork(this); 234 } 235 } 236 237 private void doSync(Network network, OmtpVvmNetworkRequestCallback callback, 238 PhoneAccountHandle phoneAccount, String action) { 239 int retryCount = NETWORK_RETRY_COUNT; 240 241 boolean uploadSuccess; 242 boolean downloadSuccess; 243 244 while (retryCount > 0) { 245 uploadSuccess = true; 246 downloadSuccess = true; 247 248 ImapHelper imapHelper = new ImapHelper(this, phoneAccount, network); 249 if (!imapHelper.isSuccessfullyInitialized()) { 250 Log.w(TAG, "Can't retrieve Imap credentials."); 251 releaseNetwork(callback); 252 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this, 253 phoneAccount); 254 return; 255 } 256 257 if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { 258 uploadSuccess = upload(imapHelper); 259 } 260 if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { 261 downloadSuccess = download(imapHelper); 262 } 263 264 Log.v(TAG, "upload succeeded: ["+ String.valueOf(uploadSuccess) 265 + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]"); 266 267 // Need to check again for whether visual voicemail is enabled because it could have 268 // been disabled while waiting for the response from the network. 269 if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount) && 270 (!uploadSuccess || !downloadSuccess)) { 271 retryCount--; 272 // Re-adjust so that only the unsuccessful action needs to be retried. 273 // No need to re-adjust if both are unsuccessful. It means the full sync 274 // failed so the action remains unchanged. 275 if (uploadSuccess) { 276 action = SYNC_DOWNLOAD_ONLY; 277 } else if (downloadSuccess) { 278 action = SYNC_UPLOAD_ONLY; 279 } 280 281 Log.v(TAG, "Retrying " + action); 282 LocalLogHelper.log(TAG, "Immediately retrying " + action); 283 } else { 284 // Nothing more to do here, just exit. 285 releaseNetwork(callback); 286 287 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this, phoneAccount); 288 return; 289 } 290 } 291 292 releaseNetwork(callback); 293 setRetryAlarm(phoneAccount, action); 294 } 295 296 private void requestNetwork(OmtpVvmNetworkRequestCallback networkCallback) { 297 getConnectivityManager().requestNetwork(networkCallback.getNetworkRequest(), 298 networkCallback, NETWORK_REQUEST_TIMEOUT_MILLIS); 299 } 300 301 private void releaseNetwork(NetworkCallback networkCallback) { 302 if (networkCallback != null) { 303 getConnectivityManager().unregisterNetworkCallback(networkCallback); 304 } 305 } 306 307 private ConnectivityManager getConnectivityManager() { 308 if (mConnectivityManager == null) { 309 mConnectivityManager = (ConnectivityManager) this.getSystemService( 310 Context.CONNECTIVITY_SERVICE); 311 } 312 return mConnectivityManager; 313 } 314 315 private void setRetryAlarm(PhoneAccountHandle phoneAccount, String action) { 316 Intent serviceIntent = new Intent(this, OmtpVvmSyncService.class); 317 serviceIntent.setAction(action); 318 serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_PHONE_ACCOUNT, phoneAccount); 319 PendingIntent pendingIntent = PendingIntent.getService(this, 0, serviceIntent, 0); 320 long retryInterval = VisualVoicemailSettingsUtil.getVisualVoicemailRetryInterval(this, 321 phoneAccount); 322 323 Log.v(TAG, "Retrying "+ action + " in " + retryInterval + "ms"); 324 LocalLogHelper.log(TAG, "Retrying "+ action + " in " + retryInterval + "ms"); 325 326 AlarmManager alarmManager = (AlarmManager) 327 this.getSystemService(Context.ALARM_SERVICE); 328 alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + retryInterval, 329 pendingIntent); 330 331 VisualVoicemailSettingsUtil.setVisualVoicemailRetryInterval(this, phoneAccount, 332 retryInterval * 2); 333 } 334 335 private boolean upload(ImapHelper imapHelper) { 336 List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(); 337 List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(); 338 339 boolean success = true; 340 341 if (deletedVoicemails.size() > 0) { 342 if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { 343 // We want to delete selectively instead of all the voicemails for this provider 344 // in case the state changed since the IMAP query was completed. 345 mQueryHelper.deleteFromDatabase(deletedVoicemails); 346 } else { 347 success = false; 348 } 349 } 350 351 if (readVoicemails.size() > 0) { 352 if (imapHelper.markMessagesAsRead(readVoicemails)) { 353 mQueryHelper.markReadInDatabase(readVoicemails); 354 } else { 355 success = false; 356 } 357 } 358 359 return success; 360 } 361 362 private boolean download(ImapHelper imapHelper) { 363 List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails(); 364 List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(); 365 366 if (localVoicemails == null || serverVoicemails == null) { 367 // Null value means the query failed. 368 return false; 369 } 370 371 Map<String, Voicemail> remoteMap = buildMap(serverVoicemails); 372 373 // Go through all the local voicemails and check if they are on the server. 374 // They may be read or deleted on the server but not locally. Perform the 375 // appropriate local operation if the status differs from the server. Remove 376 // the messages that exist both locally and on the server to know which server 377 // messages to insert locally. 378 for (int i = 0; i < localVoicemails.size(); i++) { 379 Voicemail localVoicemail = localVoicemails.get(i); 380 Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); 381 if (remoteVoicemail == null) { 382 mQueryHelper.deleteFromDatabase(localVoicemail); 383 } else { 384 if (remoteVoicemail.isRead() != localVoicemail.isRead()) { 385 mQueryHelper.markReadInDatabase(localVoicemail); 386 } 387 } 388 } 389 390 // The leftover messages are messages that exist on the server but not locally. 391 for (Voicemail remoteVoicemail : remoteMap.values()) { 392 VoicemailContract.Voicemails.insert(this, remoteVoicemail); 393 } 394 395 return true; 396 } 397 398 /** 399 * Builds a map from provider data to message for the given collection of voicemails. 400 */ 401 private Map<String, Voicemail> buildMap(List<Voicemail> messages) { 402 Map<String, Voicemail> map = new HashMap<String, Voicemail>(); 403 for (Voicemail message : messages) { 404 map.put(message.getSourceData(), message); 405 } 406 return map; 407 } 408} 409