Perform syncs with the voicemail server through data connection only.

This change covers anytime the voicemail provider data changes, initial
download of voicemails and syncing when receiving a "Mailbox Update"
sync request.

Bug: 20345518

Change-Id: Ib0ca46e4a70312a581d8f99dfa6a3e389e440162
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f25b90f..3c5e5d5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -641,16 +641,6 @@
             </intent-filter>
         </receiver>
         <service
-            android:name="com.android.phone.vvm.omtp.sync.OmtpVvmSyncService"
-            android:exported="true"
-            android:process=":sync">
-            <intent-filter>
-                <action android:name="android.content.SyncAdapter"/>
-            </intent-filter>
-            <meta-data android:name="android.content.SyncAdapter"
-                    android:resource="@xml/syncadapter" />
-        </service>
-        <service
             android:name="android.telecom.AuthenticatorService">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator"/>
@@ -677,5 +667,20 @@
                    android:mimeType="vnd.android.cursor.item/voicemail" />
           </intent-filter>
        </receiver>
+       <receiver
+           android:name="com.android.phone.vvm.omtp.sync.VoicemailProviderChangeReceiver"
+           android:exported="true">
+           <intent-filter>
+              <action android:name="android.intent.action.PROVIDER_CHANGED" />
+               <data
+                   android:scheme="content"
+                   android:host="com.android.voicemail"
+                   android:mimeType="vnd.android.cursor.dir/voicemails"/>
+          </intent-filter>
+       </receiver>
+       <service
+            android:name="com.android.phone.vvm.omtp.sync.OmtpVvmSyncService"
+            android:exported="false"
+       />
     </application>
 </manifest>
diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml
deleted file mode 100644
index b0484b1..0000000
--- a/res/xml/syncadapter.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<sync-adapter
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:contentAuthority="com.android.voicemail"
-        android:accountType="com.android.phone.vvm.omtp"
-        android:userVisible="false"
-        android:supportsUploading="true"
-        android:allowParallelSyncs="false"
-        android:isAlwaysSyncable="true"/>
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/SimChangeReceiver.java b/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
index 13e9c74..cd7a694 100644
--- a/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
+++ b/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
@@ -108,9 +108,9 @@
         }
 
         Log.i(TAG, "Requesting VVM activation for subId: " + subId);
-        SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
 
         OmtpMessageSender messageSender = null;
+        SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
         switch (vvmType) {
             case TelephonyManager.VVM_TYPE_OMTP:
                 messageSender = new OmtpStandardMessageSender(smsManager, (short) applicationPort,
diff --git a/src/com/android/phone/vvm/omtp/fetch/FetchVoicemailReceiver.java b/src/com/android/phone/vvm/omtp/fetch/FetchVoicemailReceiver.java
index 179fec3..0110d65 100644
--- a/src/com/android/phone/vvm/omtp/fetch/FetchVoicemailReceiver.java
+++ b/src/com/android/phone/vvm/omtp/fetch/FetchVoicemailReceiver.java
@@ -128,8 +128,6 @@
     private class OmtpVvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
         @Override
         public void onAvailable(final Network network) {
-            super.onAvailable(network);
-
             Executor executor = Executors.newCachedThreadPool();
             executor.execute(new Runnable() {
                 @Override
@@ -143,13 +141,11 @@
 
         @Override
         public void onLost(Network network) {
-            super.onLost(network);
             releaseNetwork();
         }
 
         @Override
         public void onUnavailable() {
-            super.onUnavailable();
             releaseNetwork();
         }
     }
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
index 7a2f5ad..222ea26 100644
--- a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -17,10 +17,8 @@
 
 import android.accounts.Account;
 import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.os.Bundle;
 import android.provider.Telephony;
 import android.provider.VoicemailContract;
 import android.telecom.PhoneAccountHandle;
@@ -32,7 +30,7 @@
 import com.android.phone.PhoneUtils;
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.sync.OmtpVvmSyncAccountManager;
-import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService.OmtpVvmSyncAdapter;
+import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService;
 import com.android.phone.vvm.omtp.sync.VoicemailsQueryHelper;
 
 /**
@@ -96,12 +94,12 @@
                 queryHelper.insertIfUnique(voicemail);
                 break;
             case OmtpConstants.MAILBOX_UPDATE:
-                // Needs a total resync
-                Bundle bundle = new Bundle();
-                bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_DOWNLOAD, true);
-                ContentResolver.requestSync(
-                        new Account(mPhoneAccount.getId(), OmtpVvmSyncAccountManager.ACCOUNT_TYPE),
-                        VoicemailContract.AUTHORITY, bundle);
+                Account account = new Account(
+                        mPhoneAccount.getId(), OmtpVvmSyncAccountManager.ACCOUNT_TYPE);
+                Intent serviceIntent = new Intent(mContext, OmtpVvmSyncService.class);
+                serviceIntent.setAction(OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
+                serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_ACCOUNT, account);
+                mContext.startService(serviceIntent);
                 break;
             case OmtpConstants.GREETINGS_UPDATE:
                 // Not implemented in V1
@@ -135,9 +133,9 @@
         // Add a phone state listener so that changes to the communication channels can be recorded.
         vvmAccountSyncManager.addPhoneStateListener(account);
 
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_DOWNLOAD, true);
-        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
-        ContentResolver.requestSync(account, VoicemailContract.AUTHORITY, bundle);
+        Intent serviceIntent = new Intent(mContext, OmtpVvmSyncService.class);
+        serviceIntent.setAction(OmtpVvmSyncService.SYNC_FULL_SYNC);
+        serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_ACCOUNT, account);
+        mContext.startService(serviceIntent);
     }
 }
diff --git a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncAccountManager.java b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncAccountManager.java
index fcb284e..6554b71 100644
--- a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncAccountManager.java
+++ b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncAccountManager.java
@@ -133,6 +133,10 @@
         mTelephonyManager.listen(phoneStateListener, 0);
     }
 
+    public Account[] getOmtpAccounts() {
+        return mAccountManager.getAccountsByType(ACCOUNT_TYPE);
+    }
+
     /**
      * Check if a certain account is registered.
      *
@@ -141,7 +145,7 @@
      * accounts. {@code false} otherwise.
      */
     public boolean isAccountRegistered(Account account) {
-        Account[] accounts = mAccountManager.getAccountsByType(ACCOUNT_TYPE);
+        Account[] accounts = getOmtpAccounts();
         for (int i = 0; i < accounts.length; i++) {
             if (account.equals(accounts[i])) {
                 return true;
diff --git a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
index 7fb4df2..0a6b5c1 100644
--- a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
+++ b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
@@ -13,27 +13,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License
  */
-
-/**
- * A {@link Service} which runs the internal implementation of {@link AbstractThreadedSyncAdapter},
- * syncing voicemails to and from a visual voicemail server.
- */
-
 package com.android.phone.vvm.omtp.sync;
 
 import android.accounts.Account;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
+import android.app.IntentService;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SyncResult;
-import android.os.Bundle;
-import android.os.IBinder;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.provider.VoicemailContract;
 import android.telecom.Voicemail;
+import android.util.Log;
 
+import com.android.phone.PhoneUtils;
 import com.android.phone.vvm.omtp.imap.ImapHelper;
 
 import java.util.HashMap;
@@ -41,108 +36,177 @@
 import java.util.Map;
 
 /**
- * A service to run the VvmSyncAdapter.
+ * Sync OMTP visual voicemail.
  */
-public class OmtpVvmSyncService extends Service {
-    // Storage for an instance of the sync adapter
-    private static OmtpVvmSyncAdapter sSyncAdapter = null;
-    // Object to use as a thread-safe lock
-    private static final Object sSyncAdapterLock = new Object();
+public class OmtpVvmSyncService extends IntentService {
+    private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
+
+    /** Signifies a sync with both uploading to the server and downloading from the server. */
+    public static final String SYNC_FULL_SYNC = "full_sync";
+    /** Only upload to the server. */
+    public static final String SYNC_UPLOAD_ONLY = "upload_only";
+    /** Only download from the server. */
+    public static final String SYNC_DOWNLOAD_ONLY = "download_only";
+    /** The account to sync. */
+    public static final String EXTRA_ACCOUNT = "account";
+
+    // Timeout used to call ConnectivityManager.requestNetwork
+    private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
+
+    private VoicemailsQueryHelper mQueryHelper;
+
+    private ConnectivityManager mConnectivityManager;
+
+    public OmtpVvmSyncService() {
+        super("OmtpVvmSyncService");
+    }
 
     @Override
     public void onCreate() {
-        synchronized (sSyncAdapterLock) {
-            if (sSyncAdapter == null) {
-                sSyncAdapter = new OmtpVvmSyncAdapter(getApplicationContext(), true);
-            }
-        }
+        super.onCreate();
+        mQueryHelper = new VoicemailsQueryHelper(this);
     }
 
     @Override
-    public IBinder onBind(Intent intent) {
-        return sSyncAdapter.getSyncAdapterBinder();
+    protected void onHandleIntent(Intent intent) {
+        if (intent == null) {
+            Log.d(TAG, "onHandleIntent: could not handle null intent");
+            return;
+        }
+
+        String action = intent.getAction();
+        OmtpVvmSyncAccountManager syncAccountManager = OmtpVvmSyncAccountManager.getInstance(this);
+        Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
+        if (account != null && syncAccountManager.isAccountRegistered(account)) {
+            Log.v(TAG, "Sync requested: " + action + " - for account: " + account.name);
+            doSync(account, action);
+        } else {
+            Log.v(TAG, "Sync requested: " + action + " - for all accounts");
+            Account[] accounts = syncAccountManager.getOmtpAccounts();
+            for (int i = 0; i < accounts.length; i++) {
+                doSync(accounts[i], action);
+            }
+        }
     }
 
-    public class OmtpVvmSyncAdapter extends AbstractThreadedSyncAdapter {
-        /**
-         * Sync triggers should pass this extra to clear the database and freshly populate from the
-         * server.
-         */
-        public static final String SYNC_EXTRAS_DOWNLOAD = "extra_download";
+    private void doSync(Account account, String action) {
+        int subId = PhoneUtils.getSubIdForPhoneAccountHandle(
+                PhoneUtils.makePstnPhoneAccountHandle(account.name));
 
-        private Context mContext;
+        NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .setNetworkSpecifier(Integer.toString(subId))
+                .build();
+        NetworkCallback networkCallback = new OmtpVvmNetworkRequestCallback(this, account, action);
+        getConnectivityManager().requestNetwork(
+                networkRequest, networkCallback, NETWORK_REQUEST_TIMEOUT_MILLIS);
+    }
 
-        public OmtpVvmSyncAdapter(Context context, boolean autoInitialize) {
-            super(context, autoInitialize);
+    private class OmtpVvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
+        Context mContext;
+        Account mAccount;
+        String mAction;
+
+        public OmtpVvmNetworkRequestCallback(Context context, Account account, String action) {
             mContext = context;
+            mAccount = account;
+            mAction = action;
         }
 
         @Override
-        public void onPerformSync(Account account, Bundle extras, String authority,
-                ContentProviderClient provider, SyncResult syncResult) {
-            ImapHelper imapHelper = new ImapHelper(mContext, account, null);
-            VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
-
-            if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
-                List<Voicemail> readVoicemails = queryHelper.getReadVoicemails();
-                List<Voicemail> deletedVoicemails = queryHelper.getDeletedVoicemails();
-
-                if (deletedVoicemails != null &&
-                        imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
-                    // We want to delete selectively instead of all the voicemails for this provider
-                    // in case the state changed since the IMAP query was completed.
-                    queryHelper.deleteFromDatabase(deletedVoicemails);
-                }
-
-                if (readVoicemails != null && imapHelper.markMessagesAsRead(readVoicemails)) {
-                    queryHelper.markReadInDatabase(readVoicemails);
-                }
+        public void onAvailable(final Network network) {
+            ImapHelper imapHelper = new ImapHelper(mContext, mAccount, network);
+            if (SYNC_FULL_SYNC.equals(mAction) || SYNC_UPLOAD_ONLY.equals(mAction)) {
+                upload(imapHelper);
             }
+            if (SYNC_FULL_SYNC.equals(mAction) || SYNC_DOWNLOAD_ONLY.equals(mAction)) {
+                download(imapHelper);
+            }
+            releaseNetwork();
+        }
 
-            if (extras.getBoolean(SYNC_EXTRAS_DOWNLOAD, false)) {
-                List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
-                List<Voicemail> localVoicemails = queryHelper.getAllVoicemails();
+        @Override
+        public void onLost(Network network) {
+            releaseNetwork();
+        }
 
-                if (localVoicemails == null || serverVoicemails == null) {
-                    // Null value means the query failed.
-                    return;
-                }
+        @Override
+        public void onUnavailable() {
+            releaseNetwork();
+        }
 
-                Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+        private void releaseNetwork() {
+            getConnectivityManager().unregisterNetworkCallback(this);
+        }
+    }
 
-                // Go through all the local voicemails and check if they are on the server.
-                // They may be read or deleted on the server but not locally. Perform the
-                // appropriate local operation if the status differs from the server. Remove
-                // the messages that exist both locally and on the server to know which server
-                // messages to insert locally.
-                for (int i = 0; i < localVoicemails.size(); i++) {
-                    Voicemail localVoicemail = localVoicemails.get(i);
-                    Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
-                    if (remoteVoicemail == null) {
-                        queryHelper.deleteFromDatabase(localVoicemail);
-                    } else {
-                        if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
-                            queryHelper.markReadInDatabase(localVoicemail);
-                        }
-                    }
-                }
+    private ConnectivityManager getConnectivityManager() {
+        if (mConnectivityManager == null) {
+            mConnectivityManager = (ConnectivityManager) this.getSystemService(
+                    Context.CONNECTIVITY_SERVICE);
+        }
+        return mConnectivityManager;
+    }
 
-                // The leftover messages are messages that exist on the server but not locally.
-                for (Voicemail remoteVoicemail : remoteMap.values()) {
-                    VoicemailContract.Voicemails.insert(mContext, remoteVoicemail);
+    private void upload(ImapHelper imapHelper) {
+        List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+        List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
+
+        if (deletedVoicemails != null &&
+                imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+            // We want to delete selectively instead of all the voicemails for this provider
+            // in case the state changed since the IMAP query was completed.
+            mQueryHelper.deleteFromDatabase(deletedVoicemails);
+        }
+
+        if (readVoicemails != null && imapHelper.markMessagesAsRead(readVoicemails)) {
+            mQueryHelper.markReadInDatabase(readVoicemails);
+        }
+    }
+
+    private void download(ImapHelper imapHelper) {
+        List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+        List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+        if (localVoicemails == null || serverVoicemails == null) {
+            // Null value means the query failed.
+            return;
+        }
+
+        Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+        // Go through all the local voicemails and check if they are on the server.
+        // They may be read or deleted on the server but not locally. Perform the
+        // appropriate local operation if the status differs from the server. Remove
+        // the messages that exist both locally and on the server to know which server
+        // messages to insert locally.
+        for (int i = 0; i < localVoicemails.size(); i++) {
+            Voicemail localVoicemail = localVoicemails.get(i);
+            Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+            if (remoteVoicemail == null) {
+                mQueryHelper.deleteFromDatabase(localVoicemail);
+            } else {
+                if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+                    mQueryHelper.markReadInDatabase(localVoicemail);
                 }
             }
         }
 
-        /**
-         * Builds a map from provider data to message for the given collection of voicemails.
-         */
-        private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
-            Map<String, Voicemail> map = new HashMap<String, Voicemail>();
-            for (Voicemail message : messages) {
-                map.put(message.getSourceData(), message);
-            }
-            return map;
+        // The leftover messages are messages that exist on the server but not locally.
+        for (Voicemail remoteVoicemail : remoteMap.values()) {
+            VoicemailContract.Voicemails.insert(this, remoteVoicemail);
         }
     }
+
+    /**
+     * Builds a map from provider data to message for the given collection of voicemails.
+     */
+    private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+        Map<String, Voicemail> map = new HashMap<String, Voicemail>();
+        for (Voicemail message : messages) {
+            map.put(message.getSourceData(), message);
+        }
+        return map;
+    }
 }
diff --git a/src/com/android/phone/vvm/omtp/sync/VoicemailProviderChangeReceiver.java b/src/com/android/phone/vvm/omtp/sync/VoicemailProviderChangeReceiver.java
new file mode 100644
index 0000000..fc1823e
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sync/VoicemailProviderChangeReceiver.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.phone.vvm.omtp.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receives changes to the voicemail provider so they can be sent to the voicemail server.
+ */
+public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        OmtpVvmSyncAccountManager syncAccountManager =
+                OmtpVvmSyncAccountManager.getInstance(context);
+        if (syncAccountManager.getOmtpAccounts().length > 0) {
+            Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class);
+            serviceIntent.setAction(OmtpVvmSyncService.SYNC_UPLOAD_ONLY);
+            context.startService(serviceIntent);
+        }
+    }
+}