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