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