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