Flesh out EasService some, and start using it.

- Fix some of the TODOs.
- Add it to the manifest.
- Route some operations from EmailSyncAdapterService to it
  during testing/development.

Note that this last bit is a bit Frankenstein right now, so
it's guarded by a constant (which should stay false when checked
in) and in no way will an account function "normally" during the
transition.

Change-Id: I52e5500c7eac1999963712f3bd96d114f9afc3ed
(cherry picked from commit c3ad5a3c532b60f73ff7337f0962211a6f14b919)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0f9392c..73430b9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -79,6 +79,17 @@
         <receiver
             android:name="com.android.emailsync.MailboxAlarmReceiver"/>
 
+        <service
+                android:name="com.android.exchange.service.EasService"
+                android:exported="true">
+<!-- TODO: Switch this from EmailSyncAdapterService
+            <intent-filter>
+                <action
+                        android:name="com.android.email.EXCHANGE_INTENT" />
+            </intent-filter>
+-->
+        </service>
+
         <!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager -->
         <service
             android:name="com.android.exchange.service.EmailSyncAdapterService"
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
index bb1f4da..fc97507 100644
--- a/src/com/android/exchange/service/EasService.java
+++ b/src/com/android/exchange/service/EasService.java
@@ -21,10 +21,12 @@
 import android.content.Intent;
 import android.content.SyncResult;
 import android.database.Cursor;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.provider.CalendarContract;
 import android.provider.ContactsContract;
+import android.text.TextUtils;
 
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
@@ -33,6 +35,7 @@
 import com.android.emailcommon.service.IEmailService;
 import com.android.emailcommon.service.IEmailServiceCallback;
 import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.service.ServiceProxy;
 import com.android.exchange.Eas;
 import com.android.exchange.eas.EasFolderSync;
 import com.android.exchange.eas.EasOperation;
@@ -57,8 +60,14 @@
      */
     private static String[] AUTHORITIES_TO_SYNC;
 
+    /** Bookkeeping for ping tasks & sync threads management. */
     private final PingSyncSynchronizer mSynchronizer;
 
+    /**
+     * Implementation of the IEmailService interface.
+     * For the most part these calls should consist of creating the correct {@link EasOperation}
+     * class and calling {@link #doOperation} with it.
+     */
     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
         @Override
         public void sendMail(final long accountId) {}
@@ -111,6 +120,49 @@
         }
     };
 
+    /**
+     * Content selection string for getting all accounts that are configured for push.
+     * TODO: Add protocol check so that we don't get e.g. IMAP accounts here.
+     * (Not currently necessary but eventually will be.)
+     */
+    private static final String PUSH_ACCOUNTS_SELECTION =
+            EmailContent.AccountColumns.SYNC_INTERVAL +
+                    "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
+
+    /** {@link AsyncTask} to restart pings for all accounts that need it. */
+    private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
+        private boolean mHasRestartedPing = false;
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI,
+                    Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
+            if (c != null) {
+                try {
+                    while (c.moveToNext()) {
+                        final Account account = new Account();
+                        account.restore(c);
+                        if (EasService.this.pingNeededForAccount(account)) {
+                            mHasRestartedPing = true;
+                            EasService.this.mSynchronizer.pushModify(account.mId);
+                        }
+                    }
+                } finally {
+                    c.close();
+                }
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            if (!mHasRestartedPing) {
+                LogUtils.d(TAG, "RestartPingsTask did not start any pings.");
+                EasService.this.mSynchronizer.stopServiceIfIdle();
+            }
+        }
+    }
+
     public EasService() {
         super();
         mSynchronizer = new PingSyncSynchronizer(this);
@@ -125,12 +177,17 @@
                 CalendarContract.AUTHORITY,
                 ContactsContract.AUTHORITY
         };
-        // TODO: Restart all pings that are needed.
+
+        // Restart push for all accounts that need it. Because this requires DB loads, we do it in
+        // an AsyncTask, and we startService to ensure that we stick around long enough for the
+        // task to complete. The task will stop the service if necessary after it's done.
+        startService(new Intent(this, EasService.class));
+        new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
     }
 
     @Override
     public void onDestroy() {
-        // TODO: Stop all running pings.
+        mSynchronizer.stopAllPings();
     }
 
     @Override
@@ -138,6 +195,23 @@
         return mBinder;
     }
 
+    @Override
+    public int onStartCommand(final Intent intent, final int flags, final int startId) {
+        if (intent != null &&
+                TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
+            if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
+                // We've been asked to forcibly shutdown. This happens if email accounts are
+                // deleted, otherwise we can get errors if services are still running for
+                // accounts that are now gone.
+                // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
+                // if accounts disappear out from under us.
+                LogUtils.d(TAG, "Forced shutdown, killing process");
+                System.exit(-1);
+            }
+        }
+        return START_STICKY;
+    }
+
     public int doOperation(final EasOperation operation, final SyncResult syncResult,
             final String loggingName) {
         final long accountId = operation.getAccountId();
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index 834d84b..96eb1a6 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -22,17 +22,20 @@
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.AbstractThreadedSyncAdapter;
+import android.content.ComponentName;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.SyncResult;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.provider.CalendarContract;
 import android.provider.ContactsContract;
@@ -80,6 +83,16 @@
 
     private static final String TAG = Eas.LOG_TAG;
 
+    /**
+     * Temporary while converting to EasService. Do not check in set to true.
+     * When true, delegates various operations to {@link EasService}, for use while developing the
+     * new service.
+     * The two following fields are used to support what happens when this is true.
+     */
+    private static final boolean DELEGATE_TO_EAS_SERVICE = false;
+    private IEmailService mEasService;
+    private ServiceConnection mConnection;
+
     private static final String EXTRA_START_PING = "START_PING";
     private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
     private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS;
@@ -362,6 +375,13 @@
         @Override
         public Bundle validate(final HostAuth hostAuth) {
             LogUtils.d(TAG, "IEmailService.validate");
+            if (mEasService != null) {
+                try {
+                    return mEasService.validate(hostAuth);
+                } catch (final RemoteException re) {
+                    LogUtils.e(TAG, re, "While asking EasService to handle validate");
+                }
+            }
             return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate();
         }
 
@@ -375,6 +395,14 @@
         @Override
         public void updateFolderList(final long accountId) {
             LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
+            if (mEasService != null) {
+                try {
+                    mEasService.updateFolderList(accountId);
+                    return;
+                } catch (final RemoteException re) {
+                    LogUtils.e(TAG, re, "While asking EasService to updateFolderList");
+                }
+            }
             final String emailAddress = getEmailAddressForAccount(accountId);
             if (emailAddress != null) {
                 final Bundle extras = new Bundle(1);
@@ -498,6 +526,21 @@
         // Restart push for all accounts that need it.
         new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
                 AsyncTask.THREAD_POOL_EXECUTOR);
+        if (DELEGATE_TO_EAS_SERVICE) {
+            // TODO: This block is temporary to support the transition to EasService.
+            mConnection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name,  IBinder binder) {
+                    mEasService = IEmailService.Stub.asInterface(binder);
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mEasService = null;
+                }
+            };
+            bindService(new Intent(this, EasService.class), mConnection, Context.BIND_AUTO_CREATE);
+        }
     }
 
     @Override
@@ -509,6 +552,10 @@
                 task.stop();
             }
         }
+        if (DELEGATE_TO_EAS_SERVICE) {
+            // TODO: This block is temporary to support the transition to EasService.
+            unbindService(mConnection);
+        }
     }
 
     @Override
diff --git a/src/com/android/exchange/service/PingSyncSynchronizer.java b/src/com/android/exchange/service/PingSyncSynchronizer.java
index 54c34a9..7871f0f 100644
--- a/src/com/android/exchange/service/PingSyncSynchronizer.java
+++ b/src/com/android/exchange/service/PingSyncSynchronizer.java
@@ -177,7 +177,7 @@
                     // No ping, no running syncs -- start a new ping.
                     // TODO: Fix this.
                     //mPingTask = new PingTask();
-                    mPingTask.start();
+                    //mPingTask.start();
                 } else {
                     // Ping is already running, so tell it to restart to pick up any new params.
                     mPingTask.restart();
@@ -327,4 +327,34 @@
             mLock.unlock();
         }
     }
+
+    /**
+     * Stops our service if our map contains no active accounts.
+     */
+    public void stopServiceIfIdle() {
+        mLock.lock();
+        try {
+            LogUtils.d(TAG, "PSS stopIfIdle");
+            if (mAccountStateMap.size() == 0) {
+                LogUtils.i(TAG, "PSS has no active accounts; stopping service.");
+                mService.stopSelf();
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
+
+    /**
+     * Tells all running ping tasks to stop.
+     */
+    public void stopAllPings() {
+        mLock.lock();
+        try {
+            for (int i = 0; i < mAccountStateMap.size(); ++i) {
+                mAccountStateMap.valueAt(i).pushStop();
+            }
+        } finally {
+            mLock.unlock();
+        }
+    }
 }