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.voicemail.impl.sync;
     17 
     18 import android.annotation.TargetApi;
     19 import android.content.Context;
     20 import android.net.Network;
     21 import android.net.Uri;
     22 import android.os.Build.VERSION_CODES;
     23 import android.support.v4.os.BuildCompat;
     24 import android.telecom.PhoneAccountHandle;
     25 import android.text.TextUtils;
     26 import android.util.ArrayMap;
     27 import com.android.dialer.logging.DialerImpression;
     28 import com.android.voicemail.VoicemailComponent;
     29 import com.android.voicemail.impl.ActivationTask;
     30 import com.android.voicemail.impl.Assert;
     31 import com.android.voicemail.impl.OmtpEvents;
     32 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
     33 import com.android.voicemail.impl.Voicemail;
     34 import com.android.voicemail.impl.VoicemailStatus;
     35 import com.android.voicemail.impl.VvmLog;
     36 import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
     37 import com.android.voicemail.impl.imap.ImapHelper;
     38 import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
     39 import com.android.voicemail.impl.mail.store.ImapFolder.Quota;
     40 import com.android.voicemail.impl.scheduling.BaseTask;
     41 import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
     42 import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
     43 import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
     44 import com.android.voicemail.impl.utils.LoggerUtils;
     45 import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
     46 import java.util.List;
     47 import java.util.Map;
     48 
     49 /** Sync OMTP visual voicemail. */
     50 @TargetApi(VERSION_CODES.O)
     51 public class OmtpVvmSyncService {
     52 
     53   private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
     54 
     55   /** Signifies a sync with both uploading to the server and downloading from the server. */
     56   public static final String SYNC_FULL_SYNC = "full_sync";
     57   /** Only upload to the server. */
     58   public static final String SYNC_UPLOAD_ONLY = "upload_only";
     59   /** Only download from the server. */
     60   public static final String SYNC_DOWNLOAD_ONLY = "download_only";
     61   /** Only download single voicemail transcription. */
     62   public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription";
     63   /** Threshold for whether we should archive and delete voicemails from the remote VM server. */
     64   private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f;
     65 
     66   private final Context mContext;
     67 
     68   private VoicemailsQueryHelper mQueryHelper;
     69 
     70   public OmtpVvmSyncService(Context context) {
     71     mContext = context;
     72     mQueryHelper = new VoicemailsQueryHelper(mContext);
     73   }
     74 
     75   public void sync(
     76       BaseTask task,
     77       String action,
     78       PhoneAccountHandle phoneAccount,
     79       Voicemail voicemail,
     80       VoicemailStatus.Editor status) {
     81     Assert.isTrue(phoneAccount != null);
     82     VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
     83     setupAndSendRequest(task, phoneAccount, voicemail, action, status);
     84   }
     85 
     86   private void setupAndSendRequest(
     87       BaseTask task,
     88       PhoneAccountHandle phoneAccount,
     89       Voicemail voicemail,
     90       String action,
     91       VoicemailStatus.Editor status) {
     92     if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
     93       VvmLog.v(TAG, "Sync requested for disabled account");
     94       return;
     95     }
     96     if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) {
     97       ActivationTask.start(mContext, phoneAccount, null);
     98       return;
     99     }
    100 
    101     OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
    102     LoggerUtils.logImpressionOnMainThread(mContext, DialerImpression.Type.VVM_SYNC_STARTED);
    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(
    107         VoicemailStatus.edit(mContext, phoneAccount), 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(
    122       BaseTask task,
    123       Network network,
    124       PhoneAccountHandle phoneAccount,
    125       Voicemail voicemail,
    126       String action,
    127       VoicemailStatus.Editor status) {
    128     try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
    129       boolean success;
    130       if (voicemail == null) {
    131         success = syncAll(action, imapHelper, phoneAccount);
    132       } else {
    133         success = syncOne(imapHelper, voicemail, phoneAccount);
    134       }
    135       if (success) {
    136         // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
    137         imapHelper.updateQuota();
    138         autoDeleteAndArchiveVM(imapHelper, phoneAccount);
    139         imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
    140         LoggerUtils.logImpressionOnMainThread(mContext, DialerImpression.Type.VVM_SYNC_COMPLETED);
    141       } else {
    142         task.fail();
    143       }
    144     } catch (InitializingException e) {
    145       VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
    146       return;
    147     }
    148   }
    149 
    150   /**
    151    * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs
    152    * and delete them from the server to ensure new VMs can be received.
    153    */
    154   private void autoDeleteAndArchiveVM(
    155       ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) {
    156     if (!isArchiveAllowedAndEnabled(mContext, phoneAccountHandle)) {
    157       VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off");
    158       LoggerUtils.logImpressionOnMainThread(
    159           mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF);
    160       return;
    161     }
    162     Quota quotaOnServer = imapHelper.getQuota();
    163     if (quotaOnServer == null) {
    164       LoggerUtils.logImpressionOnMainThread(
    165           mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_FAILED_DUE_TO_FAILED_QUOTA_CHECK);
    166       VvmLog.e(TAG, "autoDeleteAndArchiveVM failed - Can't retrieve Imap quota.");
    167       return;
    168     }
    169 
    170     if ((float) quotaOnServer.occupied / (float) quotaOnServer.total
    171         > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) {
    172       deleteAndArchiveVM(imapHelper, quotaOnServer);
    173       imapHelper.updateQuota();
    174       LoggerUtils.logImpressionOnMainThread(
    175           mContext, DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER);
    176     } else {
    177       VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold");
    178     }
    179   }
    180 
    181   private static boolean isArchiveAllowedAndEnabled(
    182       Context context, PhoneAccountHandle phoneAccountHandle) {
    183 
    184     if (!VoicemailComponent.get(context)
    185         .getVoicemailClient()
    186         .isVoicemailArchiveAvailable(context)) {
    187       VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is not available");
    188       return false;
    189     }
    190     if (!VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)) {
    191       VvmLog.i("isArchiveAllowedAndEnabled", "voicemail archive is turned off");
    192       return false;
    193     }
    194     if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle)) {
    195       VvmLog.i("isArchiveAllowedAndEnabled", "voicemail is turned off");
    196       return false;
    197     }
    198     return true;
    199   }
    200 
    201   private void deleteAndArchiveVM(ImapHelper imapHelper, Quota quotaOnServer) {
    202     // Archive column should only be used for 0 and above
    203     Assert.isTrue(BuildCompat.isAtLeastO());
    204 
    205     // The number of voicemails that exceed our threshold and should be deleted from the server
    206     int numVoicemails =
    207         quotaOnServer.occupied - (int) (AUTO_DELETE_ARCHIVE_VM_THRESHOLD * quotaOnServer.total);
    208     List<Voicemail> oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails);
    209     VvmLog.w(TAG, "number of voicemails to delete " + numVoicemails);
    210     if (!oldestVoicemails.isEmpty()) {
    211       mQueryHelper.markArchivedInDatabase(oldestVoicemails);
    212       imapHelper.markMessagesAsDeleted(oldestVoicemails);
    213       VvmLog.i(
    214           TAG,
    215           String.format(
    216               "successfully archived and deleted %d voicemails", oldestVoicemails.size()));
    217     } else {
    218       VvmLog.w(TAG, "remote voicemail server is empty");
    219     }
    220   }
    221 
    222   private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
    223     boolean uploadSuccess = true;
    224     boolean downloadSuccess = true;
    225 
    226     if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
    227       uploadSuccess = upload(account, imapHelper);
    228     }
    229     if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
    230       downloadSuccess = download(imapHelper, account);
    231     }
    232 
    233     VvmLog.v(
    234         TAG,
    235         "upload succeeded: ["
    236             + String.valueOf(uploadSuccess)
    237             + "] download succeeded: ["
    238             + String.valueOf(downloadSuccess)
    239             + "]");
    240 
    241     return uploadSuccess && downloadSuccess;
    242   }
    243 
    244   private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) {
    245     if (shouldPerformPrefetch(account, imapHelper)) {
    246       VoicemailFetchedCallback callback =
    247           new VoicemailFetchedCallback(mContext, voicemail.getUri(), account);
    248       imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
    249     }
    250 
    251     return imapHelper.fetchTranscription(
    252         new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData());
    253   }
    254 
    255   private boolean upload(PhoneAccountHandle phoneAccountHandle, ImapHelper imapHelper) {
    256     List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(phoneAccountHandle);
    257     List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(phoneAccountHandle);
    258 
    259     boolean success = true;
    260 
    261     if (deletedVoicemails.size() > 0) {
    262       if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
    263         // We want to delete selectively instead of all the voicemails for this provider
    264         // in case the state changed since the IMAP query was completed.
    265         mQueryHelper.deleteFromDatabase(deletedVoicemails);
    266       } else {
    267         success = false;
    268       }
    269     }
    270 
    271     if (readVoicemails.size() > 0) {
    272       VvmLog.i(TAG, "Marking voicemails as read");
    273       if (imapHelper.markMessagesAsRead(readVoicemails)) {
    274         VvmLog.i(TAG, "Marking voicemails as clean");
    275         mQueryHelper.markCleanInDatabase(readVoicemails);
    276       } else {
    277         success = false;
    278       }
    279     }
    280 
    281     return success;
    282   }
    283 
    284   private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
    285     List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
    286     List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(account);
    287 
    288     if (localVoicemails == null || serverVoicemails == null) {
    289       // Null value means the query failed.
    290       return false;
    291     }
    292 
    293     Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
    294 
    295     // Go through all the local voicemails and check if they are on the server.
    296     // They may be read or deleted on the server but not locally. Perform the
    297     // appropriate local operation if the status differs from the server. Remove
    298     // the messages that exist both locally and on the server to know which server
    299     // messages to insert locally.
    300     // Voicemails that were removed automatically from the server, are marked as
    301     // archived and are stored locally. We do not delete them, as they were removed from the server
    302     // by design (to make space).
    303     for (int i = 0; i < localVoicemails.size(); i++) {
    304       Voicemail localVoicemail = localVoicemails.get(i);
    305       Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
    306 
    307       // Do not delete voicemails that are archived marked as archived.
    308       if (remoteVoicemail == null) {
    309         mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail);
    310       } else {
    311         if (remoteVoicemail.isRead() && !localVoicemail.isRead()) {
    312           mQueryHelper.markReadInDatabase(localVoicemail);
    313         }
    314 
    315         if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())
    316             && TextUtils.isEmpty(localVoicemail.getTranscription())) {
    317           LoggerUtils.logImpressionOnMainThread(
    318               mContext, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED);
    319           mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription());
    320         }
    321       }
    322     }
    323 
    324     // The leftover messages are messages that exist on the server but not locally.
    325     boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
    326     for (Voicemail remoteVoicemail : remoteMap.values()) {
    327       if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())) {
    328         LoggerUtils.logImpressionOnMainThread(
    329             mContext, DialerImpression.Type.VVM_TRANSCRIPTION_DOWNLOADED);
    330       }
    331       Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
    332       if (prefetchEnabled) {
    333         VoicemailFetchedCallback fetchedCallback =
    334             new VoicemailFetchedCallback(mContext, uri, account);
    335         imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
    336       }
    337     }
    338 
    339     return true;
    340   }
    341 
    342   private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
    343     OmtpVvmCarrierConfigHelper carrierConfigHelper =
    344         new OmtpVvmCarrierConfigHelper(mContext, account);
    345     return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
    346   }
    347 
    348   /** Builds a map from provider data to message for the given collection of voicemails. */
    349   private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
    350     Map<String, Voicemail> map = new ArrayMap<String, Voicemail>();
    351     for (Voicemail message : messages) {
    352       map.put(message.getSourceData(), message);
    353     }
    354     return map;
    355   }
    356 
    357   /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */
    358   public static class TranscriptionFetchedCallback {
    359 
    360     private Context mContext;
    361     private Voicemail mVoicemail;
    362 
    363     public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
    364       mContext = context;
    365       mVoicemail = voicemail;
    366     }
    367 
    368     public void setVoicemailTranscription(String transcription) {
    369       VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
    370       queryHelper.updateWithTranscription(mVoicemail, transcription);
    371     }
    372   }
    373 }
    374