OmtpVvmSyncService.java revision 83b20211c3be04a850de3674977deee8e448d17f
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.voicemail.impl.sync;
17
18import android.annotation.TargetApi;
19import android.content.Context;
20import android.net.Network;
21import android.net.Uri;
22import android.os.Build.VERSION_CODES;
23import android.support.v4.os.BuildCompat;
24import android.telecom.PhoneAccountHandle;
25import android.text.TextUtils;
26import android.util.ArrayMap;
27import com.android.dialer.logging.DialerImpression;
28import com.android.voicemail.VoicemailComponent;
29import com.android.voicemail.impl.ActivationTask;
30import com.android.voicemail.impl.Assert;
31import com.android.voicemail.impl.OmtpEvents;
32import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
33import com.android.voicemail.impl.Voicemail;
34import com.android.voicemail.impl.VoicemailStatus;
35import com.android.voicemail.impl.VvmLog;
36import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
37import com.android.voicemail.impl.imap.ImapHelper;
38import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
39import com.android.voicemail.impl.mail.store.ImapFolder.Quota;
40import com.android.voicemail.impl.scheduling.BaseTask;
41import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
42import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
43import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
44import com.android.voicemail.impl.utils.LoggerUtils;
45import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
46import java.util.List;
47import java.util.Map;
48
49/** Sync OMTP visual voicemail. */
50@TargetApi(VERSION_CODES.O)
51public 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