1/*
2 * Copyright (C) 2014 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
17package com.android.exchange.service;
18
19import android.app.Service;
20import android.content.ContentResolver;
21import android.content.Intent;
22import android.database.Cursor;
23import android.os.AsyncTask;
24import android.os.Bundle;
25import android.os.IBinder;
26import android.provider.CalendarContract;
27import android.provider.ContactsContract;
28import android.text.TextUtils;
29
30import com.android.emailcommon.provider.Account;
31import com.android.emailcommon.provider.EmailContent;
32import com.android.emailcommon.provider.HostAuth;
33import com.android.emailcommon.provider.Mailbox;
34import com.android.emailcommon.service.IEmailService;
35import com.android.emailcommon.service.IEmailServiceCallback;
36import com.android.emailcommon.service.SearchParams;
37import com.android.emailcommon.service.ServiceProxy;
38import com.android.exchange.Eas;
39import com.android.exchange.eas.EasFolderSync;
40import com.android.exchange.eas.EasLoadAttachment;
41import com.android.exchange.eas.EasOperation;
42import com.android.exchange.eas.EasSearch;
43import com.android.mail.utils.LogUtils;
44
45import java.util.HashSet;
46import java.util.Set;
47
48/**
49 * Service to handle all communication with the EAS server. Note that this is completely decoupled
50 * from the sync adapters; sync adapters should make blocking calls on this service to actually
51 * perform any operations.
52 */
53public class EasService extends Service {
54
55    private static final String TAG = Eas.LOG_TAG;
56
57    /**
58     * The content authorities that can be synced for EAS accounts. Initialization must wait until
59     * after we have a chance to call {@link EmailContent#init} (and, for future content types,
60     * possibly other initializations) because that's how we can know what the email authority is.
61     */
62    private static String[] AUTHORITIES_TO_SYNC;
63
64    /** Bookkeeping for ping tasks & sync threads management. */
65    private final PingSyncSynchronizer mSynchronizer;
66
67    /**
68     * Implementation of the IEmailService interface.
69     * For the most part these calls should consist of creating the correct {@link EasOperation}
70     * class and calling {@link #doOperation} with it.
71     */
72    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
73        @Override
74        public void sendMail(final long accountId) {
75            LogUtils.d(TAG, "IEmailService.sendMail: %d", accountId);
76        }
77
78        @Override
79        public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
80                final long attachmentId, final boolean background) {
81            LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
82            final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId,
83                    attachmentId, callback);
84            doOperation(operation, "IEmailService.loadAttachment");
85        }
86
87        @Override
88        public void updateFolderList(final long accountId) {
89            final EasFolderSync operation = new EasFolderSync(EasService.this, accountId);
90            doOperation(operation, "IEmailService.updateFolderList");
91        }
92
93        @Override
94        public void sync(final long accountId, final boolean updateFolderList,
95                final int mailboxType, final long[] folders) {}
96
97        @Override
98        public void pushModify(final long accountId) {
99            LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId);
100            final Account account = Account.restoreAccountWithId(EasService.this, accountId);
101            if (pingNeededForAccount(account)) {
102                mSynchronizer.pushModify(account);
103            } else {
104                mSynchronizer.pushStop(accountId);
105            }
106        }
107
108        @Override
109        public Bundle validate(final HostAuth hostAuth) {
110            final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth);
111            doOperation(operation, "IEmailService.validate");
112            return operation.getValidationResult();
113        }
114
115        @Override
116        public int searchMessages(final long accountId, final SearchParams searchParams,
117                final long destMailboxId) {
118            final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams,
119                    destMailboxId);
120            doOperation(operation, "IEmailService.searchMessages");
121            return operation.getTotalResults();
122        }
123
124        @Override
125        public void sendMeetingResponse(final long messageId, final int response) {
126            LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
127        }
128
129        @Override
130        public Bundle autoDiscover(final String username, final String password) {
131            LogUtils.d(TAG, "IEmailService.autoDiscover");
132            return null;
133        }
134
135        @Override
136        public void setLogging(final int flags) {
137            LogUtils.d(TAG, "IEmailService.setLogging");
138        }
139
140        @Override
141        public void deleteAccountPIMData(final String emailAddress) {
142            LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
143        }
144    };
145
146    /**
147     * Content selection string for getting all accounts that are configured for push.
148     * TODO: Add protocol check so that we don't get e.g. IMAP accounts here.
149     * (Not currently necessary but eventually will be.)
150     */
151    private static final String PUSH_ACCOUNTS_SELECTION =
152            EmailContent.AccountColumns.SYNC_INTERVAL +
153                    "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
154
155    /** {@link AsyncTask} to restart pings for all accounts that need it. */
156    private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
157        private boolean mHasRestartedPing = false;
158
159        @Override
160        protected Void doInBackground(Void... params) {
161            final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI,
162                    Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
163            if (c != null) {
164                try {
165                    while (c.moveToNext()) {
166                        final Account account = new Account();
167                        account.restore(c);
168                        if (EasService.this.pingNeededForAccount(account)) {
169                            mHasRestartedPing = true;
170                            EasService.this.mSynchronizer.pushModify(account);
171                        }
172                    }
173                } finally {
174                    c.close();
175                }
176            }
177            return null;
178        }
179
180        @Override
181        protected void onPostExecute(Void result) {
182            if (!mHasRestartedPing) {
183                LogUtils.d(TAG, "RestartPingsTask did not start any pings.");
184                EasService.this.mSynchronizer.stopServiceIfIdle();
185            }
186        }
187    }
188
189    public EasService() {
190        super();
191        mSynchronizer = new PingSyncSynchronizer(this);
192    }
193
194    @Override
195    public void onCreate() {
196        super.onCreate();
197        EmailContent.init(this);
198        AUTHORITIES_TO_SYNC = new String[] {
199                EmailContent.AUTHORITY,
200                CalendarContract.AUTHORITY,
201                ContactsContract.AUTHORITY
202        };
203
204        // Restart push for all accounts that need it. Because this requires DB loads, we do it in
205        // an AsyncTask, and we startService to ensure that we stick around long enough for the
206        // task to complete. The task will stop the service if necessary after it's done.
207        startService(new Intent(this, EasService.class));
208        new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
209    }
210
211    @Override
212    public void onDestroy() {
213        mSynchronizer.stopAllPings();
214    }
215
216    @Override
217    public IBinder onBind(final Intent intent) {
218        return mBinder;
219    }
220
221    @Override
222    public int onStartCommand(final Intent intent, final int flags, final int startId) {
223        if (intent != null &&
224                TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
225            if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
226                // We've been asked to forcibly shutdown. This happens if email accounts are
227                // deleted, otherwise we can get errors if services are still running for
228                // accounts that are now gone.
229                // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
230                // if accounts disappear out from under us.
231                LogUtils.d(TAG, "Forced shutdown, killing process");
232                System.exit(-1);
233            }
234        }
235        return START_STICKY;
236    }
237
238    public int doOperation(final EasOperation operation, final String loggingName) {
239        final long accountId = operation.getAccountId();
240        final Account account = operation.getAccount();
241        LogUtils.d(TAG, "%s: %d", loggingName, accountId);
242        mSynchronizer.syncStart(accountId);
243        // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA
244        // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls.
245        // If we add a wakelock (or anything else for that matter) here, must remember to undo
246        // it in the finally block below.
247        // On the other hand, even for SAs, it doesn't hurt to get a wakelock here.
248        try {
249            return operation.performOperation();
250        } finally {
251            mSynchronizer.syncEnd(account);
252        }
253    }
254
255    /**
256     * Determine whether this account is configured with folders that are ready for push
257     * notifications.
258     * @param account The {@link Account} that we're interested in.
259     * @return Whether this account needs to ping.
260     */
261    public boolean pingNeededForAccount(final Account account) {
262        // Check account existence.
263        if (account == null || account.mId == Account.NO_ACCOUNT) {
264            LogUtils.d(TAG, "Do not ping: Account not found or not valid");
265            return false;
266        }
267
268        // Check if account is configured for a push sync interval.
269        if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) {
270            LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId);
271            return false;
272        }
273
274        // Check security hold status of the account.
275        if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
276            LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId);
277            return false;
278        }
279
280        // Check if the account has performed at least one sync so far (accounts must perform
281        // the initial sync before push is possible).
282        if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
283            LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId);
284            return false;
285        }
286
287        // Check that there's at least one mailbox that is both configured for push notifications,
288        // and whose content type is enabled for sync in the account manager.
289        final android.accounts.Account amAccount = new android.accounts.Account(
290                        account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
291
292        final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC);
293        // If we have at least one sync-enabled content type, check for syncing mailboxes.
294        if (!authsToSync.isEmpty()) {
295            final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId);
296            if (c != null) {
297                try {
298                    while (c.moveToNext()) {
299                        final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
300                        if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
301                            return true;
302                        }
303                    }
304                } finally {
305                    c.close();
306                }
307            }
308        }
309        LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId);
310        return false;
311    }
312
313    /**
314     * Determine which content types are set to sync for an account.
315     * @param account The account whose sync settings we're looking for.
316     * @param authorities All possible authorities we could care about.
317     * @return The authorities for the content types we want to sync for account.
318     */
319    private static Set<String> getAuthoritiesToSync(final android.accounts.Account account,
320            final String[] authorities) {
321        final HashSet<String> authsToSync = new HashSet();
322        for (final String authority : authorities) {
323            if (ContentResolver.getSyncAutomatically(account, authority)) {
324                authsToSync.add(authority);
325            }
326        }
327        return authsToSync;
328    }
329}
330