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