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.devicelock; 18 19 import android.annotation.WorkerThread; 20 import android.util.AtomicFile; 21 import android.util.Slog; 22 import android.util.Xml; 23 24 import com.android.devicelockcontroller.util.ThreadAsserts; 25 26 import org.xmlpull.v1.XmlPullParser; 27 import org.xmlpull.v1.XmlPullParserException; 28 import org.xmlpull.v1.XmlSerializer; 29 30 import java.io.File; 31 import java.io.FileInputStream; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.util.concurrent.Executor; 35 36 /** 37 * Class that manages persisting device state data for the system service. 38 */ 39 public final class DeviceLockPersistentStore { 40 private static final String TAG = DeviceLockPersistentStore.class.getSimpleName(); 41 private static final String SYSTEM_DIR = "system"; 42 private static final String DEVICE_LOCK_DIR = "device_lock"; 43 private static final String DEVICE_STATE_FILE = "device_state.xml"; 44 private static final String TAG_DEVICE_STATE = "device_state"; 45 private static final String ATTR_IS_DEVICE_FINALIZED = "is_device_finalized"; 46 47 private final Executor mBgExecutor; 48 private final File mFile; 49 DeviceLockPersistentStore(Executor bgExecutor, File dataDirectory)50 DeviceLockPersistentStore(Executor bgExecutor, File dataDirectory) { 51 mBgExecutor = bgExecutor; 52 final File systemDir = new File(dataDirectory, SYSTEM_DIR); 53 final File deviceLockDir = new File(systemDir, DEVICE_LOCK_DIR); 54 if (!deviceLockDir.exists()) { 55 final boolean madeDirs = deviceLockDir.mkdirs(); 56 if (!madeDirs) { 57 Slog.e(TAG, "Failed to make directory " + deviceLockDir.getAbsolutePath()); 58 } 59 } 60 mFile = new File(deviceLockDir, DEVICE_STATE_FILE); 61 } 62 63 /** 64 * Schedule a write of the finalized state. 65 * 66 * @param finalized true if device is fully finalized 67 */ scheduleWrite(boolean finalized)68 public void scheduleWrite(boolean finalized) { 69 mBgExecutor.execute(() -> writeState(finalized)); 70 } 71 72 /** 73 * Read the finalized state from disk 74 * 75 * @param callback callback for when state is read 76 * @param callbackExecutor executor to run callback on 77 */ readFinalizedState(DeviceStateCallback callback, Executor callbackExecutor)78 public void readFinalizedState(DeviceStateCallback callback, Executor callbackExecutor) { 79 mBgExecutor.execute(() -> { 80 final boolean isFinalized = readState(); 81 callbackExecutor.execute(() -> callback.onDeviceStateRead(isFinalized)); 82 }); 83 } 84 85 @WorkerThread writeState(boolean finalized)86 private void writeState(boolean finalized) { 87 ThreadAsserts.assertWorkerThread("writeState"); 88 synchronized (this) { 89 AtomicFile atomicFile = new AtomicFile(mFile); 90 91 try (FileOutputStream fileOutputStream = atomicFile.startWrite()) { 92 try { 93 XmlSerializer serializer = Xml.newSerializer(); 94 serializer.setOutput(fileOutputStream, Xml.Encoding.UTF_16.name()); 95 serializer.startDocument(Xml.Encoding.UTF_16.name(), true /* standalone */); 96 writeToXml(serializer, finalized); 97 serializer.endDocument(); 98 fileOutputStream.flush(); 99 atomicFile.finishWrite(fileOutputStream); 100 } catch (IOException e) { 101 Slog.e(TAG, "Failed to write to XML", e); 102 atomicFile.failWrite(fileOutputStream); 103 } 104 } catch (IOException e) { 105 Slog.e(TAG, "Failed to start write", e); 106 } 107 } 108 } 109 writeToXml(XmlSerializer serializer, boolean finalized)110 private void writeToXml(XmlSerializer serializer, boolean finalized) throws IOException { 111 serializer.startTag(null /* namespace */, TAG_DEVICE_STATE); 112 serializer.attribute(null /* namespace */, 113 ATTR_IS_DEVICE_FINALIZED, Boolean.toString(finalized)); 114 serializer.endTag(null /* namespace */, TAG_DEVICE_STATE); 115 } 116 117 @WorkerThread readState()118 private boolean readState() { 119 ThreadAsserts.assertWorkerThread("readState"); 120 synchronized (this) { 121 if (!mFile.exists()) { 122 return false; 123 } 124 AtomicFile atomicFile = new AtomicFile(mFile); 125 126 try (FileInputStream inputStream = atomicFile.openRead()) { 127 XmlPullParser parser = Xml.newPullParser(); 128 parser.setInput(inputStream, /* inputEncoding= */ null); 129 return getStateFromXml(parser); 130 } catch (XmlPullParserException | IOException e) { 131 Slog.e(TAG, "Failed to read XML", e); 132 return false; 133 } 134 } 135 } 136 getStateFromXml(XmlPullParser parser)137 private boolean getStateFromXml(XmlPullParser parser) 138 throws XmlPullParserException, IOException { 139 while (parser.getEventType() != XmlPullParser.START_TAG 140 || !TAG_DEVICE_STATE.equals(parser.getName())) { 141 if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { 142 throw new XmlPullParserException("Malformed XML. Unable to find start of tag."); 143 } 144 parser.next(); 145 } 146 return Boolean.parseBoolean( 147 parser.getAttributeValue(null /* namespace */, ATTR_IS_DEVICE_FINALIZED)); 148 } 149 150 /** 151 * Callback for when state is read from disk. 152 */ 153 interface DeviceStateCallback { 154 /** 155 * Callback for when state is finished reading from disk. 156 * 157 * @param isFinalized whether device is finalized 158 */ onDeviceStateRead(boolean isFinalized)159 void onDeviceStateRead(boolean isFinalized); 160 } 161 } 162