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.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