• 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.phone.vvm.omtp.sync;
17 
18 import android.content.Context;
19 import android.net.Network;
20 import android.net.Uri;
21 import android.provider.VoicemailContract;
22 import android.telecom.PhoneAccountHandle;
23 import android.telecom.Voicemail;
24 import android.text.TextUtils;
25 import com.android.phone.Assert;
26 import com.android.phone.PhoneUtils;
27 import com.android.phone.VoicemailStatus;
28 import com.android.phone.settings.VisualVoicemailSettingsUtil;
29 import com.android.phone.vvm.omtp.ActivationTask;
30 import com.android.phone.vvm.omtp.OmtpEvents;
31 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
32 import com.android.phone.vvm.omtp.VvmLog;
33 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
34 import com.android.phone.vvm.omtp.imap.ImapHelper;
35 import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException;
36 import com.android.phone.vvm.omtp.scheduling.BaseTask;
37 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.NetworkWrapper;
38 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.RequestFailedException;
39 import com.android.phone.vvm.omtp.utils.PhoneAccountHandleConverter;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 
44 /**
45  * Sync OMTP visual voicemail.
46  */
47 public class OmtpVvmSyncService {
48 
49     private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
50 
51     /**
52      * Signifies a sync with both uploading to the server and downloading from the server.
53      */
54     public static final String SYNC_FULL_SYNC = "full_sync";
55     /**
56      * Only upload to the server.
57      */
58     public static final String SYNC_UPLOAD_ONLY = "upload_only";
59     /**
60      * Only download from the server.
61      */
62     public static final String SYNC_DOWNLOAD_ONLY = "download_only";
63     /**
64      * Only download single voicemail transcription.
65      */
66     public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION =
67             "download_one_transcription";
68 
69     private final Context mContext;
70 
71     // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
72     private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
73     // Constant indicating that there has never been a full sync.
74     public static final long NO_PRIOR_FULL_SYNC = -1;
75 
76     private VoicemailsQueryHelper mQueryHelper;
77 
OmtpVvmSyncService(Context context)78     public OmtpVvmSyncService(Context context) {
79         mContext = context;
80         mQueryHelper = new VoicemailsQueryHelper(mContext);
81     }
82 
sync(BaseTask task, String action, PhoneAccountHandle phoneAccount, Voicemail voicemail, VoicemailStatus.Editor status)83     public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount,
84             Voicemail voicemail, VoicemailStatus.Editor status) {
85         Assert.isTrue(phoneAccount != null);
86         VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
87         setupAndSendRequest(task, phoneAccount, voicemail, action, status);
88     }
89 
setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action, VoicemailStatus.Editor status)90     private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount,
91             Voicemail voicemail, String action, VoicemailStatus.Editor status) {
92         if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
93             VvmLog.v(TAG, "Sync requested for disabled account");
94             return;
95         }
96         int subId = PhoneAccountHandleConverter.toSubId(phoneAccount);
97         if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) {
98             ActivationTask.start(mContext, subId, null);
99             return;
100         }
101 
102         OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, subId);
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(VoicemailStatus.edit(mContext, phoneAccount),
107                 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(BaseTask task, Network network, PhoneAccountHandle phoneAccount,
122             Voicemail voicemail, String action, VoicemailStatus.Editor status) {
123         try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
124             boolean success;
125             if (voicemail == null) {
126                 success = syncAll(action, imapHelper, phoneAccount);
127             } else {
128                 success = syncOne(imapHelper, voicemail, phoneAccount);
129             }
130             if (success) {
131                 // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
132                 imapHelper.updateQuota();
133                 imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
134             } else {
135                 task.fail();
136             }
137         } catch (InitializingException e) {
138             VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
139             return;
140         }
141     }
142 
syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account)143     private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
144         boolean uploadSuccess = true;
145         boolean downloadSuccess = true;
146 
147         if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
148             uploadSuccess = upload(imapHelper);
149         }
150         if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
151             downloadSuccess = download(imapHelper, account);
152         }
153 
154         VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess)
155                 + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
156 
157         return uploadSuccess && downloadSuccess;
158     }
159 
syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account)160     private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail,
161             PhoneAccountHandle account) {
162         if (shouldPerformPrefetch(account, imapHelper)) {
163             VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext,
164                     voicemail.getUri(), account);
165             imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
166         }
167 
168         return imapHelper.fetchTranscription(
169                 new TranscriptionFetchedCallback(mContext, voicemail),
170                 voicemail.getSourceData());
171     }
172 
upload(ImapHelper imapHelper)173     private boolean upload(ImapHelper imapHelper) {
174         List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
175         List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
176 
177         boolean success = true;
178 
179         if (deletedVoicemails.size() > 0) {
180             if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
181                 // We want to delete selectively instead of all the voicemails for this provider
182                 // in case the state changed since the IMAP query was completed.
183                 mQueryHelper.deleteFromDatabase(deletedVoicemails);
184             } else {
185                 success = false;
186             }
187         }
188 
189         if (readVoicemails.size() > 0) {
190             if (imapHelper.markMessagesAsRead(readVoicemails)) {
191                 mQueryHelper.markCleanInDatabase(readVoicemails);
192             } else {
193                 success = false;
194             }
195         }
196 
197         return success;
198     }
199 
download(ImapHelper imapHelper, PhoneAccountHandle account)200     private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
201         List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
202         List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
203 
204         if (localVoicemails == null || serverVoicemails == null) {
205             // Null value means the query failed.
206             return false;
207         }
208 
209         Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
210 
211         // Go through all the local voicemails and check if they are on the server.
212         // They may be read or deleted on the server but not locally. Perform the
213         // appropriate local operation if the status differs from the server. Remove
214         // the messages that exist both locally and on the server to know which server
215         // messages to insert locally.
216         for (int i = 0; i < localVoicemails.size(); i++) {
217             Voicemail localVoicemail = localVoicemails.get(i);
218             Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
219             if (remoteVoicemail == null) {
220                 mQueryHelper.deleteFromDatabase(localVoicemail);
221             } else {
222                 if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
223                     mQueryHelper.markReadInDatabase(localVoicemail);
224                 }
225 
226                 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) &&
227                         TextUtils.isEmpty(localVoicemail.getTranscription())) {
228                     mQueryHelper.updateWithTranscription(localVoicemail,
229                             remoteVoicemail.getTranscription());
230                 }
231             }
232         }
233 
234         // The leftover messages are messages that exist on the server but not locally.
235         boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
236         for (Voicemail remoteVoicemail : remoteMap.values()) {
237             Uri uri = VoicemailContract.Voicemails.insert(mContext, remoteVoicemail);
238             if (prefetchEnabled) {
239                 VoicemailFetchedCallback fetchedCallback =
240                         new VoicemailFetchedCallback(mContext, uri, account);
241                 imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
242             }
243         }
244 
245         return true;
246     }
247 
shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper)248     private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
249         OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
250                 mContext, PhoneUtils.getSubIdForPhoneAccountHandle(account));
251         return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
252     }
253 
254     /**
255      * Builds a map from provider data to message for the given collection of voicemails.
256      */
buildMap(List<Voicemail> messages)257     private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
258         Map<String, Voicemail> map = new HashMap<String, Voicemail>();
259         for (Voicemail message : messages) {
260             map.put(message.getSourceData(), message);
261         }
262         return map;
263     }
264 
265     public class TranscriptionFetchedCallback {
266 
267         private Context mContext;
268         private Voicemail mVoicemail;
269 
TranscriptionFetchedCallback(Context context, Voicemail voicemail)270         public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
271             mContext = context;
272             mVoicemail = voicemail;
273         }
274 
setVoicemailTranscription(String transcription)275         public void setVoicemailTranscription(String transcription) {
276             VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
277             queryHelper.updateWithTranscription(mVoicemail, transcription);
278         }
279     }
280 }
281