blob: 8f77dfa3bd245f2bf920d524f92d2e0449042917 [file] [log] [blame]
/*
* 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.healthconnect.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.healthconnect.Constants;
import android.os.Environment;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Xml;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
class FirstGrantTimeDatastoreXmlPersistence implements FirstGrantTimeDatastore {
private static final String TAG = "HealthConnectFirstGrantTimeDatastore";
private static final String GRANT_TIME_FILE_NAME = "health-permissions-first-grant-times.xml";
private static final String TAG_FIRST_GRANT_TIMES = "first-grant-times";
private static final String TAG_PACKAGE = "package";
private static final String TAG_SHARED_USER = "shared-user";
private static final String ATTRIBUTE_NAME = "name";
private static final String ATTRIBUTE_FIRST_GRANT_TIME = "first-grant-time";
private static final String ATTRIBUTE_VERSION = "version";
/**
* Read {@link UserGrantTimeState for given user}.
*
* @hide
*/
@Nullable
@Override
public UserGrantTimeState readForUser(@NonNull UserHandle user) {
File file = getFile(user);
if (Constants.DEBUG) {
Log.d(TAG, "Reading xml from " + file);
}
try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(inputStream, /* inputEncoding= */ null);
return parseXml(parser);
} catch (FileNotFoundException e) {
Log.i(TAG, GRANT_TIME_FILE_NAME + " not found");
return null;
} catch (XmlPullParserException | IOException e) {
throw new IllegalStateException("Failed to read " + file, e);
}
}
/**
* Write {@link UserGrantTimeState for given user}.
*
* @hide
*/
@Override
public void writeForUser(
@NonNull UserGrantTimeState grantTimesState, @NonNull UserHandle user) {
File file = getFile(user);
if (Constants.DEBUG) {
Log.d(TAG, "Writing xml to " + file);
}
AtomicFile atomicFile = new AtomicFile(file);
FileOutputStream outputStream = null;
try {
outputStream = atomicFile.startWrite();
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
serializer.startDocument(/* encoding= */ null, /* standalone= */ true);
serializeGrantTimes(serializer, grantTimesState);
serializer.endDocument();
atomicFile.finishWrite(outputStream);
} catch (Exception e) {
Log.wtf(
TAG,
"Failed to write " + GRANT_TIME_FILE_NAME + ", restoring backup: " + file,
e);
atomicFile.failWrite(outputStream);
} finally {
IoUtils.closeQuietly(outputStream);
}
}
private static void serializeGrantTimes(
@NonNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState)
throws IOException {
serializer.startTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
serializer.attribute(
/* namespace= */ null,
ATTRIBUTE_VERSION,
Integer.toString(userGrantTimeState.getVersion()));
for (Map.Entry<String, Instant> entry :
userGrantTimeState.getPackageGrantTimes().entrySet()) {
String packageName = entry.getKey();
Instant grantTime = entry.getValue();
serializer.startTag(/* namespace= */ null, TAG_PACKAGE);
serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, packageName);
serializer.attribute(
/* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
serializer.endTag(/* namespace= */ null, TAG_PACKAGE);
}
for (Map.Entry<String, Instant> entry :
userGrantTimeState.getSharedUserGrantTimes().entrySet()) {
String sharedUserName = entry.getKey();
Instant grantTime = entry.getValue();
serializer.startTag(/* namespace= */ null, TAG_SHARED_USER);
serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, sharedUserName);
serializer.attribute(
/* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
serializer.endTag(/* namespace= */ null, TAG_SHARED_USER);
}
serializer.endTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
}
@NonNull
private static UserGrantTimeState parseXml(@NonNull XmlPullParser parser)
throws IOException, XmlPullParserException {
int targetDepth = parser.getDepth() + 1;
int type = parser.next();
// Scan the xml until find the grant time tag at the target depth.
while (type != XmlPullParser.END_DOCUMENT
&& (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) {
if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) {
type = parser.next();
continue;
}
if (parser.getName().equals(TAG_FIRST_GRANT_TIMES)) {
return parseFirstGrantTimes(parser);
}
type = parser.next();
}
throw new IllegalStateException(
"Missing <" + TAG_FIRST_GRANT_TIMES + "> in " + GRANT_TIME_FILE_NAME);
}
@NonNull
private static UserGrantTimeState parseFirstGrantTimes(@NonNull XmlPullParser parser)
throws IOException, XmlPullParserException {
String versionValue = parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_VERSION);
int version =
versionValue != null
? Integer.parseInt(versionValue)
: UserGrantTimeState.NO_VERSION;
Map<String, Instant> packagePermissions = new ArrayMap<>();
Map<String, Instant> sharedUserPermissions = new ArrayMap<>();
int targetDepth = parser.getDepth() + 1;
int type = parser.next();
// Scan the xml until find the needed tags at the target depth.
while (type != XmlPullParser.END_DOCUMENT
&& (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) {
if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) {
type = parser.next();
continue;
}
switch (parser.getName()) {
case TAG_PACKAGE:
{
String packageName =
parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME);
Instant firstGrantTime =
Instant.parse(
parser.getAttributeValue(
/* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME));
packagePermissions.put(packageName, firstGrantTime);
break;
}
case TAG_SHARED_USER:
{
String sharedUserName =
parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME);
Instant firstGrantTime =
Instant.parse(
parser.getAttributeValue(
/* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME));
sharedUserPermissions.put(sharedUserName, firstGrantTime);
break;
}
default:
{
Log.w(TAG, "Tag " + parser.getName() + " is not parsed");
}
}
type = parser.next();
}
return new UserGrantTimeState(packagePermissions, sharedUserPermissions, version);
}
private static File getModuleDataDirectoryPerUser(@NonNull UserHandle user) {
// TODO(b/243144969): Switch to the final data destination once it's finalised.
File systemDataDir = new File(Environment.getDataDirectory(), /* child= */ "system");
File moduleDataDir = new File(systemDataDir, /* child= */ "healthconnect");
return new File(moduleDataDir, String.valueOf(user.getIdentifier()));
}
@NonNull
private static File getFile(@NonNull UserHandle user) {
File moduleDataDirPerUser = getModuleDataDirectoryPerUser(user);
return new File(moduleDataDirPerUser, GRANT_TIME_FILE_NAME);
}
}