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