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