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.phone.vvm.omtp.sync;
17
18import android.app.AlarmManager;
19import android.app.IntentService;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.Intent;
23import android.net.ConnectivityManager;
24import android.net.Network;
25import android.net.ConnectivityManager.NetworkCallback;
26import android.net.NetworkCapabilities;
27import android.net.NetworkRequest;
28import android.provider.VoicemailContract;
29import android.telecom.PhoneAccountHandle;
30import android.telecom.Voicemail;
31import android.telephony.TelephonyManager;
32import android.util.Log;
33
34import com.android.phone.PhoneUtils;
35import com.android.phone.settings.VisualVoicemailSettingsUtil;
36import com.android.phone.vvm.omtp.LocalLogHelper;
37import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
38import com.android.phone.vvm.omtp.imap.ImapHelper;
39
40import java.util.HashMap;
41import java.util.List;
42import java.util.Map;
43import java.util.Set;
44
45/**
46 * Sync OMTP visual voicemail.
47 */
48public class OmtpVvmSyncService extends IntentService {
49    private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
50
51    /** Signifies a sync with both uploading to the server and downloading from the server. */
52    public static final String SYNC_FULL_SYNC = "full_sync";
53    /** Only upload to the server. */
54    public static final String SYNC_UPLOAD_ONLY = "upload_only";
55    /** Only download from the server. */
56    public static final String SYNC_DOWNLOAD_ONLY = "download_only";
57    /** The account to sync. */
58    public static final String EXTRA_PHONE_ACCOUNT = "phone_account";
59
60    // Timeout used to call ConnectivityManager.requestNetwork
61    private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
62
63    // Minimum time allowed between full syncs
64    private static final int MINIMUM_FULL_SYNC_INTERVAL_MILLIS = 60 * 1000;
65
66    // Number of retries
67    private static final int NETWORK_RETRY_COUNT = 6;
68
69    private VoicemailsQueryHelper mQueryHelper;
70    private ConnectivityManager mConnectivityManager;
71
72    public OmtpVvmSyncService() {
73        super("OmtpVvmSyncService");
74    }
75
76    public static Intent getSyncIntent(Context context, String action,
77            PhoneAccountHandle phoneAccount, boolean firstAttempt) {
78        if (firstAttempt) {
79            if (phoneAccount != null) {
80                VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context,
81                        phoneAccount);
82            } else {
83                OmtpVvmSourceManager vvmSourceManager =
84                        OmtpVvmSourceManager.getInstance(context);
85                Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources();
86                for (PhoneAccountHandle source : sources) {
87                    VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, source);
88                }
89            }
90        }
91
92        Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class);
93        serviceIntent.setAction(action);
94        if (phoneAccount != null) {
95            serviceIntent.putExtra(EXTRA_PHONE_ACCOUNT, phoneAccount);
96        }
97
98        cancelRetriesForIntent(context, serviceIntent);
99        return serviceIntent;
100    }
101
102    /**
103     * Cancel all retry syncs for an account.
104     * @param context The context the service runs in.
105     * @param phoneAccount The phone account for which to cancel syncs.
106     */
107    public static void cancelAllRetries(Context context, PhoneAccountHandle phoneAccount) {
108        cancelRetriesForIntent(context, getSyncIntent(context, SYNC_FULL_SYNC, phoneAccount,
109                false));
110    }
111
112    /**
113     * A helper method to cancel all pending alarms for intents that would be identical to the given
114     * intent.
115     * @param context The context the service runs in.
116     * @param intent The intent to search and cancel.
117     */
118    private static void cancelRetriesForIntent(Context context, Intent intent) {
119        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
120        alarmManager.cancel(PendingIntent.getService(context, 0, intent, 0));
121
122        Intent copyIntent = new Intent(intent);
123        if (SYNC_FULL_SYNC.equals(copyIntent.getAction())) {
124            // A full sync action should also cancel both of the other types of syncs
125            copyIntent.setAction(SYNC_DOWNLOAD_ONLY);
126            alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0));
127            copyIntent.setAction(SYNC_UPLOAD_ONLY);
128            alarmManager.cancel(PendingIntent.getService(context, 0, copyIntent, 0));
129        }
130    }
131
132    @Override
133    public void onCreate() {
134        super.onCreate();
135        mQueryHelper = new VoicemailsQueryHelper(this);
136    }
137
138    @Override
139    protected void onHandleIntent(Intent intent) {
140        if (intent == null) {
141            Log.d(TAG, "onHandleIntent: could not handle null intent");
142            return;
143        }
144
145        String action = intent.getAction();
146
147        PhoneAccountHandle phoneAccount = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT);
148
149        LocalLogHelper.log(TAG, "Sync requested: " + action +
150                " for all accounts: " + String.valueOf(phoneAccount == null));
151
152        if (phoneAccount != null) {
153            Log.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
154            setupAndSendRequest(phoneAccount, action);
155        } else {
156            Log.v(TAG, "Sync requested: " + action + " - for all accounts");
157            OmtpVvmSourceManager vvmSourceManager =
158                    OmtpVvmSourceManager.getInstance(this);
159            Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources();
160            for (PhoneAccountHandle source : sources) {
161                setupAndSendRequest(source, action);
162            }
163        }
164    }
165
166    private void setupAndSendRequest(PhoneAccountHandle phoneAccount, String action) {
167        if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount)) {
168            Log.v(TAG, "Sync requested for disabled account");
169            return;
170        }
171
172        if (SYNC_FULL_SYNC.equals(action)) {
173            long lastSyncTime = VisualVoicemailSettingsUtil.getVisualVoicemailLastFullSyncTime(
174                    this, phoneAccount);
175            long currentTime = System.currentTimeMillis();
176            if (currentTime - lastSyncTime < MINIMUM_FULL_SYNC_INTERVAL_MILLIS) {
177                // If it's been less than a minute since the last sync, bail.
178                Log.v(TAG, "Avoiding duplicate full sync: synced recently for "
179                        + phoneAccount.getId());
180                return;
181            }
182            VisualVoicemailSettingsUtil.setVisualVoicemailLastFullSyncTime(
183                    this, phoneAccount, currentTime);
184        }
185
186        int subId = PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount);
187        OmtpVvmCarrierConfigHelper carrierConfigHelper =
188                new OmtpVvmCarrierConfigHelper(this, subId);
189
190        if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) {
191            doSync(null, null, phoneAccount, action);
192        } else {
193            OmtpVvmNetworkRequestCallback networkCallback = new OmtpVvmNetworkRequestCallback(
194                    phoneAccount, action);
195            requestNetwork(networkCallback);
196        }
197    }
198
199    private class OmtpVvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
200        PhoneAccountHandle mPhoneAccount;
201        String mAction;
202        NetworkRequest mNetworkRequest;
203
204        public OmtpVvmNetworkRequestCallback(PhoneAccountHandle phoneAccount,
205                String action) {
206            mPhoneAccount = phoneAccount;
207            mAction = action;
208            mNetworkRequest = new NetworkRequest.Builder()
209                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
210                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
211                    .setNetworkSpecifier(
212                            Integer.toString(
213                                    PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount)))
214                    .build();
215        }
216
217        public NetworkRequest getNetworkRequest() {
218            return mNetworkRequest;
219        }
220
221        @Override
222        public void onAvailable(final Network network) {
223            doSync(network, this, mPhoneAccount, mAction);
224        }
225
226        @Override
227        public void onLost(Network network) {
228            releaseNetwork(this);
229        }
230
231        @Override
232        public void onUnavailable() {
233            releaseNetwork(this);
234        }
235    }
236
237    private void doSync(Network network, OmtpVvmNetworkRequestCallback callback,
238            PhoneAccountHandle phoneAccount, String action) {
239        int retryCount = NETWORK_RETRY_COUNT;
240
241        boolean uploadSuccess;
242        boolean downloadSuccess;
243
244        while (retryCount > 0) {
245            uploadSuccess = true;
246            downloadSuccess = true;
247
248            ImapHelper imapHelper = new ImapHelper(this, phoneAccount, network);
249            if (!imapHelper.isSuccessfullyInitialized()) {
250                Log.w(TAG, "Can't retrieve Imap credentials.");
251                releaseNetwork(callback);
252                VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this,
253                        phoneAccount);
254                return;
255            }
256
257            if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
258                uploadSuccess = upload(imapHelper);
259            }
260            if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
261                downloadSuccess = download(imapHelper);
262            }
263
264            Log.v(TAG, "upload succeeded: ["+  String.valueOf(uploadSuccess)
265                    + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
266
267            // Need to check again for whether visual voicemail is enabled because it could have
268            // been disabled while waiting for the response from the network.
269            if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount) &&
270                    (!uploadSuccess || !downloadSuccess)) {
271                retryCount--;
272                // Re-adjust so that only the unsuccessful action needs to be retried.
273                // No need to re-adjust if both are unsuccessful. It means the full sync
274                // failed so the action remains unchanged.
275                if (uploadSuccess) {
276                    action = SYNC_DOWNLOAD_ONLY;
277                } else if (downloadSuccess) {
278                    action = SYNC_UPLOAD_ONLY;
279                }
280
281                Log.v(TAG, "Retrying " + action);
282                LocalLogHelper.log(TAG, "Immediately retrying " + action);
283            } else {
284                // Nothing more to do here, just exit.
285                releaseNetwork(callback);
286
287                VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this, phoneAccount);
288                return;
289            }
290        }
291
292        releaseNetwork(callback);
293        setRetryAlarm(phoneAccount, action);
294    }
295
296    private void requestNetwork(OmtpVvmNetworkRequestCallback networkCallback) {
297        getConnectivityManager().requestNetwork(networkCallback.getNetworkRequest(),
298                networkCallback, NETWORK_REQUEST_TIMEOUT_MILLIS);
299    }
300
301    private void releaseNetwork(NetworkCallback networkCallback) {
302        if (networkCallback != null) {
303            getConnectivityManager().unregisterNetworkCallback(networkCallback);
304        }
305    }
306
307    private ConnectivityManager getConnectivityManager() {
308        if (mConnectivityManager == null) {
309            mConnectivityManager = (ConnectivityManager) this.getSystemService(
310                    Context.CONNECTIVITY_SERVICE);
311        }
312        return mConnectivityManager;
313    }
314
315    private void setRetryAlarm(PhoneAccountHandle phoneAccount, String action) {
316        Intent serviceIntent = new Intent(this, OmtpVvmSyncService.class);
317        serviceIntent.setAction(action);
318        serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_PHONE_ACCOUNT, phoneAccount);
319        PendingIntent pendingIntent = PendingIntent.getService(this, 0, serviceIntent, 0);
320        long retryInterval = VisualVoicemailSettingsUtil.getVisualVoicemailRetryInterval(this,
321                phoneAccount);
322
323        Log.v(TAG, "Retrying "+ action + " in " + retryInterval + "ms");
324        LocalLogHelper.log(TAG, "Retrying "+ action + " in " + retryInterval + "ms");
325
326        AlarmManager alarmManager = (AlarmManager)
327                this.getSystemService(Context.ALARM_SERVICE);
328        alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + retryInterval,
329                pendingIntent);
330
331        VisualVoicemailSettingsUtil.setVisualVoicemailRetryInterval(this, phoneAccount,
332                retryInterval * 2);
333    }
334
335    private boolean upload(ImapHelper imapHelper) {
336        List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
337        List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
338
339        boolean success = true;
340
341        if (deletedVoicemails.size() > 0) {
342            if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
343                // We want to delete selectively instead of all the voicemails for this provider
344                // in case the state changed since the IMAP query was completed.
345                mQueryHelper.deleteFromDatabase(deletedVoicemails);
346            } else {
347                success = false;
348            }
349        }
350
351        if (readVoicemails.size() > 0) {
352            if (imapHelper.markMessagesAsRead(readVoicemails)) {
353                mQueryHelper.markReadInDatabase(readVoicemails);
354            } else {
355                success = false;
356            }
357        }
358
359        return success;
360    }
361
362    private boolean download(ImapHelper imapHelper) {
363        List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
364        List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
365
366        if (localVoicemails == null || serverVoicemails == null) {
367            // Null value means the query failed.
368            return false;
369        }
370
371        Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
372
373        // Go through all the local voicemails and check if they are on the server.
374        // They may be read or deleted on the server but not locally. Perform the
375        // appropriate local operation if the status differs from the server. Remove
376        // the messages that exist both locally and on the server to know which server
377        // messages to insert locally.
378        for (int i = 0; i < localVoicemails.size(); i++) {
379            Voicemail localVoicemail = localVoicemails.get(i);
380            Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
381            if (remoteVoicemail == null) {
382                mQueryHelper.deleteFromDatabase(localVoicemail);
383            } else {
384                if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
385                    mQueryHelper.markReadInDatabase(localVoicemail);
386                }
387            }
388        }
389
390        // The leftover messages are messages that exist on the server but not locally.
391        for (Voicemail remoteVoicemail : remoteMap.values()) {
392            VoicemailContract.Voicemails.insert(this, remoteVoicemail);
393        }
394
395        return true;
396    }
397
398    /**
399     * Builds a map from provider data to message for the given collection of voicemails.
400     */
401    private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
402        Map<String, Voicemail> map = new HashMap<String, Voicemail>();
403        for (Voicemail message : messages) {
404            map.put(message.getSourceData(), message);
405        }
406        return map;
407    }
408}
409