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