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