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