Home | History | Annotate | Download | only in sync
      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 
     93     public OmtpVvmSyncService() {
     94         super("OmtpVvmSyncService");
     95     }
     96 
     97     public static Intent getSyncIntent(Context context, String action,
     98             PhoneAccountHandle phoneAccount, boolean firstAttempt) {
     99         return getSyncIntent(context, action, phoneAccount, null, firstAttempt);
    100     }
    101 
    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      */
    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      */
    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
    164     public void onCreate() {
    165         super.onCreate();
    166         mQueryHelper = new VoicemailsQueryHelper(this);
    167     }
    168 
    169     @Override
    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 
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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      */
    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 
    460         public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
    461             mContext = context;
    462             mVoicemail = voicemail;
    463         }
    464 
    465         public void setVoicemailTranscription(String transcription) {
    466             VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
    467             queryHelper.updateWithTranscription(mVoicemail, transcription);
    468         }
    469     }
    470 }
    471