1/*
2 * Copyright (C) 2010 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.email.service;
18
19import android.accounts.AccountManager;
20import android.app.IntentService;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.PeriodicSync;
28import android.content.pm.PackageManager;
29import android.database.Cursor;
30import android.net.Uri;
31import android.os.Bundle;
32import android.provider.CalendarContract;
33import android.provider.ContactsContract;
34import android.text.TextUtils;
35import android.text.format.DateUtils;
36
37import com.android.email.EmailIntentService;
38import com.android.email.Preferences;
39import com.android.email.R;
40import com.android.email.SecurityPolicy;
41import com.android.email.provider.AccountReconciler;
42import com.android.emailcommon.Logging;
43import com.android.emailcommon.provider.Account;
44import com.android.emailcommon.provider.EmailContent;
45import com.android.emailcommon.provider.EmailContent.AccountColumns;
46import com.android.emailcommon.provider.HostAuth;
47import com.android.mail.providers.UIProvider;
48import com.android.mail.utils.LogUtils;
49import com.android.mail.utils.NotificationActionUtils;
50import com.google.common.annotations.VisibleForTesting;
51import com.google.common.collect.Maps;
52
53import java.util.Collections;
54import java.util.HashSet;
55import java.util.List;
56import java.util.Map;
57import java.util.Set;
58
59/**
60 * The service that really handles broadcast intents on a worker thread.
61 *
62 * We make it a service, because:
63 * <ul>
64 *   <li>So that it's less likely for the process to get killed.
65 *   <li>Even if it does, the Intent that have started it will be re-delivered by the system,
66 *   and we can start the process again.  (Using {@link #setIntentRedelivery}).
67 * </ul>
68 *
69 * This also handles the DeviceAdminReceiver in SecurityPolicy, because it is also
70 * a BroadcastReceiver and requires the same processing semantics.
71 */
72public class EmailBroadcastProcessorService extends IntentService {
73    // Action used for BroadcastReceiver entry point
74    private static final String ACTION_BROADCAST = "broadcast_receiver";
75
76    // This is a helper used to process DeviceAdminReceiver messages
77    private static final String ACTION_DEVICE_POLICY_ADMIN = "com.android.email.devicepolicy";
78    private static final String EXTRA_DEVICE_POLICY_ADMIN = "message_code";
79
80    // Action used for EmailUpgradeBroadcastReceiver.
81    private static final String ACTION_UPGRADE_BROADCAST = "upgrade_broadcast_receiver";
82
83    public EmailBroadcastProcessorService() {
84        // Class name will be the thread name.
85        super(EmailBroadcastProcessorService.class.getName());
86
87        // Intent should be redelivered if the process gets killed before completing the job.
88        setIntentRedelivery(true);
89    }
90
91    /**
92     * Entry point for {@link EmailBroadcastReceiver}.
93     */
94    public static void processBroadcastIntent(Context context, Intent broadcastIntent) {
95        Intent i = new Intent(context, EmailBroadcastProcessorService.class);
96        i.setAction(ACTION_BROADCAST);
97        i.putExtra(Intent.EXTRA_INTENT, broadcastIntent);
98        context.startService(i);
99    }
100
101    public static void processUpgradeBroadcastIntent(final Context context) {
102        final Intent i = new Intent(context, EmailBroadcastProcessorService.class);
103        i.setAction(ACTION_UPGRADE_BROADCAST);
104        context.startService(i);
105    }
106
107    /**
108     * Entry point for {@link com.android.email.SecurityPolicy.PolicyAdmin}.  These will
109     * simply callback to {@link
110     * com.android.email.SecurityPolicy#onDeviceAdminReceiverMessage(Context, int)}.
111     */
112    public static void processDevicePolicyMessage(Context context, int message) {
113        Intent i = new Intent(context, EmailBroadcastProcessorService.class);
114        i.setAction(ACTION_DEVICE_POLICY_ADMIN);
115        i.putExtra(EXTRA_DEVICE_POLICY_ADMIN, message);
116        context.startService(i);
117    }
118
119    @Override
120    protected void onHandleIntent(Intent intent) {
121        // This method is called on a worker thread.
122
123        // Dispatch from entry point
124        final String action = intent.getAction();
125        if (ACTION_BROADCAST.equals(action)) {
126            final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
127            final String broadcastAction = broadcastIntent.getAction();
128
129            if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
130                onBootCompleted();
131            } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
132                onSystemAccountChanged();
133            } else if (Intent.ACTION_LOCALE_CHANGED.equals(broadcastAction) ||
134                    UIProvider.ACTION_UPDATE_NOTIFICATION.equals((broadcastAction))) {
135                broadcastIntent.setClass(this, EmailIntentService.class);
136                startService(broadcastIntent);
137            }
138        } else if (ACTION_DEVICE_POLICY_ADMIN.equals(action)) {
139            int message = intent.getIntExtra(EXTRA_DEVICE_POLICY_ADMIN, -1);
140            SecurityPolicy.onDeviceAdminReceiverMessage(this, message);
141        } else if (ACTION_UPGRADE_BROADCAST.equals(action)) {
142            onAppUpgrade();
143        }
144    }
145
146    private void disableComponent(final Class<?> klass) {
147        getPackageManager().setComponentEnabledSetting(new ComponentName(this, klass),
148                PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
149    }
150
151    private boolean isComponentDisabled(final Class<?> klass) {
152        return getPackageManager().getComponentEnabledSetting(new ComponentName(this, klass))
153                == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
154    }
155
156    private void updateAccountManagerAccountsOfType(final String amAccountType,
157            final Map<String, String> protocolMap) {
158        final android.accounts.Account[] amAccounts =
159                AccountManager.get(this).getAccountsByType(amAccountType);
160
161        for (android.accounts.Account amAccount: amAccounts) {
162            EmailServiceUtils.updateAccountManagerType(this, amAccount, protocolMap);
163        }
164    }
165
166    /**
167     * Delete all periodic syncs for an account.
168     * @param amAccount The account for which to disable syncs.
169     * @param authority The authority for which to disable syncs.
170     */
171    private static void removePeriodicSyncs(final android.accounts.Account amAccount,
172            final String authority) {
173        final List<PeriodicSync> syncs =
174                ContentResolver.getPeriodicSyncs(amAccount, authority);
175        for (final PeriodicSync sync : syncs) {
176            ContentResolver.removePeriodicSync(amAccount, authority, sync.extras);
177        }
178    }
179
180    /**
181     * Remove all existing periodic syncs for an account type, and add the necessary syncs.
182     * @param amAccountType The account type to handle.
183     * @param syncIntervals The map of all account addresses to sync intervals in the DB.
184     */
185    private void fixPeriodicSyncs(final String amAccountType,
186            final Map<String, Integer> syncIntervals) {
187        final android.accounts.Account[] amAccounts =
188                AccountManager.get(this).getAccountsByType(amAccountType);
189        for (android.accounts.Account amAccount : amAccounts) {
190            // First delete existing periodic syncs.
191            removePeriodicSyncs(amAccount, EmailContent.AUTHORITY);
192            removePeriodicSyncs(amAccount, CalendarContract.AUTHORITY);
193            removePeriodicSyncs(amAccount, ContactsContract.AUTHORITY);
194
195            // Add back a sync for this account if necessary (i.e. the account has a positive
196            // sync interval in the DB). This assumes that the email app requires unique email
197            // addresses for each account, which is currently the case.
198            final Integer syncInterval = syncIntervals.get(amAccount.name);
199            if (syncInterval != null && syncInterval > 0) {
200                // Sync interval is stored in minutes in DB, but we want the value in seconds.
201                ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, Bundle.EMPTY,
202                        syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
203            }
204        }
205    }
206
207    /** Projection used for getting sync intervals for all accounts. */
208    private static final String[] ACCOUNT_SYNC_INTERVAL_PROJECTION =
209            { AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_INTERVAL };
210    private static final int ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN = 0;
211    private static final int ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN = 1;
212
213    /**
214     * Get the sync interval for all accounts, as stored in the DB.
215     * @return The map of all sync intervals by account email address.
216     */
217    private Map<String, Integer> getSyncIntervals() {
218        final Cursor c = getContentResolver().query(Account.CONTENT_URI,
219                ACCOUNT_SYNC_INTERVAL_PROJECTION, null, null, null);
220        if (c != null) {
221            final Map<String, Integer> periodicSyncs =
222                    Maps.newHashMapWithExpectedSize(c.getCount());
223            try {
224                while (c.moveToNext()) {
225                    periodicSyncs.put(c.getString(ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN),
226                            c.getInt(ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN));
227                }
228            } finally {
229                c.close();
230            }
231            return periodicSyncs;
232        }
233        return Collections.emptyMap();
234    }
235
236    @VisibleForTesting
237    protected static void removeNoopUpgrades(final Map<String, String> protocolMap) {
238        final Set<String> keySet = new HashSet<String>(protocolMap.keySet());
239        for (final String key : keySet) {
240            if (TextUtils.equals(key, protocolMap.get(key))) {
241                protocolMap.remove(key);
242            }
243        }
244    }
245
246    private void onAppUpgrade() {
247        if (isComponentDisabled(EmailUpgradeBroadcastReceiver.class)) {
248            return;
249        }
250        // When upgrading to a version that changes the protocol strings, we need to essentially
251        // rename the account manager type for all existing accounts, so we add new ones and delete
252        // the old.
253        // We specify the translations in this map. We map from old protocol name to new protocol
254        // name, and from protocol name + "_type" to new account manager type name. (Email1 did
255        // not use distinct account manager types for POP and IMAP, but Email2 does, hence this
256        // weird mapping.)
257        final Map<String, String> protocolMap = Maps.newHashMapWithExpectedSize(4);
258        protocolMap.put("imap", getString(R.string.protocol_legacy_imap));
259        protocolMap.put("pop3", getString(R.string.protocol_pop3));
260        removeNoopUpgrades(protocolMap);
261        if (!protocolMap.isEmpty()) {
262            protocolMap.put("imap_type", getString(R.string.account_manager_type_legacy_imap));
263            protocolMap.put("pop3_type", getString(R.string.account_manager_type_pop3));
264            updateAccountManagerAccountsOfType("com.android.email", protocolMap);
265        }
266
267        protocolMap.clear();
268        protocolMap.put("eas", getString(R.string.protocol_eas));
269        removeNoopUpgrades(protocolMap);
270        if (!protocolMap.isEmpty()) {
271            protocolMap.put("eas_type", getString(R.string.account_manager_type_exchange));
272            updateAccountManagerAccountsOfType("com.android.exchange", protocolMap);
273        }
274
275        // Disable the old authenticators.
276        disableComponent(LegacyEmailAuthenticatorService.class);
277        disableComponent(LegacyEasAuthenticatorService.class);
278
279        // Fix periodic syncs.
280        final Map<String, Integer> syncIntervals = getSyncIntervals();
281        for (final EmailServiceUtils.EmailServiceInfo service
282                : EmailServiceUtils.getServiceInfoList(this)) {
283            fixPeriodicSyncs(service.accountType, syncIntervals);
284        }
285
286        // Disable the upgrade broadcast receiver now that we're fully upgraded.
287        disableComponent(EmailUpgradeBroadcastReceiver.class);
288    }
289
290    /**
291     * Handles {@link Intent#ACTION_BOOT_COMPLETED}.  Called on a worker thread.
292     */
293    private void onBootCompleted() {
294        performOneTimeInitialization();
295        reconcileAndStartServices();
296    }
297
298    private void reconcileAndStartServices() {
299        /**
300         *  We can get here before the ACTION_UPGRADE_BROADCAST is received, so make sure the
301         *  accounts are converted otherwise terrible, horrible things will happen.
302         */
303        onAppUpgrade();
304        // Reconcile accounts
305        AccountReconciler.reconcileAccounts(this);
306        // Starts remote services, if any
307        EmailServiceUtils.startRemoteServices(this);
308    }
309
310    private void performOneTimeInitialization() {
311        final Preferences pref = Preferences.getPreferences(this);
312        int progress = pref.getOneTimeInitializationProgress();
313        final int initialProgress = progress;
314
315        if (progress < 1) {
316            LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 1");
317            progress = 1;
318            EmailServiceUtils.enableExchangeComponent(this);
319        }
320
321        if (progress < 2) {
322            LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 2");
323            progress = 2;
324            setImapDeletePolicy(this);
325        }
326
327        // Add your initialization steps here.
328        // Use "progress" to skip the initializations that's already done before.
329        // Using this preference also makes it safe when a user skips an upgrade.  (i.e. upgrading
330        // version N to version N+2)
331
332        if (progress != initialProgress) {
333            pref.setOneTimeInitializationProgress(progress);
334            LogUtils.i(Logging.LOG_TAG, "Onetime initialization: completed.");
335        }
336    }
337
338    /**
339     * Sets the delete policy to the correct value for all IMAP accounts. This will have no
340     * effect on either EAS or POP3 accounts.
341     */
342    /*package*/ static void setImapDeletePolicy(Context context) {
343        ContentResolver resolver = context.getContentResolver();
344        Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
345                null, null, null);
346        try {
347            while (c.moveToNext()) {
348                long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
349                HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey);
350                String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
351                if (legacyImapProtocol.equals(recvAuth.mProtocol)) {
352                    int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
353                    flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
354                    flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT;
355                    ContentValues cv = new ContentValues();
356                    cv.put(AccountColumns.FLAGS, flags);
357                    long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
358                    Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
359                    resolver.update(uri, cv, null, null);
360                }
361            }
362        } finally {
363            c.close();
364        }
365    }
366
367    private void onSystemAccountChanged() {
368        LogUtils.i(Logging.LOG_TAG, "System accounts updated.");
369        reconcileAndStartServices();
370        // Resend all notifications, so that there is no notification that points to a removed
371        // account.
372        NotificationActionUtils.resendNotifications(getApplicationContext(),
373                null /* all accounts */, null /* all folders */);
374    }
375}
376