Persist policies set through the device policy engine

Bug: 232918480
Test: manual
Change-Id: Ia7eb2e356ae470e02b669f6eb7afd8004a4c725b
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a4f4f65..9fd990f 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3920,6 +3920,11 @@
      */
     public static final String AUTO_TIMEZONE_POLICY = "autoTimezone";
 
+    /**
+     * @hide
+     */
+    public static final String PERMISSION_GRANT_POLICY_KEY = "permissionGrant";
+
     // TODO: Expose this as SystemAPI once we add the query API
     /**
      * @hide
@@ -3928,7 +3933,7 @@
             @NonNull String packageName, @NonNull String permission) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(permission);
-        return "permissionGrant_" + packageName + "_" + permission;
+        return PERMISSION_GRANT_POLICY_KEY + "_" + packageName + "_" + permission;
     }
 
     /**
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
new file mode 100644
index 0000000..8a8485a
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+final class BooleanPolicySerializer extends PolicySerializer<Boolean> {
+
+    @Override
+    void saveToXml(TypedXmlSerializer serializer, String attributeName, Boolean value)
+            throws IOException {
+        serializer.attributeBoolean(/* namespace= */ null, attributeName, value);
+    }
+
+    @Override
+    Boolean readFromXml(TypedXmlPullParser parser, String attributeName)
+            throws XmlPullParserException {
+        return parser.getAttributeBoolean(/* namespace= */ null, attributeName);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 47db0f9..15c8c27 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -19,9 +19,25 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.os.Environment;
 import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.Log;
 import android.util.SparseArray;
+import android.util.Xml;
 
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -31,9 +47,10 @@
  * admins on the device.
  */
 final class DevicePolicyEngine {
-    private static final String TAG = "DevicePolicyEngine";
+    static final String TAG = "DevicePolicyEngine";
 
     private final Context mContext;
+    // TODO(b/256849338): add more granular locks
     private final Object mLock = new Object();
 
     /**
@@ -133,6 +150,7 @@
             if (policyChanged) {
                 enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), userId);
             }
+            write();
             return policyChanged;
         }
     }
@@ -159,6 +177,7 @@
                 enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(),
                         UserHandle.USER_ALL);
             }
+            write();
             return policyChanged;
         }
     }
@@ -193,15 +212,15 @@
 
         if (policyDefinition.isGlobalOnlyPolicy()) {
             throw new IllegalArgumentException("Can't set global policy "
-                    + policyDefinition.getKey() + " locally.");
+                    + policyDefinition.getPolicyKey() + " locally.");
         }
 
         if (!mUserPolicies.contains(userId)) {
             mUserPolicies.put(userId, new HashMap<>());
         }
-        if (!mUserPolicies.get(userId).containsKey(policyDefinition.getKey())) {
+        if (!mUserPolicies.get(userId).containsKey(policyDefinition.getPolicyKey())) {
             mUserPolicies.get(userId).put(
-                    policyDefinition.getKey(), new PolicyState<>(policyDefinition));
+                    policyDefinition.getPolicyKey(), new PolicyState<>(policyDefinition));
         }
         return getPolicyState(mUserPolicies.get(userId), policyDefinition);
     }
@@ -211,12 +230,12 @@
 
         if (policyDefinition.isLocalOnlyPolicy()) {
             throw new IllegalArgumentException("Can't set local policy "
-                    + policyDefinition.getKey() + " globally.");
+                    + policyDefinition.getPolicyKey() + " globally.");
         }
 
-        if (!mGlobalPolicies.containsKey(policyDefinition.getKey())) {
+        if (!mGlobalPolicies.containsKey(policyDefinition.getPolicyKey())) {
             mGlobalPolicies.put(
-                    policyDefinition.getKey(), new PolicyState<>(policyDefinition));
+                    policyDefinition.getPolicyKey(), new PolicyState<>(policyDefinition));
         }
         return getPolicyState(mGlobalPolicies, policyDefinition);
     }
@@ -228,7 +247,7 @@
             // we've created two policies with the same key but different types - we can only have
             // stored a PolicyState of the right type.
             PolicyState<V> policyState = (PolicyState<V>) policies.get(
-                    policyDefinition.getKey());
+                    policyDefinition.getPolicyKey());
             return policyState;
         } catch (ClassCastException exception) {
             // TODO: handle exception properly
@@ -245,4 +264,198 @@
         // TODO: notify calling admin of result (e.g. success, runtime failure, policy set by
         //  a different admin)
     }
+
+    private void write() {
+        Log.d(TAG, "Writing device policies to file.");
+        new DevicePoliciesReaderWriter().writeToFileLocked();
+    }
+
+    // TODO(b/256852787): trigger resolving logic after loading policies as roles are recalculated
+    //  and could result in a different enforced policy
+    void load() {
+        Log.d(TAG, "Reading device policies from file.");
+        synchronized (mLock) {
+            clear();
+            new DevicePoliciesReaderWriter().readFromFileLocked();
+        }
+    }
+
+    private void clear() {
+        synchronized (mLock) {
+            mGlobalPolicies.clear();
+            mUserPolicies.clear();
+        }
+    }
+
+    private class DevicePoliciesReaderWriter {
+        private static final String DEVICE_POLICIES_XML = "device_policies.xml";
+        private static final String TAG_USER_POLICY_ENTRY = "user-policy-entry";
+        private static final String TAG_DEVICE_POLICY_ENTRY = "device-policy-entry";
+        private static final String TAG_ADMINS_POLICY_ENTRY = "admins-policy-entry";
+        private static final String ATTR_USER_ID = "user-id";
+        private static final String ATTR_POLICY_ID = "policy-id";
+
+        private final File mFile;
+
+        private DevicePoliciesReaderWriter() {
+            mFile = new File(Environment.getDataSystemDirectory(), DEVICE_POLICIES_XML);
+        }
+
+        void writeToFileLocked() {
+            Log.d(TAG, "Writing to " + mFile);
+
+            AtomicFile f = new AtomicFile(mFile);
+            FileOutputStream outputStream = null;
+            try {
+                outputStream = f.startWrite();
+                TypedXmlSerializer out = Xml.resolveSerializer(outputStream);
+
+                out.startDocument(null, true);
+
+                // Actual content
+                writeInner(out);
+
+                out.endDocument();
+                out.flush();
+
+                // Commit the content.
+                f.finishWrite(outputStream);
+                outputStream = null;
+
+            } catch (IOException e) {
+                Log.e(TAG, "Exception when writing", e);
+                if (outputStream != null) {
+                    f.failWrite(outputStream);
+                }
+            }
+        }
+
+        // TODO(b/256846294): Add versioning to read/write
+        void writeInner(TypedXmlSerializer serializer) throws IOException {
+            writeUserPoliciesInner(serializer);
+            writeDevicePoliciesInner(serializer);
+        }
+
+        private void writeUserPoliciesInner(TypedXmlSerializer serializer) throws IOException {
+            if (mUserPolicies != null) {
+                for (int i = 0; i < mUserPolicies.size(); i++) {
+                    int userId = mUserPolicies.keyAt(i);
+                    for (Map.Entry<String, PolicyState<?>> policy : mUserPolicies.get(
+                            userId).entrySet()) {
+                        serializer.startTag(/* namespace= */ null, TAG_USER_POLICY_ENTRY);
+
+                        serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, userId);
+                        serializer.attribute(
+                                /* namespace= */ null, ATTR_POLICY_ID, policy.getKey());
+
+                        serializer.startTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
+                        policy.getValue().saveToXml(serializer);
+                        serializer.endTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
+
+                        serializer.endTag(/* namespace= */ null, TAG_USER_POLICY_ENTRY);
+                    }
+                }
+            }
+        }
+
+        private void writeDevicePoliciesInner(TypedXmlSerializer serializer) throws IOException {
+            if (mGlobalPolicies != null) {
+                for (Map.Entry<String, PolicyState<?>> policy : mGlobalPolicies.entrySet()) {
+                    serializer.startTag(/* namespace= */ null, TAG_DEVICE_POLICY_ENTRY);
+
+                    serializer.attribute(/* namespace= */ null, ATTR_POLICY_ID, policy.getKey());
+
+                    serializer.startTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
+                    policy.getValue().saveToXml(serializer);
+                    serializer.endTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
+
+                    serializer.endTag(/* namespace= */ null, TAG_DEVICE_POLICY_ENTRY);
+                }
+            }
+        }
+
+        void readFromFileLocked() {
+            if (!mFile.exists()) {
+                Log.d(TAG, "" + mFile + " doesn't exist");
+                return;
+            }
+
+            Log.d(TAG, "Reading from " + mFile);
+            AtomicFile f = new AtomicFile(mFile);
+            InputStream input = null;
+            try {
+                input = f.openRead();
+                TypedXmlPullParser parser = Xml.resolvePullParser(input);
+
+                readInner(parser);
+
+            } catch (XmlPullParserException | IOException | ClassNotFoundException e) {
+                Log.e(TAG, "Error parsing resources file", e);
+            } finally {
+                IoUtils.closeQuietly(input);
+            }
+        }
+
+        private void readInner(TypedXmlPullParser parser)
+                throws IOException, XmlPullParserException, ClassNotFoundException {
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                String tag = parser.getName();
+                switch (tag) {
+                    case TAG_USER_POLICY_ENTRY:
+                        readUserPoliciesInner(parser);
+                        break;
+                    case TAG_DEVICE_POLICY_ENTRY:
+                        readDevicePoliciesInner(parser);
+                        break;
+                    default:
+                        Log.e(TAG, "Unknown tag " + tag);
+                }
+            }
+        }
+
+        private void readUserPoliciesInner(TypedXmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
+            String policyKey = parser.getAttributeValue(
+                    /* namespace= */ null, ATTR_POLICY_ID);
+            if (!mUserPolicies.contains(userId)) {
+                mUserPolicies.put(userId, new HashMap<>());
+            }
+            PolicyState<?> adminsPolicy = parseAdminsPolicy(parser);
+            if (adminsPolicy != null) {
+                mUserPolicies.get(userId).put(policyKey, adminsPolicy);
+            } else {
+                Log.e(TAG, "Error parsing file, " + policyKey + "doesn't have an "
+                        + "AdminsPolicy.");
+            }
+        }
+
+        private void readDevicePoliciesInner(TypedXmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_ID);
+            PolicyState<?> adminsPolicy = parseAdminsPolicy(parser);
+            if (adminsPolicy != null) {
+                mGlobalPolicies.put(policyKey, adminsPolicy);
+            } else {
+                Log.e(TAG, "Error parsing file, " + policyKey + "doesn't have an "
+                        + "AdminsPolicy.");
+            }
+        }
+
+        @Nullable
+        private PolicyState<?> parseAdminsPolicy(TypedXmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                String tag = parser.getName();
+                if (tag.equals(TAG_ADMINS_POLICY_ENTRY)) {
+                    return PolicyState.readFromXml(parser);
+                }
+                Log.e(TAG, "Unknown tag " + tag);
+            }
+            Log.e(TAG, "Error parsing file, AdminsPolicy not found");
+            return null;
+        }
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
index e6ef1af..f4953b4 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
@@ -20,51 +20,91 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.role.RoleManagerLocal;
 import com.android.server.LocalManagerRegistry;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
+/**
+ * {@code EnforcingAdmins} can have the following authority types:
+ *
+ * <ul>
+ *     <li> {@link #DPC_AUTHORITY} meaning it's an enterprise admin (e.g. PO, DO, COPE)
+ *     <li> {@link #DEVICE_ADMIN_AUTHORITY} which is a legacy non enterprise admin
+ *     <li> Or a role authority, in which case {@link #mAuthorities} contains a list of all roles
+ *     held by the given {@code packageName}
+ * </ul>
+ *
+ */
 final class EnforcingAdmin {
     static final String ROLE_AUTHORITY_PREFIX = "role:";
     static final String DPC_AUTHORITY = "enterprise";
     static final String DEVICE_ADMIN_AUTHORITY = "device_admin";
     static final String DEFAULT_AUTHORITY = "default";
 
+    private static final String ATTR_PACKAGE_NAME = "package-name";
+    private static final String ATTR_CLASS_NAME = "class-name";
+    private static final String ATTR_AUTHORITIES = "authorities";
+    private static final String ATTR_AUTHORITIES_SEPARATOR = ";";
+    private static final String ATTR_USER_ID = "user-id";
+    private static final String ATTR_IS_ROLE = "is-role";
+
     private final String mPackageName;
-    // This is needed for DPCs and device admins
-    @Nullable private final ComponentName mComponentName;
-    // TODO: implement lazy loading for authorities
-    private final Set<String> mAuthorities;
+    // This is needed for DPCs and active admins
+    private final ComponentName mComponentName;
+    private Set<String> mAuthorities;
+    private final int mUserId;
+    private final boolean mIsRoleAuthority;
 
     static EnforcingAdmin createEnforcingAdmin(@NonNull String packageName, int userId) {
         Objects.requireNonNull(packageName);
-        return new EnforcingAdmin(packageName, getRoleAuthoritiesOrDefault(packageName, userId));
+        return new EnforcingAdmin(packageName, userId);
     }
 
-    static EnforcingAdmin createEnterpriseEnforcingAdmin(ComponentName componentName) {
+    static EnforcingAdmin createEnterpriseEnforcingAdmin(@NonNull ComponentName componentName) {
         Objects.requireNonNull(componentName);
-        return new EnforcingAdmin(componentName, Set.of(DPC_AUTHORITY));
+        return new EnforcingAdmin(
+                componentName.getPackageName(), componentName, Set.of(DPC_AUTHORITY));
     }
 
     static EnforcingAdmin createDeviceAdminEnforcingAdmin(ComponentName componentName) {
         Objects.requireNonNull(componentName);
-        return new EnforcingAdmin(componentName, Set.of(DEVICE_ADMIN_AUTHORITY));
+        return new EnforcingAdmin(
+                componentName.getPackageName(), componentName, Set.of(DEVICE_ADMIN_AUTHORITY));
     }
 
-    private EnforcingAdmin(String packageName, Set<String> authorities) {
+    private EnforcingAdmin(
+            String packageName, ComponentName componentName, Set<String> authorities) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(componentName);
+        Objects.requireNonNull(authorities);
+
+        // Role authorities should not be using this constructor
+        mIsRoleAuthority = false;
         mPackageName = packageName;
-        mComponentName = null;
-        mAuthorities = new HashSet<>(authorities);
-    }
-
-    private EnforcingAdmin(ComponentName componentName, Set<String> authorities) {
-        mPackageName = componentName.getPackageName();
         mComponentName = componentName;
         mAuthorities = new HashSet<>(authorities);
+        mUserId = -1; // not needed for non role authorities
+    }
+
+    private EnforcingAdmin(String packageName, int userId) {
+        Objects.requireNonNull(packageName);
+
+        // Only role authorities use this constructor.
+        mIsRoleAuthority = true;
+        mPackageName = packageName;
+        mUserId = userId;
+        mComponentName = null;
+        // authorities will be loaded when needed
+        mAuthorities = null;
     }
 
     private static Set<String> getRoleAuthoritiesOrDefault(String packageName, int userId) {
@@ -90,8 +130,15 @@
         return roles;
     }
 
+    private Set<String> getAuthorities() {
+        if (mAuthorities == null) {
+            mAuthorities = getRoleAuthoritiesOrDefault(mPackageName, mUserId);
+        }
+        return mAuthorities;
+    }
+
     boolean hasAuthority(String authority) {
-        return mAuthorities.contains(authority);
+        return getAuthorities().contains(authority);
     }
 
     /**
@@ -113,24 +160,56 @@
         EnforcingAdmin other = (EnforcingAdmin) o;
         return Objects.equals(mPackageName, other.mPackageName)
                 && Objects.equals(mComponentName, other.mComponentName)
+                && Objects.equals(mIsRoleAuthority, other.mIsRoleAuthority)
                 && hasMatchingAuthorities(this, other);
     }
 
     private static boolean hasMatchingAuthorities(EnforcingAdmin admin1, EnforcingAdmin admin2) {
-        if (admin1.mAuthorities.equals(admin2.mAuthorities)) {
+        if (admin1.mIsRoleAuthority && admin2.mIsRoleAuthority) {
             return true;
         }
-        return !admin1.hasAuthority(DPC_AUTHORITY) && !admin1.hasAuthority(DEVICE_ADMIN_AUTHORITY)
-                && !admin2.hasAuthority(DPC_AUTHORITY) && !admin2.hasAuthority(
-                DEVICE_ADMIN_AUTHORITY);
+        return admin1.getAuthorities().equals(admin2.getAuthorities());
     }
 
     @Override
     public int hashCode() {
-        if (mAuthorities.contains(DPC_AUTHORITY) || mAuthorities.contains(DEVICE_ADMIN_AUTHORITY)) {
-            return Objects.hash(mComponentName, mAuthorities);
-        } else {
+        if (mIsRoleAuthority) {
+            // TODO(b/256854977): should we add UserId?
             return Objects.hash(mPackageName);
+        } else {
+            return Objects.hash(mComponentName, getAuthorities());
+        }
+    }
+
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName);
+        serializer.attributeBoolean(/* namespace= */ null, ATTR_IS_ROLE, mIsRoleAuthority);
+        if (mIsRoleAuthority) {
+            serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, mUserId);
+        } else {
+            serializer.attribute(
+                    /* namespace= */ null, ATTR_CLASS_NAME, mComponentName.getClassName());
+            serializer.attribute(
+                    /* namespace= */ null,
+                    ATTR_AUTHORITIES,
+                    String.join(ATTR_AUTHORITIES_SEPARATOR, getAuthorities()));
+        }
+    }
+
+    static EnforcingAdmin readFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException {
+        String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME);
+        boolean isRoleAuthority = parser.getAttributeBoolean(/* namespace= */ null, ATTR_IS_ROLE);
+        String authoritiesStr = parser.getAttributeValue(/* namespace= */ null, ATTR_AUTHORITIES);
+
+        if (isRoleAuthority) {
+            int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
+            return new EnforcingAdmin(packageName, userId);
+        } else {
+            String className = parser.getAttributeValue(/* namespace= */ null, ATTR_CLASS_NAME);
+            ComponentName componentName = new ComponentName(packageName, className);
+            Set<String> authorities = Set.of(authoritiesStr.split(ATTR_AUTHORITIES_SEPARATOR));
+            return new EnforcingAdmin(packageName, componentName, authorities);
         }
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
new file mode 100644
index 0000000..3152f0b
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+final class IntegerPolicySerializer extends PolicySerializer<Integer> {
+
+    @Override
+    void saveToXml(TypedXmlSerializer serializer, String attributeName, Integer value)
+            throws IOException {
+        serializer.attributeInt(/* namespace= */ null, attributeName, value);
+    }
+
+    @Override
+    Integer readFromXml(TypedXmlPullParser parser, String attributeName)
+            throws XmlPullParserException {
+        return parser.getAttributeInt(/* namespace= */ null, attributeName);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 2a56f2c..b22477b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -22,12 +22,19 @@
 import android.content.Context;
 
 import com.android.internal.util.function.QuadFunction;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 final class PolicyDefinition<V> {
     private static final int POLICY_FLAG_NONE = 0;
+
     // Only use this flag if a policy can not be applied locally.
     private static final int POLICY_FLAG_GLOBAL_ONLY_POLICY = 1;
 
@@ -37,39 +44,66 @@
     private static final MostRestrictive<Boolean> FALSE_MORE_RESTRICTIVE = new MostRestrictive<>(
             List.of(false, true));
 
+    private static final String ATTR_POLICY_KEY = "policy-key";
+    private static final String ATTR_POLICY_DEFINITION_KEY = "policy-type-key";
+    private static final String ATTR_CALLBACK_ARGS = "callback-args";
+    private static final String ATTR_CALLBACK_ARGS_SEPARATOR = ";";
+
+
     static PolicyDefinition<Boolean> AUTO_TIMEZONE = new PolicyDefinition<>(
             DevicePolicyManager.AUTO_TIMEZONE_POLICY,
             // auto timezone is enabled by default, hence disabling it is more restrictive.
             FALSE_MORE_RESTRICTIVE,
             POLICY_FLAG_GLOBAL_ONLY_POLICY,
             (Boolean value, Context context, Integer userId, String[] args) ->
-                    PolicyEnforcerCallbacks.setAutoTimezoneEnabled(value, context));
+                    PolicyEnforcerCallbacks.setAutoTimezoneEnabled(value, context),
+            new BooleanPolicySerializer());
+
+    // This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
+    // actual permission grant policy with the correct arguments (packageName and permission name)
+    // when reading the policies from xml.
+    private static final PolicyDefinition<Integer> PERMISSION_GRANT_NO_ARGS =
+            new PolicyDefinition<>(DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY,
+                    // TODO: is this really the best mechanism, what makes denied more
+                    //  restrictive than
+                    //  granted?
+                    new MostRestrictive<>(
+                            List.of(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED,
+                                    DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED,
+                                    DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT)),
+                    POLICY_FLAG_LOCAL_ONLY_POLICY,
+                    PolicyEnforcerCallbacks::setPermissionGrantState,
+                    new IntegerPolicySerializer());
 
     static PolicyDefinition<Integer> PERMISSION_GRANT(
             @NonNull String packageName, @NonNull String permission) {
-        return new PolicyDefinition<>(
+        return PERMISSION_GRANT_NO_ARGS.setArgs(
                 DevicePolicyManager.PERMISSION_GRANT_POLICY(packageName, permission),
-                // TODO: is this really the best mechanism, what makes denied more restrictive than
-                //  granted?
-                new MostRestrictive<>(
-                        List.of(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED,
-                                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED,
-                                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT)),
-                POLICY_FLAG_LOCAL_ONLY_POLICY,
-                PolicyEnforcerCallbacks::setPermissionGrantState,
                 new String[]{packageName, permission});
     }
 
-    private final String mKey;
+    private static Map<String, PolicyDefinition<?>> sPolicyDefinitions = Map.of(
+            DevicePolicyManager.AUTO_TIMEZONE_POLICY, AUTO_TIMEZONE,
+            DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY, PERMISSION_GRANT_NO_ARGS
+    );
+
+    private final String mPolicyKey;
+    private final String mPolicyDefinitionKey;
     private final ResolutionMechanism<V> mResolutionMechanism;
     private final int mPolicyFlags;
     // A function that accepts  policy to apple, context, userId, callback arguments, and returns
     // true if the policy has been enforced successfully.
     private final QuadFunction<V, Context, Integer, String[], Boolean> mPolicyEnforcerCallback;
     private final String[] mCallbackArgs;
+    private final PolicySerializer<V> mPolicySerializer;
 
-    String getKey() {
-        return mKey;
+    private PolicyDefinition<V> setArgs(String key, String[] callbackArgs) {
+        return new PolicyDefinition<>(key, mPolicyDefinitionKey, mResolutionMechanism,
+                mPolicyFlags, mPolicyEnforcerCallback, mPolicySerializer, callbackArgs);
+    }
+
+    String getPolicyKey() {
+        return mPolicyKey;
     }
 
     /**
@@ -97,55 +131,90 @@
 
     /**
      * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals)} implementation.
+     * {@link Object#equals} implementation.
      */
     private PolicyDefinition(
             String key,
             ResolutionMechanism<V> resolutionMechanism,
             QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback,
-            String[] callbackArgs) {
-        this(key, resolutionMechanism, POLICY_FLAG_NONE, policyEnforcerCallback,
-                callbackArgs);
+            PolicySerializer<V> policySerializer) {
+        this(key, resolutionMechanism, POLICY_FLAG_NONE, policyEnforcerCallback, policySerializer);
     }
 
     /**
      * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals)} implementation.
-     */
-    private PolicyDefinition(
-            String key,
-            ResolutionMechanism<V> resolutionMechanism,
-            QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback) {
-        this(key, resolutionMechanism, policyEnforcerCallback, /* callbackArgs= */ null);
-    }
-
-    /**
-     * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals)} implementation.
+     * {@link Object#equals} implementation.
      */
     private PolicyDefinition(
             String key,
             ResolutionMechanism<V> resolutionMechanism,
             int policyFlags,
             QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback,
+            PolicySerializer<V> policySerializer) {
+        this(key, key, resolutionMechanism, policyFlags, policyEnforcerCallback,
+                policySerializer, /* callbackArs= */ null);
+    }
+
+    /**
+     * Callers must ensure that {@code policyType} have implemented an appropriate
+     * {@link Object#equals} implementation.
+     */
+    private PolicyDefinition(
+            String policyKey,
+            String policyDefinitionKey,
+            ResolutionMechanism<V> resolutionMechanism,
+            int policyFlags,
+            QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback,
+            PolicySerializer<V> policySerializer,
             String[] callbackArgs) {
-        mKey = key;
+        mPolicyKey = policyKey;
+        mPolicyDefinitionKey = policyDefinitionKey;
         mResolutionMechanism = resolutionMechanism;
         mPolicyFlags = policyFlags;
         mPolicyEnforcerCallback = policyEnforcerCallback;
+        mPolicySerializer = policySerializer;
         mCallbackArgs = callbackArgs;
+
+        // TODO: maybe use this instead of manually adding to the map
+//        sPolicyDefinitions.put(policyDefinitionKey, this);
     }
 
-    /**
-     * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals)} implementation.
-     */
-    private PolicyDefinition(
-            String key,
-            ResolutionMechanism<V> resolutionMechanism,
-            int policyFlags,
-            QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback) {
-        this(key, resolutionMechanism, policyFlags, policyEnforcerCallback,
-                /* callbackArgs= */ null);
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mPolicyKey);
+        serializer.attribute(
+                /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY, mPolicyDefinitionKey);
+        if (mCallbackArgs != null) {
+            serializer.attribute(/* namespace= */ null, ATTR_CALLBACK_ARGS,
+                    String.join(ATTR_CALLBACK_ARGS_SEPARATOR, mCallbackArgs));
+        }
+    }
+
+    static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser) {
+        String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY);
+        String policyDefinitionKey = parser.getAttributeValue(
+                /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY);
+        String callbackArgsStr = parser.getAttributeValue(
+                /* namespace= */ null, ATTR_CALLBACK_ARGS);
+        String[] callbackArgs = callbackArgsStr == null
+                ? null
+                : callbackArgsStr.split(ATTR_CALLBACK_ARGS_SEPARATOR);
+
+        // TODO: can we avoid casting?
+        if (callbackArgs == null) {
+            return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey);
+        } else {
+            return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey).setArgs(
+                    policyKey, callbackArgs);
+        }
+    }
+
+    void savePolicyValueToXml(TypedXmlSerializer serializer, String attributeName, V value)
+            throws IOException {
+        mPolicySerializer.saveToXml(serializer, attributeName, value);
+    }
+
+    V readPolicyValueFromXml(TypedXmlPullParser parser, String attributeName)
+            throws XmlPullParserException {
+        return mPolicySerializer.readFromXml(parser, attributeName);
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
new file mode 100644
index 0000000..b3259d3
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+abstract class PolicySerializer<V> {
+    abstract void saveToXml(TypedXmlSerializer serializer, String attributeName, V value)
+            throws IOException;
+    abstract V readFromXml(TypedXmlPullParser parser, String attributeName)
+            throws XmlPullParserException;
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index a584d59..aad82cd 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -18,7 +18,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.util.Log;
 
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.Objects;
 
@@ -26,6 +34,11 @@
  * Class containing all values set for a certain policy by different admins.
  */
 final class PolicyState<V> {
+    private static final String TAG_ADMIN_POLICY_ENTRY = "admin-policy-entry";
+    private static final String TAG_ENFORCING_ADMIN_ENTRY = "enforcing-admin-entry";
+    private static final String ATTR_POLICY_VALUE = "policy-value";
+    private static final String ATTR_RESOLVED_POLICY = "resolved-policy";
+
     private final PolicyDefinition<V> mPolicyDefinition;
     private final LinkedHashMap<EnforcingAdmin, V> mAdminsPolicy = new LinkedHashMap<>();
     private V mCurrentResolvedPolicy;
@@ -34,6 +47,18 @@
         mPolicyDefinition = Objects.requireNonNull(policyDefinition);
     }
 
+    private PolicyState(
+            @NonNull PolicyDefinition<V> policyDefinition,
+            @NonNull LinkedHashMap<EnforcingAdmin, V> adminsPolicy,
+            V currentEnforcedPolicy) {
+        Objects.requireNonNull(policyDefinition);
+        Objects.requireNonNull(adminsPolicy);
+
+        mPolicyDefinition = policyDefinition;
+        mAdminsPolicy.putAll(adminsPolicy);
+        mCurrentResolvedPolicy = currentEnforcedPolicy;
+    }
+
     /**
      * Returns {@code true} if the resolved policy has changed, {@code false} otherwise.
      */
@@ -65,4 +90,50 @@
     V getCurrentResolvedPolicy() {
         return mCurrentResolvedPolicy;
     }
+
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        mPolicyDefinition.saveToXml(serializer);
+
+        mPolicyDefinition.savePolicyValueToXml(
+                serializer, ATTR_RESOLVED_POLICY, mCurrentResolvedPolicy);
+
+        for (EnforcingAdmin admin : mAdminsPolicy.keySet()) {
+            serializer.startTag(/* namespace= */ null, TAG_ADMIN_POLICY_ENTRY);
+
+            mPolicyDefinition.savePolicyValueToXml(
+                    serializer, ATTR_POLICY_VALUE, mAdminsPolicy.get(admin));
+
+            serializer.startTag(/* namespace= */ null, TAG_ENFORCING_ADMIN_ENTRY);
+            admin.saveToXml(serializer);
+            serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN_ENTRY);
+
+            serializer.endTag(/* namespace= */ null, TAG_ADMIN_POLICY_ENTRY);
+        }
+    }
+
+    static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        PolicyDefinition<V> policyDefinition = PolicyDefinition.readFromXml(parser);
+        LinkedHashMap<EnforcingAdmin, V> adminsPolicy = new LinkedHashMap<>();
+        V currentResolvedPolicy = policyDefinition.readPolicyValueFromXml(
+                parser, ATTR_RESOLVED_POLICY);
+        int outerDepth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tag = parser.getName();
+            if (TAG_ADMIN_POLICY_ENTRY.equals(tag)) {
+                V value = policyDefinition.readPolicyValueFromXml(
+                        parser, ATTR_POLICY_VALUE);
+                EnforcingAdmin admin;
+                int adminPolicyDepth = parser.getDepth();
+                if (XmlUtils.nextElementWithin(parser, adminPolicyDepth)
+                        && parser.getName().equals(TAG_ENFORCING_ADMIN_ENTRY)) {
+                    admin = EnforcingAdmin.readFromXml(parser);
+                    adminsPolicy.put(admin, value);
+                }
+            } else {
+                Log.e(DevicePolicyEngine.TAG, "Unknown tag: " + tag);
+            }
+        }
+        return new PolicyState<V>(policyDefinition, adminsPolicy, currentResolvedPolicy);
+    }
 }