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