• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
OmtpVvmSyncService(Context context)70   public OmtpVvmSyncService(Context context) {
71     mContext = context;
72     mQueryHelper = new VoicemailsQueryHelper(mContext);
73   }
74 
sync( BaseTask task, String action, PhoneAccountHandle phoneAccount, Voicemail voicemail, VoicemailStatus.Editor status)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 
setupAndSendRequest( BaseTask task, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action, VoicemailStatus.Editor status)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 
doSync( BaseTask task, Network network, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action, VoicemailStatus.Editor status)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    */
autoDeleteAndArchiveVM( ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle)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 
isArchiveAllowedAndEnabled( Context context, PhoneAccountHandle phoneAccountHandle)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 
deleteAndArchiveVM(ImapHelper imapHelper, Quota quotaOnServer)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 
syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account)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 
syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account)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 
upload(PhoneAccountHandle phoneAccountHandle, ImapHelper imapHelper)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 
download(ImapHelper imapHelper, PhoneAccountHandle account)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 
shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper)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. */
buildMap(List<Voicemail> messages)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 
TranscriptionFetchedCallback(Context context, Voicemail voicemail)363     public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
364       mContext = context;
365       mVoicemail = voicemail;
366     }
367 
setVoicemailTranscription(String transcription)368     public void setVoicemailTranscription(String transcription) {
369       VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
370       queryHelper.updateWithTranscription(mVoicemail, transcription);
371     }
372   }
373 }
374