SecurityPolicy.java revision 4f5d4e29a8180b5c1e8cd80f5e3e9c4ed098049c
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;
18
19import com.android.email.activity.setup.AccountSecurity;
20import com.android.email.service.EmailBroadcastProcessorService;
21import com.android.emailcommon.Logging;
22import com.android.emailcommon.provider.EmailContent;
23import com.android.emailcommon.provider.EmailContent.Account;
24import com.android.emailcommon.provider.EmailContent.AccountColumns;
25import com.android.emailcommon.service.PolicySet;
26
27import android.app.admin.DeviceAdminInfo;
28import android.app.admin.DeviceAdminReceiver;
29import android.app.admin.DevicePolicyManager;
30import android.content.ComponentName;
31import android.content.ContentResolver;
32import android.content.ContentValues;
33import android.content.Context;
34import android.content.Intent;
35import android.database.Cursor;
36import android.os.Environment;
37import android.util.Log;
38
39/**
40 * Utility functions to support reading and writing security policies, and handshaking the device
41 * into and out of various security states.
42 */
43public class SecurityPolicy {
44    private static final String TAG = "SecurityPolicy";
45    private static SecurityPolicy sInstance = null;
46    private Context mContext;
47    private DevicePolicyManager mDPM;
48    private ComponentName mAdminName;
49    private PolicySet mAggregatePolicy;
50
51    /* package */ static final PolicySet NO_POLICY_SET =
52            new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, false);
53
54    /**
55     * This projection on Account is for scanning/reading
56     */
57    private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] {
58        AccountColumns.ID, AccountColumns.SECURITY_FLAGS
59    };
60    private static final int ACCOUNT_SECURITY_COLUMN_ID = 0;
61    private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1;
62
63    // Messages used for DevicePolicyManager callbacks
64    private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1;
65    private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2;
66    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3;
67    private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4;
68
69    /**
70     * Get the security policy instance
71     */
72    public synchronized static SecurityPolicy getInstance(Context context) {
73        if (sInstance == null) {
74            sInstance = new SecurityPolicy(context.getApplicationContext());
75        }
76        return sInstance;
77    }
78
79    /**
80     * Private constructor (one time only)
81     */
82    private SecurityPolicy(Context context) {
83        mContext = context.getApplicationContext();
84        mDPM = null;
85        mAdminName = new ComponentName(context, PolicyAdmin.class);
86        mAggregatePolicy = null;
87    }
88
89    /**
90     * For testing only: Inject context into already-created instance
91     */
92    /* package */ void setContext(Context context) {
93        mContext = context;
94    }
95
96    /**
97     * Compute the aggregate policy for all accounts that require it, and record it.
98     *
99     * The business logic is as follows:
100     *  min password length         take the max
101     *  password mode               take the max (strongest mode)
102     *  max password fails          take the min
103     *  max screen lock time        take the min
104     *  require remote wipe         take the max (logical or)
105     *  password history            take the max (strongest mode)
106     *  password expiration         take the min (strongest mode)
107     *  password complex chars      take the max (strongest mode)
108     *  encryption                  take the max (logical or)
109     *  encryption (external)       take the max (logical or)
110     *
111     * @return a policy representing the strongest aggregate.  If no policy sets are defined,
112     * a lightweight "nothing required" policy will be returned.  Never null.
113     */
114    /*package*/ PolicySet computeAggregatePolicy() {
115        boolean policiesFound = false;
116
117        int minPasswordLength = Integer.MIN_VALUE;
118        int passwordMode = Integer.MIN_VALUE;
119        int maxPasswordFails = Integer.MAX_VALUE;
120        int maxScreenLockTime = Integer.MAX_VALUE;
121        boolean requireRemoteWipe = false;
122        int passwordHistory = Integer.MIN_VALUE;
123        int passwordExpirationDays = Integer.MAX_VALUE;
124        int passwordComplexChars = Integer.MIN_VALUE;
125        boolean requireEncryption = false;
126        boolean requireEncryptionExternal = false;
127
128        Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
129                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
130        try {
131            while (c.moveToNext()) {
132                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
133                if (flags != 0) {
134                    PolicySet p = new PolicySet(flags);
135                    minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
136                    passwordMode  = Math.max(p.mPasswordMode, passwordMode);
137                    if (p.mMaxPasswordFails > 0) {
138                        maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails);
139                    }
140                    if (p.mMaxScreenLockTime > 0) {
141                        maxScreenLockTime = Math.min(p.mMaxScreenLockTime, maxScreenLockTime);
142                    }
143                    if (p.mPasswordHistory > 0) {
144                        passwordHistory = Math.max(p.mPasswordHistory, passwordHistory);
145                    }
146                    if (p.mPasswordExpirationDays > 0) {
147                        passwordExpirationDays =
148                                Math.min(p.mPasswordExpirationDays, passwordExpirationDays);
149                    }
150                    if (p.mPasswordComplexChars > 0) {
151                        passwordComplexChars = Math.max(p.mPasswordComplexChars,
152                                passwordComplexChars);
153                    }
154                    requireRemoteWipe |= p.mRequireRemoteWipe;
155                    requireEncryption |= p.mRequireEncryption;
156                    requireEncryptionExternal |= p.mRequireEncryptionExternal;
157                    policiesFound = true;
158                }
159            }
160        } finally {
161            c.close();
162        }
163        if (policiesFound) {
164            // final cleanup pass converts any untouched min/max values to zero (not specified)
165            if (minPasswordLength == Integer.MIN_VALUE) minPasswordLength = 0;
166            if (passwordMode == Integer.MIN_VALUE) passwordMode = 0;
167            if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
168            if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
169            if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 0;
170            if (passwordExpirationDays == Integer.MAX_VALUE) passwordExpirationDays = 0;
171            if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0;
172
173            return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
174                    maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory,
175                    passwordComplexChars, requireEncryption, requireEncryptionExternal);
176        } else {
177            return NO_POLICY_SET;
178        }
179    }
180
181    /**
182     * Return updated aggregate policy, from cached value if possible
183     */
184    public synchronized PolicySet getAggregatePolicy() {
185        if (mAggregatePolicy == null) {
186            mAggregatePolicy = computeAggregatePolicy();
187        }
188        return mAggregatePolicy;
189    }
190
191    /**
192     * Get the dpm.  This mainly allows us to make some utility calls without it, for testing.
193     */
194    /* package */ synchronized DevicePolicyManager getDPM() {
195        if (mDPM == null) {
196            mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
197        }
198        return mDPM;
199    }
200
201    /**
202     * API: Report that policies may have been updated due to rewriting values in an Account.
203     * @param accountId the account that has been updated, -1 if unknown/deleted
204     */
205    public synchronized void updatePolicies(long accountId) {
206        mAggregatePolicy = null;
207    }
208
209    /**
210     * API: Report that policies may have been updated *and* the caller vouches that the
211     * change is a reduction in policies.  This forces an immediate change to device state.
212     * Typically used when deleting accounts, although we may use it for server-side policy
213     * rollbacks.
214     */
215    public void reducePolicies() {
216        updatePolicies(-1);
217        setActivePolicies();
218    }
219
220    /**
221     * API: Query if the proposed set of policies are supported on the device.
222     *
223     * @param policies the polices that were requested
224     * @return boolean if supported
225     */
226    public boolean isSupported(PolicySet policies) {
227        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
228        // encryption (which requires low-level systems support).  Other policies are fully
229        // supported by the framework and do not need to be checked.
230        if (policies.mRequireEncryption) {
231            int encryptionStatus = getDPM().getStorageEncryptionStatus();
232            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
233                return false;
234            }
235        }
236        if (policies.mRequireEncryptionExternal) {
237            // At this time, we only support "external encryption" when it is provided by virtue
238            // of emulating the external storage inside an encrypted device.
239            if (!policies.mRequireEncryption) return false;
240            if (Environment.isExternalStorageRemovable()) return false;
241            if (!Environment.isExternalStorageEmulated()) return false;
242        }
243        return true;
244    }
245
246    /**
247     * API: Remove any unsupported policies
248     *
249     * This is used when we have a set of polices that have been requested, but the server
250     * is willing to allow unsupported policies to be considered optional.
251     *
252     * @param policies the polices that were requested
253     * @return the same PolicySet if all are supported;  A replacement PolicySet if any
254     *   unsupported policies were removed
255     */
256    public PolicySet clearUnsupportedPolicies(PolicySet policies) {
257        PolicySet result = policies;
258        // IMPLEMENTATION:  At this time, the only policy which might not be supported is
259        // encryption (which requires low-level systems support).  Other policies are fully
260        // supported by the framework and do not need to be checked.
261        if (policies.mRequireEncryption) {
262            int encryptionStatus = getDPM().getStorageEncryptionStatus();
263            if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {
264                // Make new PolicySet w/o encryption
265                result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode,
266                        policies.mMaxPasswordFails, policies.mMaxScreenLockTime,
267                        policies.mRequireRemoteWipe, policies.mPasswordExpirationDays,
268                        policies.mPasswordHistory, policies.mPasswordComplexChars, false, false);
269            }
270        }
271        // At this time, we only support "external encryption" when it is provided by virtue
272        // of emulating the external storage inside an encrypted device.
273        if (policies.mRequireEncryptionExternal) {
274            if (Environment.isExternalStorageRemovable()
275                    || !Environment.isExternalStorageEmulated()) {
276                // Make new PolicySet w/o encryption
277                result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode,
278                        policies.mMaxPasswordFails, policies.mMaxScreenLockTime,
279                        policies.mRequireRemoteWipe, policies.mPasswordExpirationDays,
280                        policies.mPasswordHistory, policies.mPasswordComplexChars, false, false);
281            }
282        }
283        return result;
284    }
285
286    /**
287     * API: Query used to determine if a given policy is "active" (the device is operating at
288     * the required security level).
289     *
290     * @param policies the policies requested, or null to check aggregate stored policies
291     * @return true if the requested policies are active, false if not.
292     */
293    public boolean isActive(PolicySet policies) {
294        // Since the DPM reports password failures erroneously, we add this workaround that
295        // ensures that our most recent aggregate policy is set before checking whether those
296        // policies are in force
297        setActivePolicies();
298        int reasons = getInactiveReasons(policies);
299        return reasons == 0;
300    }
301
302    /**
303     * Return bits from isActive:  Device Policy Manager has not been activated
304     */
305    public final static int INACTIVE_NEED_ACTIVATION = 1;
306
307    /**
308     * Return bits from isActive:  Some required configuration is not correct (no user action).
309     */
310    public final static int INACTIVE_NEED_CONFIGURATION = 2;
311
312    /**
313     * Return bits from isActive:  Password needs to be set or updated
314     */
315    public final static int INACTIVE_NEED_PASSWORD = 4;
316
317    /**
318     * Return bits from isActive:  Encryption has not be enabled
319     */
320    public final static int INACTIVE_NEED_ENCRYPTION = 8;
321
322    /**
323     * API: Query used to determine if a given policy is "active" (the device is operating at
324     * the required security level).
325     *
326     * This can be used when syncing a specific account, by passing a specific set of policies
327     * for that account.  Or, it can be used at any time to compare the device
328     * state against the aggregate set of device policies stored in all accounts.
329     *
330     * This method is for queries only, and does not trigger any change in device state.
331     *
332     * NOTE:  If there are multiple accounts with password expiration policies, the device
333     * password will be set to expire in the shortest required interval (most secure).  This method
334     * will return 'false' as soon as the password expires - irrespective of which account caused
335     * the expiration.  In other words, all accounts (that require expiration) will run/stop
336     * based on the requirements of the account with the shortest interval.
337     *
338     * @param policies the policies requested, or null to check aggregate stored policies
339     * @return zero if the requested policies are active, non-zero bits indicates that more work
340     * is needed (typically, by the user) before the required security polices are fully active.
341     */
342    public int getInactiveReasons(PolicySet policies) {
343        // select aggregate set if needed
344        if (policies == null) {
345            policies = getAggregatePolicy();
346        }
347        // quick check for the "empty set" of no policies
348        if (policies == NO_POLICY_SET) {
349            return 0;
350        }
351        int reasons = 0;
352        DevicePolicyManager dpm = getDPM();
353        if (isActiveAdmin()) {
354            // check each policy explicitly
355            if (policies.mMinPasswordLength > 0) {
356                if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) {
357                    reasons |= INACTIVE_NEED_PASSWORD;
358                }
359            }
360            if (policies.mPasswordMode > 0) {
361                if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) {
362                    reasons |= INACTIVE_NEED_PASSWORD;
363                }
364                if (!dpm.isActivePasswordSufficient()) {
365                    reasons |= INACTIVE_NEED_PASSWORD;
366                }
367            }
368            if (policies.mMaxScreenLockTime > 0) {
369                // Note, we use seconds, dpm uses milliseconds
370                if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) {
371                    reasons |= INACTIVE_NEED_CONFIGURATION;
372                }
373            }
374            if (policies.mPasswordExpirationDays > 0) {
375                // confirm that expirations are currently set
376                long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName);
377                if (currentTimeout == 0
378                        || currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) {
379                    reasons |= INACTIVE_NEED_PASSWORD;
380                }
381                // confirm that the current password hasn't expired
382                long expirationDate = dpm.getPasswordExpiration(mAdminName);
383                long timeUntilExpiration = expirationDate - System.currentTimeMillis();
384                boolean expired = timeUntilExpiration < 0;
385                if (expired) {
386                    reasons |= INACTIVE_NEED_PASSWORD;
387                }
388            }
389            if (policies.mPasswordHistory > 0) {
390                if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) {
391                    reasons |= INACTIVE_NEED_PASSWORD;
392                }
393            }
394            if (policies.mPasswordComplexChars > 0) {
395                if (dpm.getPasswordMinimumNonLetter(mAdminName) < policies.mPasswordComplexChars) {
396                    reasons |= INACTIVE_NEED_PASSWORD;
397                }
398            }
399            if (policies.mRequireEncryption) {
400                int encryptionStatus = getDPM().getStorageEncryptionStatus();
401                if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) {
402                    reasons |= INACTIVE_NEED_ENCRYPTION;
403                }
404            }
405            // TODO: If we ever support external storage encryption as a first-class feature,
406            // it will need to be checked here.  For now, if there is a policy request for
407            // external storage encryption, it's sufficient that we've activated internal
408            // storage encryption.
409
410            // password failures are counted locally - no test required here
411            // no check required for remote wipe (it's supported, if we're the admin)
412
413            // If we made it all the way, reasons == 0 here.  Otherwise it's a list of grievances.
414            return reasons;
415        }
416        // return false, not active
417        return INACTIVE_NEED_ACTIVATION;
418    }
419
420    /**
421     * Set the requested security level based on the aggregate set of requests.
422     * If the set is empty, we release our device administration.  If the set is non-empty,
423     * we only proceed if we are already active as an admin.
424     */
425    public void setActivePolicies() {
426        DevicePolicyManager dpm = getDPM();
427        // compute aggregate set of policies
428        PolicySet policies = getAggregatePolicy();
429        // if empty set, detach from policy manager
430        if (policies == NO_POLICY_SET) {
431            dpm.removeActiveAdmin(mAdminName);
432        } else if (isActiveAdmin()) {
433            // set each policy in the policy manager
434            // password mode & length
435            dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality());
436            dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength);
437            // screen lock time
438            dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
439            // local wipe (failed passwords limit)
440            dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
441            // password expiration (days until a password expires).  API takes mSec.
442            dpm.setPasswordExpirationTimeout(mAdminName,
443                    policies.getDPManagerPasswordExpirationTimeout());
444            // password history length (number of previous passwords that may not be reused)
445            dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory);
446            // password minimum complex characters.
447            // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM,
448            // setting the quality to complex also defaults min symbols=1 and min numeric=1.
449            // We always / safely clear minSymbols & minNumeric to zero (there is no policy
450            // configuration in which we explicitly require a minimum number of digits or symbols.)
451            dpm.setPasswordMinimumSymbols(mAdminName, 0);
452            dpm.setPasswordMinimumNumeric(mAdminName, 0);
453            dpm.setPasswordMinimumNonLetter(mAdminName, policies.mPasswordComplexChars);
454            // encryption required
455            dpm.setStorageEncryption(mAdminName, policies.mRequireEncryption);
456            // TODO: If we ever support external storage encryption as a first-class feature,
457            // it will need to be set here.  For now, if there is a policy request for
458            // external storage encryption, it's sufficient that we've activated internal
459            // storage encryption.
460        }
461    }
462
463    /**
464     * Convenience method; see javadoc below
465     */
466    public static void setAccountHoldFlag(Context context, long accountId, boolean newState) {
467        Account account = Account.restoreAccountWithId(context, accountId);
468        if (account != null) {
469            setAccountHoldFlag(context, account, newState);
470        }
471    }
472
473    /**
474     * API: Set/Clear the "hold" flag in any account.  This flag serves a dual purpose:
475     * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
476     * signal to try syncing again.
477     * @param context
478     * @param account the account whose hold flag is to be set/cleared
479     * @param newState true = security hold, false = free to sync
480     */
481    public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
482        if (newState) {
483            account.mFlags |= Account.FLAGS_SECURITY_HOLD;
484        } else {
485            account.mFlags &= ~Account.FLAGS_SECURITY_HOLD;
486        }
487        ContentValues cv = new ContentValues();
488        cv.put(AccountColumns.FLAGS, account.mFlags);
489        account.update(context, cv);
490    }
491
492    /**
493     * API: Sync service should call this any time a sync fails due to isActive() returning false.
494     * This will kick off the notify-acquire-admin-state process and/or increase the security level.
495     * The caller needs to write the required policies into this account before making this call.
496     * Should not be called from UI thread - uses DB lookups to prepare new notifications
497     *
498     * @param accountId the account for which sync cannot proceed
499     */
500    public void policiesRequired(long accountId) {
501        Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
502        // In case the account has been deleted, just return
503        if (account == null) return;
504
505        // Mark the account as "on hold".
506        setAccountHoldFlag(mContext, account, true);
507
508        // Put up a notification
509        String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
510                account.getDisplayName());
511        String contentTitle = mContext.getString(R.string.security_notification_content_title);
512        String contentText = account.getDisplayName();
513        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId, true);
514        NotificationController.getInstance(mContext).postAccountNotification(
515                account, tickerText, contentTitle, contentText, intent,
516                NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
517    }
518
519    /**
520     * Called from the notification's intent receiver to register that the notification can be
521     * cleared now.
522     */
523    public void clearNotification(long accountId) {
524        NotificationController.getInstance(mContext).cancelNotification(
525                NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
526    }
527
528    /**
529     * API: Remote wipe (from server).  This is final, there is no confirmation.  It will only
530     * return to the caller if there is an unexpected failure.
531     */
532    public void remoteWipe() {
533        DevicePolicyManager dpm = getDPM();
534        if (dpm.isAdminActive(mAdminName)) {
535            dpm.wipeData(0);
536        } else {
537            Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin.");
538        }
539    }
540    /**
541     * If we are not the active device admin, try to become so.
542     *
543     * Also checks for any policies that we have added during the lifetime of this app.
544     * This catches the case where the user granted an earlier (smaller) set of policies
545     * but an app upgrade requires that new policies be granted.
546     *
547     * @return true if we are already active, false if we are not
548     */
549    public boolean isActiveAdmin() {
550        DevicePolicyManager dpm = getDPM();
551        return dpm.isAdminActive(mAdminName)
552                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD)
553                && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE);
554    }
555
556    /**
557     * Report admin component name - for making calls into device policy manager
558     */
559    public ComponentName getAdminComponent() {
560        return mAdminName;
561    }
562
563    /**
564     * Delete all accounts whose security flags aren't zero (i.e. they have security enabled).
565     * This method is synchronous, so it should normally be called within a worker thread (the
566     * exception being for unit tests)
567     *
568     * @param context the caller's context
569     */
570    /*package*/ void deleteSecuredAccounts(Context context) {
571        ContentResolver cr = context.getContentResolver();
572        // Find all accounts with security and delete them
573        Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
574                AccountColumns.SECURITY_FLAGS + "!=0", null, null);
575        try {
576            Log.w(TAG, "Email administration disabled; deleting " + c.getCount() +
577                    " secured account(s)");
578            while (c.moveToNext()) {
579                Controller.getInstance(context).deleteAccountSync(
580                        c.getLong(EmailContent.ID_PROJECTION_COLUMN), context);
581            }
582        } finally {
583            c.close();
584        }
585        updatePolicies(-1);
586    }
587
588    /**
589     * Internal handler for enabled->disabled transitions.  Deletes all secured accounts.
590     * Must call from worker thread, not on UI thread.
591     */
592    /*package*/ void onAdminEnabled(boolean isEnabled) {
593        if (!isEnabled) {
594            deleteSecuredAccounts(mContext);
595        }
596    }
597
598    /**
599     * Handle password expiration - if any accounts appear to have triggered this, put up
600     * warnings, or even shut them down.
601     *
602     * NOTE:  If there are multiple accounts with password expiration policies, the device
603     * password will be set to expire in the shortest required interval (most secure).  The logic
604     * in this method operates based on the aggregate setting - irrespective of which account caused
605     * the expiration.  In other words, all accounts (that require expiration) will run/stop
606     * based on the requirements of the account with the shortest interval.
607     */
608    private void onPasswordExpiring(Context context) {
609        // 1.  Do we have any accounts that matter here?
610        long nextExpiringAccountId = findShortestExpiration(context);
611
612        // 2.  If not, exit immediately
613        if (nextExpiringAccountId == -1) {
614            return;
615        }
616
617        // 3.  If yes, are we warning or expired?
618        long expirationDate = getDPM().getPasswordExpiration(mAdminName);
619        long timeUntilExpiration = expirationDate - System.currentTimeMillis();
620        boolean expired = timeUntilExpiration < 0;
621        if (!expired) {
622            // 4.  If warning, simply put up a generic notification and report that it came from
623            // the shortest-expiring account.
624            Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
625            if (account == null) return;
626            Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(context,
627                    nextExpiringAccountId, false);
628            String ticker = context.getString(
629                    R.string.password_expire_warning_ticker_fmt, account.getDisplayName());
630            String contentTitle = context.getString(
631                    R.string.password_expire_warning_content_title);
632            String contentText = account.getDisplayName();
633            NotificationController nc = NotificationController.getInstance(mContext);
634            nc.postAccountNotification(account, ticker, contentTitle, contentText, intent,
635                    NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
636        } else {
637            // 5.  Actually expired - find all accounts that expire passwords, and wipe them
638            boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context));
639            if (wiped) {
640                // Post notification
641                Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
642                if (account == null) return;
643                Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(context,
644                        nextExpiringAccountId, true);
645                String ticker = context.getString(R.string.password_expired_ticker);
646                String contentTitle = context.getString(R.string.password_expired_content_title);
647                String contentText = account.getDisplayName();
648                NotificationController nc = NotificationController.getInstance(mContext);
649                nc.postAccountNotification(account, ticker, contentTitle,
650                        contentText, intent,
651                        NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
652            }
653        }
654    }
655
656    /**
657     * Find the account with the shortest expiration time.  This is always assumed to be
658     * the account that forces the password to be refreshed.
659     * @return -1 if no expirations, or accountId if one is found
660     */
661    /* package */ static long findShortestExpiration(Context context) {
662        long nextExpiringAccountId = -1;
663        long shortestExpiration = Long.MAX_VALUE;
664        Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
665                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
666        try {
667            while (c.moveToNext()) {
668                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
669                if (flags != 0) {
670                    PolicySet p = new PolicySet(flags);
671                    if (p.mPasswordExpirationDays > 0 &&
672                            p.mPasswordExpirationDays < shortestExpiration) {
673                        nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
674                        shortestExpiration = p.mPasswordExpirationDays;
675                    }
676                }
677            }
678        } finally {
679            c.close();
680        }
681        return nextExpiringAccountId;
682    }
683
684    /**
685     * For all accounts that require password expiration, put them in security hold and wipe
686     * their data.
687     * @param context
688     * @param controller
689     * @return true if one or more accounts were wiped
690     */
691    /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) {
692        boolean result = false;
693        Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
694                ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
695        try {
696            while (c.moveToNext()) {
697                long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
698                if (flags != 0) {
699                    PolicySet p = new PolicySet(flags);
700                    if (p.mPasswordExpirationDays > 0) {
701                        long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
702                        Account account = Account.restoreAccountWithId(context, accountId);
703                        if (account != null) {
704                            // Mark the account as "on hold".
705                            setAccountHoldFlag(context, account, true);
706                            // Erase data
707                            controller.deleteSyncedDataSync(accountId);
708                            // Report one or more were found
709                            result = true;
710                        }
711                    }
712                }
713            }
714        } finally {
715            c.close();
716        }
717        return result;
718    }
719
720    /**
721     * Callback from EmailBroadcastProcessorService.  This provides the workers for the
722     * DeviceAdminReceiver calls.  These should perform the work directly and not use async
723     * threads for completion.
724     */
725    public static void onDeviceAdminReceiverMessage(Context context, int message) {
726        SecurityPolicy instance = SecurityPolicy.getInstance(context);
727        switch (message) {
728            case DEVICE_ADMIN_MESSAGE_ENABLED:
729                instance.onAdminEnabled(true);
730                break;
731            case DEVICE_ADMIN_MESSAGE_DISABLED:
732                instance.onAdminEnabled(false);
733                break;
734            case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED:
735                // TODO make a small helper for this
736                // Clear security holds (if any)
737                Account.clearSecurityHoldOnAllAccounts(context);
738                // Cancel any active notifications (if any are posted)
739                NotificationController nc = NotificationController.getInstance(context);
740                nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
741                nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
742                break;
743            case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING:
744                instance.onPasswordExpiring(instance.mContext);
745                break;
746        }
747    }
748
749    /**
750     * Device Policy administrator.  This is primarily a listener for device state changes.
751     * Note:  This is instantiated by incoming messages.
752     * Note:  This is actually a BroadcastReceiver and must remain within the guidelines required
753     *        for proper behavior, including avoidance of ANRs.
754     * Note:  We do not implement onPasswordFailed() because the default behavior of the
755     *        DevicePolicyManager - complete local wipe after 'n' failures - is sufficient.
756     */
757    public static class PolicyAdmin extends DeviceAdminReceiver {
758
759        /**
760         * Called after the administrator is first enabled.
761         */
762        @Override
763        public void onEnabled(Context context, Intent intent) {
764            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
765                    DEVICE_ADMIN_MESSAGE_ENABLED);
766        }
767
768        /**
769         * Called prior to the administrator being disabled.
770         */
771        @Override
772        public void onDisabled(Context context, Intent intent) {
773            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
774                    DEVICE_ADMIN_MESSAGE_DISABLED);
775        }
776
777        /**
778         * Called when the user asks to disable administration; we return a warning string that
779         * will be presented to the user
780         */
781        @Override
782        public CharSequence onDisableRequested(Context context, Intent intent) {
783            return context.getString(R.string.disable_admin_warning);
784        }
785
786        /**
787         * Called after the user has changed their password.
788         */
789        @Override
790        public void onPasswordChanged(Context context, Intent intent) {
791            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
792                    DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED);
793        }
794
795        /**
796         * Called when device password is expiring
797         */
798        @Override
799        public void onPasswordExpiring(Context context, Intent intent) {
800            EmailBroadcastProcessorService.processDevicePolicyMessage(context,
801                    DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING);
802        }
803    }
804}
805