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.app.AlarmManager;
19 import android.app.IntentService;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.net.Network;
24 import android.net.NetworkInfo;
25 import android.net.Uri;
26 import android.provider.VoicemailContract;
27 import android.provider.VoicemailContract.Status;
28 import android.telecom.PhoneAccountHandle;
29 import android.telecom.Voicemail;
30 import android.text.TextUtils;
31 import android.util.Log;
32 
33 import com.android.phone.PhoneUtils;
34 import com.android.phone.VoicemailUtils;
35 import com.android.phone.settings.VisualVoicemailSettingsUtil;
36 import com.android.phone.vvm.omtp.LocalLogHelper;
37 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
38 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
39 import com.android.phone.vvm.omtp.imap.ImapHelper;
40 
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 
46 /**
47  * Sync OMTP visual voicemail.
48  */
49 public class OmtpVvmSyncService extends IntentService {
50 
51     private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
52 
53     // Number of retries
54     private static final int NETWORK_RETRY_COUNT = 3;
55 
56     /**
57      * Signifies a sync with both uploading to the server and downloading from the server.
58      */
59     public static final String SYNC_FULL_SYNC = "full_sync";
60     /**
61      * Only upload to the server.
62      */
63     public static final String SYNC_UPLOAD_ONLY = "upload_only";
64     /**
65      * Only download from the server.
66      */
67     public static final String SYNC_DOWNLOAD_ONLY = "download_only";
68     /**
69      * Only download single voicemail transcription.
70      */
71     public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION =
72             "download_one_transcription";
73     /**
74      * The account to sync.
75      */
76     public static final String EXTRA_PHONE_ACCOUNT = "phone_account";
77     /**
78      * The voicemail to fetch.
79      */
80     public static final String EXTRA_VOICEMAIL = "voicemail";
81     /**
82      * The sync request is initiated by the user, should allow shorter sync interval.
83      */
84     public static final String EXTRA_IS_MANUAL_SYNC = "is_manual_sync";
85     // Minimum time allowed between full syncs
86     private static final int MINIMUM_FULL_SYNC_INTERVAL_MILLIS = 60 * 1000;
87 
88     // Minimum time allowed between manual syncs
89     private static final int MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS = 3 * 1000;
90 
91     private VoicemailsQueryHelper mQueryHelper;
92 
OmtpVvmSyncService()93     public OmtpVvmSyncService() {
94         super("OmtpVvmSyncService");
95     }
96 
getSyncIntent(Context context, String action, PhoneAccountHandle phoneAccount, boolean firstAttempt)97     public static Intent getSyncIntent(Context context, String action,
98             PhoneAccountHandle phoneAccount, boolean firstAttempt) {
99         return getSyncIntent(context, action, phoneAccount, null, firstAttempt);
100     }
101 
getSyncIntent(Context context, String action, PhoneAccountHandle phoneAccount, Voicemail voicemail, boolean firstAttempt)102     public static Intent getSyncIntent(Context context, String action,
103             PhoneAccountHandle phoneAccount, Voicemail voicemail, boolean firstAttempt) {
104         if (firstAttempt) {
105             if (phoneAccount != null) {
106                 VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context,
107                         phoneAccount);
108             } else {
109                 OmtpVvmSourceManager vvmSourceManager =
110                         OmtpVvmSourceManager.getInstance(context);
111                 Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources();
112                 for (PhoneAccountHandle source : sources) {
113                     VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, source);
114                 }
115             }
116         }
117 
118         Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class);
119         serviceIntent.setAction(action);
120         if (phoneAccount != null) {
121             serviceIntent.putExtra(EXTRA_PHONE_ACCOUNT, phoneAccount);
122         }
123         if (voicemail != null) {
124             serviceIntent.putExtra(EXTRA_VOICEMAIL, voicemail);
125         }
126 
127         cancelRetriesForIntent(context, serviceIntent);
128         return serviceIntent;
129     }
130 
131     /**
132      * Cancel all retry syncs for an account.
133      *
134      * @param context The context the service runs in.
135      * @param phoneAccount The phone account for which to cancel syncs.
136      */
cancelAllRetries(Context context, PhoneAccountHandle phoneAccount)137     public static void cancelAllRetries(Context context, PhoneAccountHandle phoneAccount) {
138         cancelRetriesForIntent(context, getSyncIntent(context, SYNC_FULL_SYNC, phoneAccount,
139                 false));
140     }
141 
142     /**
143      * A helper method to cancel all pending alarms for intents that would be identical to the given
144      * intent.
145      *
146      * @param context The context the service runs in.
147      * @param intent The intent to search and cancel.
148      */
cancelRetriesForIntent(Context context, Intent intent)149     private static void cancelRetriesForIntent(Context context, Intent intent) {
150         AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
151         alarmManager.cancel(PendingIntent.getService(context, 0, intent, 0));
152 
153         Intent copyIntent = new Intent(intent);
154         if (SYNC_FULL_SYNC.equals(copyIntent.getAction())) {
155             // A full sync action should also cancel both of the other types of syncs
156             copyIntent.setAction(SYNC_DOWNLOAD_ONLY);
157             alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0));
158             copyIntent.setAction(SYNC_UPLOAD_ONLY);
159             alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0));
160         }
161     }
162 
163     @Override
onCreate()164     public void onCreate() {
165         super.onCreate();
166         mQueryHelper = new VoicemailsQueryHelper(this);
167     }
168 
169     @Override
onHandleIntent(Intent intent)170     protected void onHandleIntent(Intent intent) {
171         if (intent == null) {
172             Log.d(TAG, "onHandleIntent: could not handle null intent");
173             return;
174         }
175         String action = intent.getAction();
176         PhoneAccountHandle phoneAccount = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT);
177         LocalLogHelper.log(TAG, "Sync requested: " + action +
178                 " for all accounts: " + String.valueOf(phoneAccount == null));
179 
180         boolean isManualSync = intent.getBooleanExtra(EXTRA_IS_MANUAL_SYNC, false);
181         Voicemail voicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
182         if (phoneAccount != null) {
183             Log.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
184             setupAndSendRequest(phoneAccount, voicemail, action, isManualSync);
185         } else {
186             Log.v(TAG, "Sync requested: " + action + " - for all accounts");
187             OmtpVvmSourceManager vvmSourceManager =
188                     OmtpVvmSourceManager.getInstance(this);
189             Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources();
190             for (PhoneAccountHandle source : sources) {
191                 setupAndSendRequest(source, null, action, isManualSync);
192             }
193         }
194     }
195 
setupAndSendRequest(PhoneAccountHandle phoneAccount, Voicemail voicemail, String action, boolean isManualSync)196     private void setupAndSendRequest(PhoneAccountHandle phoneAccount, Voicemail voicemail,
197             String action, boolean isManualSync) {
198         if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount)) {
199             Log.v(TAG, "Sync requested for disabled account");
200             return;
201         }
202 
203         if (SYNC_FULL_SYNC.equals(action)) {
204             long lastSyncTime = VisualVoicemailSettingsUtil.getVisualVoicemailLastFullSyncTime(
205                     this, phoneAccount);
206             long currentTime = System.currentTimeMillis();
207             int minimumInterval = isManualSync ? MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS
208                     : MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS;
209             if (currentTime - lastSyncTime < minimumInterval) {
210                 // If it's been less than a minute since the last sync, bail.
211                 Log.v(TAG, "Avoiding duplicate full sync: synced recently for "
212                         + phoneAccount.getId());
213 
214                 /**
215                  *  Perform a NOOP change to the database so the sender can observe the sync is
216                  *  completed.
217                  *  TODO: Instead of this hack, refactor the sync to be synchronous so the sender
218                  *  can use sendOrderedBroadcast() to register a callback once all syncs are
219                  *  finished
220                  *  b/26937720
221                  */
222                 Status.setStatus(this, phoneAccount,
223                         Status.CONFIGURATION_STATE_IGNORE,
224                         Status.DATA_CHANNEL_STATE_IGNORE,
225                         Status.NOTIFICATION_CHANNEL_STATE_IGNORE);
226                 return;
227             }
228             VisualVoicemailSettingsUtil.setVisualVoicemailLastFullSyncTime(
229                     this, phoneAccount, currentTime);
230         }
231 
232         VvmNetworkRequestCallback networkCallback = new SyncNetworkRequestCallback(this,
233                 phoneAccount, voicemail, action);
234         networkCallback.requestNetwork();
235     }
236 
doSync(Network network, VvmNetworkRequestCallback callback, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action)237     private void doSync(Network network, VvmNetworkRequestCallback callback,
238             PhoneAccountHandle phoneAccount, Voicemail voicemail, String action) {
239         int retryCount = NETWORK_RETRY_COUNT;
240         try {
241             while (retryCount > 0) {
242                 ImapHelper imapHelper = new ImapHelper(this, phoneAccount, network);
243                 if (!imapHelper.isSuccessfullyInitialized()) {
244                     Log.w(TAG, "Can't retrieve Imap credentials.");
245                     VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this,
246                             phoneAccount);
247                     return;
248                 }
249 
250                 boolean success = true;
251                 if (voicemail == null) {
252                     success = syncAll(action, imapHelper, phoneAccount);
253                 } else {
254                     success = syncOne(imapHelper, voicemail, phoneAccount);
255                 }
256                 imapHelper.updateQuota();
257 
258                 // Need to check again for whether visual voicemail is enabled because it could have
259                 // been disabled while waiting for the response from the network.
260                 if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount) &&
261                         !success) {
262                     retryCount--;
263                     Log.v(TAG, "Retrying " + action);
264                 } else {
265                     // Nothing more to do here, just exit.
266                     VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this,
267                             phoneAccount);
268                     VoicemailUtils.setDataChannelState(
269                             this, phoneAccount, Status.DATA_CHANNEL_STATE_OK);
270                     return;
271                 }
272             }
273         } finally {
274             if (callback != null) {
275                 callback.releaseNetwork();
276             }
277         }
278     }
279 
syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account)280     private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
281         boolean uploadSuccess = true;
282         boolean downloadSuccess = true;
283 
284         if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
285             uploadSuccess = upload(imapHelper);
286         }
287         if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
288             downloadSuccess = download(imapHelper, account);
289         }
290 
291         Log.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess)
292                 + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
293 
294         boolean success = uploadSuccess && downloadSuccess;
295         if (!uploadSuccess || !downloadSuccess) {
296             if (uploadSuccess) {
297                 action = SYNC_DOWNLOAD_ONLY;
298             } else if (downloadSuccess) {
299                 action = SYNC_UPLOAD_ONLY;
300             }
301         }
302 
303         return success;
304     }
305 
syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account)306     private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail,
307             PhoneAccountHandle account) {
308         if (shouldPerformPrefetch(account, imapHelper)) {
309             VoicemailFetchedCallback callback = new VoicemailFetchedCallback(this,
310                     voicemail.getUri());
311             imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
312         }
313 
314         return imapHelper.fetchTranscription(
315                 new TranscriptionFetchedCallback(this, voicemail),
316                 voicemail.getSourceData());
317     }
318 
319     private class SyncNetworkRequestCallback extends VvmNetworkRequestCallback {
320 
321         Voicemail mVoicemail;
322         private String mAction;
323 
SyncNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount, Voicemail voicemail, String action)324         public SyncNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount,
325                 Voicemail voicemail, String action) {
326             super(context, phoneAccount);
327             mAction = action;
328             mVoicemail = voicemail;
329         }
330 
331         @Override
onAvailable(Network network)332         public void onAvailable(Network network) {
333             super.onAvailable(network);
334             NetworkInfo info = getConnectivityManager().getNetworkInfo(network);
335             if (info == null) {
336                 Log.d(TAG, "Network Type: Unknown");
337             } else {
338                 Log.d(TAG, "Network Type: " + info.getTypeName());
339             }
340 
341             doSync(network, this, mPhoneAccount, mVoicemail, mAction);
342         }
343 
344     }
345 
upload(ImapHelper imapHelper)346     private boolean upload(ImapHelper imapHelper) {
347         List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
348         List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
349 
350         boolean success = true;
351 
352         if (deletedVoicemails.size() > 0) {
353             if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
354                 // We want to delete selectively instead of all the voicemails for this provider
355                 // in case the state changed since the IMAP query was completed.
356                 mQueryHelper.deleteFromDatabase(deletedVoicemails);
357             } else {
358                 success = false;
359             }
360         }
361 
362         if (readVoicemails.size() > 0) {
363             if (imapHelper.markMessagesAsRead(readVoicemails)) {
364                 mQueryHelper.markReadInDatabase(readVoicemails);
365             } else {
366                 success = false;
367             }
368         }
369 
370         return success;
371     }
372 
download(ImapHelper imapHelper, PhoneAccountHandle account)373     private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
374         List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
375         List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
376 
377         if (localVoicemails == null || serverVoicemails == null) {
378             // Null value means the query failed.
379             return false;
380         }
381 
382         Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
383 
384         // Go through all the local voicemails and check if they are on the server.
385         // They may be read or deleted on the server but not locally. Perform the
386         // appropriate local operation if the status differs from the server. Remove
387         // the messages that exist both locally and on the server to know which server
388         // messages to insert locally.
389         for (int i = 0; i < localVoicemails.size(); i++) {
390             Voicemail localVoicemail = localVoicemails.get(i);
391             Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
392             if (remoteVoicemail == null) {
393                 mQueryHelper.deleteFromDatabase(localVoicemail);
394             } else {
395                 if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
396                     mQueryHelper.markReadInDatabase(localVoicemail);
397                 }
398 
399                 if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) &&
400                         TextUtils.isEmpty(localVoicemail.getTranscription())) {
401                     mQueryHelper.updateWithTranscription(localVoicemail,
402                             remoteVoicemail.getTranscription());
403                 }
404             }
405         }
406 
407         // The leftover messages are messages that exist on the server but not locally.
408         boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
409         for (Voicemail remoteVoicemail : remoteMap.values()) {
410             Uri uri = VoicemailContract.Voicemails.insert(this, remoteVoicemail);
411             if (prefetchEnabled) {
412                 VoicemailFetchedCallback fetchedCallback = new VoicemailFetchedCallback(this, uri);
413                 imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
414             }
415         }
416 
417         return true;
418     }
419 
shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper)420     private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
421         OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
422                 this, PhoneUtils.getSubIdForPhoneAccountHandle(account));
423         return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
424     }
425 
setRetryAlarm(PhoneAccountHandle phoneAccount, String action)426     protected void setRetryAlarm(PhoneAccountHandle phoneAccount, String action) {
427         Intent serviceIntent = new Intent(this, OmtpVvmSyncService.class);
428         serviceIntent.setAction(action);
429         serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_PHONE_ACCOUNT, phoneAccount);
430         PendingIntent pendingIntent = PendingIntent.getService(this, 0, serviceIntent, 0);
431         long retryInterval = VisualVoicemailSettingsUtil.getVisualVoicemailRetryInterval(this,
432                 phoneAccount);
433 
434         Log.v(TAG, "Retrying " + action + " in " + retryInterval + "ms");
435 
436         AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
437         alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + retryInterval,
438                 pendingIntent);
439 
440         VisualVoicemailSettingsUtil.setVisualVoicemailRetryInterval(this, phoneAccount,
441                 retryInterval * 2);
442     }
443 
444     /**
445      * Builds a map from provider data to message for the given collection of voicemails.
446      */
buildMap(List<Voicemail> messages)447     private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
448         Map<String, Voicemail> map = new HashMap<String, Voicemail>();
449         for (Voicemail message : messages) {
450             map.put(message.getSourceData(), message);
451         }
452         return map;
453     }
454 
455     public class TranscriptionFetchedCallback {
456 
457         private Context mContext;
458         private Voicemail mVoicemail;
459 
TranscriptionFetchedCallback(Context context, Voicemail voicemail)460         public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
461             mContext = context;
462             mVoicemail = voicemail;
463         }
464 
setVoicemailTranscription(String transcription)465         public void setVoicemailTranscription(String transcription) {
466             VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
467             queryHelper.updateWithTranscription(mVoicemail, transcription);
468         }
469     }
470 }
471