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