1 /* 2 * Copyright (C) 2023 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.server.healthconnect.permission; 18 19 import android.annotation.NonNull; 20 import android.util.ArrayMap; 21 import android.util.AtomicFile; 22 import android.util.Log; 23 import android.util.Xml; 24 25 import libcore.io.IoUtils; 26 27 import org.xmlpull.v1.XmlPullParser; 28 import org.xmlpull.v1.XmlPullParserException; 29 import org.xmlpull.v1.XmlSerializer; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileNotFoundException; 34 import java.io.FileOutputStream; 35 import java.io.IOException; 36 import java.nio.charset.StandardCharsets; 37 import java.time.Instant; 38 import java.util.Map; 39 40 /** 41 * Helper class for serialisation / parsing grant time xml file. 42 * 43 * @hide 44 */ 45 public final class GrantTimeXmlHelper { 46 private static final String TAG = "GrantTimeSerializer"; 47 private static final String TAG_FIRST_GRANT_TIMES = "first-grant-times"; 48 private static final String TAG_PACKAGE = "package"; 49 private static final String TAG_SHARED_USER = "shared-user"; 50 51 private static final String ATTRIBUTE_NAME = "name"; 52 private static final String ATTRIBUTE_FIRST_GRANT_TIME = "first-grant-time"; 53 private static final String ATTRIBUTE_VERSION = "version"; 54 55 /** 56 * Serializes the grant times into the passed file. 57 * 58 * @param userGrantTimeState the grant times to be serialized. 59 * @param file the file into which the serialized data should be written. 60 */ serializeGrantTimes( @onNull File file, @NonNull UserGrantTimeState userGrantTimeState)61 public static void serializeGrantTimes( 62 @NonNull File file, @NonNull UserGrantTimeState userGrantTimeState) { 63 AtomicFile atomicFile = new AtomicFile(file); 64 FileOutputStream outputStream = null; 65 try { 66 outputStream = atomicFile.startWrite(); 67 68 XmlSerializer serializer = Xml.newSerializer(); 69 serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); 70 serializer.startDocument(/* encoding= */ null, /* standalone= */ true); 71 GrantTimeXmlHelper.writeGrantTimes(serializer, userGrantTimeState); 72 73 serializer.endDocument(); 74 atomicFile.finishWrite(outputStream); 75 } catch (Exception e) { 76 Log.wtf(TAG, "Failed to write, restoring backup: " + file, e); 77 atomicFile.failWrite(outputStream); 78 } finally { 79 IoUtils.closeQuietly(outputStream); 80 } 81 } 82 83 /** 84 * Parses the passed grant time file to return the grant times. 85 * 86 * @param file the file from which the data should be parsed. 87 * @return the grant times. 88 */ 89 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression parseGrantTime(File file)90 public static UserGrantTimeState parseGrantTime(File file) { 91 try (FileInputStream inputStream = new AtomicFile(file).openRead()) { 92 XmlPullParser parser = Xml.newPullParser(); 93 parser.setInput(inputStream, /* inputEncoding= */ null); 94 return parseXml(parser); 95 } catch (FileNotFoundException e) { 96 Log.w(TAG, file.getPath() + " not found"); 97 return null; 98 } catch (XmlPullParserException | IOException e) { 99 throw new IllegalStateException("Failed to read " + file, e); 100 } 101 } 102 103 @NonNull parseXml(@onNull XmlPullParser parser)104 private static UserGrantTimeState parseXml(@NonNull XmlPullParser parser) 105 throws IOException, XmlPullParserException { 106 int targetDepth = parser.getDepth() + 1; 107 int type = parser.next(); 108 109 // Scan the xml until find the grant time tag at the target depth. 110 while (type != XmlPullParser.END_DOCUMENT 111 && (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) { 112 if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) { 113 type = parser.next(); 114 continue; 115 } 116 117 if (parser.getName().equals(TAG_FIRST_GRANT_TIMES)) { 118 return parseFirstGrantTimes(parser); 119 } 120 121 type = parser.next(); 122 } 123 throw new IllegalStateException( 124 "Missing <" + TAG_FIRST_GRANT_TIMES + "> in provided file."); 125 } 126 writeGrantTimes( @onNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState)127 private static void writeGrantTimes( 128 @NonNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState) 129 throws IOException { 130 serializer.startTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES); 131 serializer.attribute( 132 /* namespace= */ null, 133 ATTRIBUTE_VERSION, 134 Integer.toString(userGrantTimeState.getVersion())); 135 136 for (Map.Entry<String, Instant> entry : 137 userGrantTimeState.getPackageGrantTimes().entrySet()) { 138 String packageName = entry.getKey(); 139 Instant grantTime = entry.getValue(); 140 141 serializer.startTag(/* namespace= */ null, TAG_PACKAGE); 142 serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, packageName); 143 serializer.attribute( 144 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString()); 145 serializer.endTag(/* namespace= */ null, TAG_PACKAGE); 146 } 147 148 for (Map.Entry<String, Instant> entry : 149 userGrantTimeState.getSharedUserGrantTimes().entrySet()) { 150 String sharedUserName = entry.getKey(); 151 Instant grantTime = entry.getValue(); 152 153 serializer.startTag(/* namespace= */ null, TAG_SHARED_USER); 154 serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, sharedUserName); 155 serializer.attribute( 156 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString()); 157 serializer.endTag(/* namespace= */ null, TAG_SHARED_USER); 158 } 159 160 serializer.endTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES); 161 } 162 163 @NonNull parseFirstGrantTimes(@onNull XmlPullParser parser)164 private static UserGrantTimeState parseFirstGrantTimes(@NonNull XmlPullParser parser) 165 throws IOException, XmlPullParserException { 166 String versionValue = parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_VERSION); 167 int version = 168 versionValue != null 169 ? Integer.parseInt(versionValue) 170 : UserGrantTimeState.NO_VERSION; 171 Map<String, Instant> packagePermissions = new ArrayMap<>(); 172 Map<String, Instant> sharedUserPermissions = new ArrayMap<>(); 173 174 int targetDepth = parser.getDepth() + 1; 175 int type = parser.next(); 176 // Scan the xml until find the needed tags at the target depth. 177 while (type != XmlPullParser.END_DOCUMENT 178 && (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) { 179 if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) { 180 type = parser.next(); 181 continue; 182 } 183 switch (parser.getName()) { 184 case TAG_PACKAGE: 185 { 186 String packageName = 187 parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME); 188 Instant firstGrantTime = 189 Instant.parse( 190 parser.getAttributeValue( 191 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME)); 192 packagePermissions.put(packageName, firstGrantTime); 193 break; 194 } 195 case TAG_SHARED_USER: 196 { 197 String sharedUserName = 198 parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME); 199 Instant firstGrantTime = 200 Instant.parse( 201 parser.getAttributeValue( 202 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME)); 203 sharedUserPermissions.put(sharedUserName, firstGrantTime); 204 break; 205 } 206 default: 207 { 208 Log.w(TAG, "Tag " + parser.getName() + " is not parsed"); 209 } 210 } 211 type = parser.next(); 212 } 213 214 return new UserGrantTimeState(packagePermissions, sharedUserPermissions, version); 215 } 216 } 217