1 /*
2  * Copyright (C) 2022 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.companion.datatransfer;
18 
19 import static android.companion.datatransfer.SystemDataTransferRequest.DATA_TYPE_PERMISSION_SYNC;
20 
21 import static com.android.internal.util.XmlUtils.readBooleanAttribute;
22 import static com.android.internal.util.XmlUtils.readIntAttribute;
23 import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
24 import static com.android.internal.util.XmlUtils.writeIntAttribute;
25 import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
26 import static com.android.server.companion.DataStoreUtils.isEndOfTag;
27 import static com.android.server.companion.DataStoreUtils.isStartOfTag;
28 import static com.android.server.companion.DataStoreUtils.writeToFileSafely;
29 
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.annotation.UserIdInt;
33 import android.companion.datatransfer.PermissionSyncRequest;
34 import android.companion.datatransfer.SystemDataTransferRequest;
35 import android.util.AtomicFile;
36 import android.util.Slog;
37 import android.util.SparseArray;
38 import android.util.Xml;
39 
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.util.XmlUtils;
42 import com.android.modules.utils.TypedXmlPullParser;
43 import com.android.modules.utils.TypedXmlSerializer;
44 
45 import org.xmlpull.v1.XmlPullParserException;
46 
47 import java.io.FileInputStream;
48 import java.io.IOException;
49 import java.util.ArrayList;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.concurrent.ConcurrentMap;
54 import java.util.concurrent.ExecutionException;
55 import java.util.concurrent.ExecutorService;
56 import java.util.concurrent.Executors;
57 import java.util.concurrent.Future;
58 import java.util.concurrent.TimeUnit;
59 import java.util.concurrent.TimeoutException;
60 
61 /**
62  * The class is responsible for reading/writing SystemDataTransferRequest records from/to the disk.
63  * <p>
64  * The following snippet is a sample XML file stored in the disk.
65  * <pre>{@code
66  * <requests>
67  *   <request
68  *     association_id="1"
69  *     data_type="1"
70  *     user_id="12"
71  *     is_user_consented="true"
72  *   </request>
73  * </requests>
74  * }</pre>
75  */
76 public class SystemDataTransferRequestStore {
77 
78     private static final String LOG_TAG = "CDM_SystemDataTransferRequestStore";
79 
80     private static final String FILE_NAME = "companion_device_system_data_transfer_requests.xml";
81 
82     private static final String XML_TAG_REQUESTS = "requests";
83     private static final String XML_TAG_REQUEST = "request";
84 
85     private static final String XML_ATTR_ASSOCIATION_ID = "association_id";
86     private static final String XML_ATTR_DATA_TYPE = "data_type";
87     private static final String XML_ATTR_USER_ID = "user_id";
88     private static final String XML_ATTR_IS_USER_CONSENTED = "is_user_consented";
89 
90     private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds
91 
92     private final ExecutorService mExecutor;
93     private final ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
94             new ConcurrentHashMap<>();
95 
96     private final Object mLock = new Object();
97 
98     @GuardedBy("mLock")
99     private final SparseArray<ArrayList<SystemDataTransferRequest>> mCachedPerUser =
100             new SparseArray<>();
101 
SystemDataTransferRequestStore()102     public SystemDataTransferRequestStore() {
103         mExecutor = Executors.newSingleThreadExecutor();
104     }
105 
106     @NonNull
readRequestsByAssociationId(@serIdInt int userId, int associationId)107     public List<SystemDataTransferRequest> readRequestsByAssociationId(@UserIdInt int userId,
108             int associationId) {
109         List<SystemDataTransferRequest> cachedRequests;
110         synchronized (mLock) {
111             cachedRequests = readRequestsFromCache(userId);
112         }
113 
114         List<SystemDataTransferRequest> requestsByAssociationId = new ArrayList<>();
115         for (SystemDataTransferRequest request : cachedRequests) {
116             if (request.getAssociationId() == associationId) {
117                 requestsByAssociationId.add(request);
118             }
119         }
120         return requestsByAssociationId;
121     }
122 
writeRequest(@serIdInt int userId, SystemDataTransferRequest request)123     public void writeRequest(@UserIdInt int userId, SystemDataTransferRequest request) {
124         Slog.i(LOG_TAG, "Writing request=" + request + " to store.");
125         ArrayList<SystemDataTransferRequest> cachedRequests;
126         synchronized (mLock) {
127             // Write to cache
128             cachedRequests = readRequestsFromCache(userId);
129             cachedRequests.removeIf(
130                     request1 -> request1.getAssociationId() == request.getAssociationId());
131             cachedRequests.add(request);
132             mCachedPerUser.set(userId, cachedRequests);
133         }
134         // Write to store
135         mExecutor.execute(() -> writeRequestsToStore(userId, cachedRequests));
136     }
137 
138     /**
139      * Remove requests by association id. userId must be the one which owns the associationId.
140      */
removeRequestsByAssociationId(@serIdInt int userId, int associationId)141     public void removeRequestsByAssociationId(@UserIdInt int userId, int associationId) {
142         Slog.i(LOG_TAG, "Removing system data transfer requests for userId=" + userId
143                 + ", associationId=" + associationId);
144         ArrayList<SystemDataTransferRequest> cachedRequests;
145         synchronized (mLock) {
146             // Remove requests from cache
147             cachedRequests = readRequestsFromCache(userId);
148             cachedRequests.removeIf(request -> request.getAssociationId() == associationId);
149             mCachedPerUser.set(userId, cachedRequests);
150         }
151         // Remove requests from store
152         mExecutor.execute(() -> writeRequestsToStore(userId, cachedRequests));
153     }
154 
155     @GuardedBy("mLock")
readRequestsFromCache(@serIdInt int userId)156     private ArrayList<SystemDataTransferRequest> readRequestsFromCache(@UserIdInt int userId) {
157         ArrayList<SystemDataTransferRequest> cachedRequests = mCachedPerUser.get(userId);
158         if (cachedRequests == null) {
159             Future<ArrayList<SystemDataTransferRequest>> future =
160                     mExecutor.submit(() -> readRequestsFromStore(userId));
161             try {
162                 cachedRequests = future.get(READ_FROM_DISK_TIMEOUT, TimeUnit.SECONDS);
163             } catch (InterruptedException e) {
164                 Slog.e(LOG_TAG, "Thread reading SystemDataTransferRequest from disk is "
165                         + "interrupted.");
166             } catch (ExecutionException e) {
167                 Slog.e(LOG_TAG, "Error occurred while reading SystemDataTransferRequest "
168                         + "from disk.");
169             } catch (TimeoutException e) {
170                 Slog.e(LOG_TAG, "Reading SystemDataTransferRequest from disk timed out.");
171             }
172             mCachedPerUser.set(userId, cachedRequests);
173         }
174         return cachedRequests;
175     }
176 
177     /**
178      * Reads previously persisted data for the given user
179      *
180      * @param userId Android UserID
181      * @return a list of SystemDataTransferRequest
182      */
183     @NonNull
readRequestsFromStore(@serIdInt int userId)184     private ArrayList<SystemDataTransferRequest> readRequestsFromStore(@UserIdInt int userId) {
185         final AtomicFile file = getStorageFileForUser(userId);
186         Slog.i(LOG_TAG, "Reading SystemDataTransferRequests for user " + userId + " from "
187                 + "file=" + file.getBaseFile().getPath());
188 
189         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
190         // accesses to the file on the file system using this AtomicFile object.
191         synchronized (file) {
192             if (!file.getBaseFile().exists()) {
193                 Slog.d(LOG_TAG, "File does not exist -> Abort");
194                 return new ArrayList<>();
195             }
196             try (FileInputStream in = file.openRead()) {
197                 final TypedXmlPullParser parser = Xml.resolvePullParser(in);
198                 XmlUtils.beginDocument(parser, XML_TAG_REQUESTS);
199 
200                 return readRequestsFromXml(parser);
201             } catch (XmlPullParserException | IOException e) {
202                 Slog.e(LOG_TAG, "Error while reading requests file", e);
203                 return new ArrayList<>();
204             }
205         }
206     }
207 
208     @NonNull
readRequestsFromXml( @onNull TypedXmlPullParser parser)209     private ArrayList<SystemDataTransferRequest> readRequestsFromXml(
210             @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException {
211         if (!isStartOfTag(parser, XML_TAG_REQUESTS)) {
212             throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_REQUESTS);
213         }
214 
215         ArrayList<SystemDataTransferRequest> requests = new ArrayList<>();
216 
217         while (true) {
218             parser.nextTag();
219             if (isEndOfTag(parser, XML_TAG_REQUESTS)) {
220                 break;
221             }
222             if (isStartOfTag(parser, XML_TAG_REQUEST)) {
223                 requests.add(readRequestFromXml(parser));
224             }
225         }
226 
227         return requests;
228     }
229 
readRequestFromXml(@onNull TypedXmlPullParser parser)230     private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser)
231             throws XmlPullParserException, IOException {
232         if (!isStartOfTag(parser, XML_TAG_REQUEST)) {
233             throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_REQUEST);
234         }
235 
236         final int associationId = readIntAttribute(parser, XML_ATTR_ASSOCIATION_ID);
237         final int dataType = readIntAttribute(parser, XML_ATTR_DATA_TYPE);
238         final int userId = readIntAttribute(parser, XML_ATTR_USER_ID);
239         final boolean isUserConsented = readBooleanAttribute(parser, XML_ATTR_IS_USER_CONSENTED);
240 
241         switch (dataType) {
242             case DATA_TYPE_PERMISSION_SYNC:
243                 PermissionSyncRequest request = new PermissionSyncRequest(associationId);
244                 request.setUserId(userId);
245                 request.setUserConsented(isUserConsented);
246                 return request;
247             default:
248                 return null;
249         }
250     }
251 
252     /**
253      * Persisted user's SystemDataTransferRequest data to the disk.
254      *
255      * @param userId   Android UserID
256      * @param requests a list of user's SystemDataTransferRequest.
257      */
writeRequestsToStore(@serIdInt int userId, @NonNull List<SystemDataTransferRequest> requests)258     void writeRequestsToStore(@UserIdInt int userId,
259             @NonNull List<SystemDataTransferRequest> requests) {
260         final AtomicFile file = getStorageFileForUser(userId);
261         Slog.i(LOG_TAG, "Writing SystemDataTransferRequests for user " + userId + " to file="
262                 + file.getBaseFile().getPath());
263 
264         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
265         // accesses to the file on the file system using this AtomicFile object.
266         synchronized (file) {
267             writeToFileSafely(file, out -> {
268                 final TypedXmlSerializer serializer = Xml.resolveSerializer(out);
269                 serializer.setFeature(
270                         "http://xmlpull.org/v1/doc/features.html#indent-output", true);
271                 serializer.startDocument(null, true);
272                 writeRequestsToXml(serializer, requests);
273                 serializer.endDocument();
274             });
275         }
276     }
277 
writeRequestsToXml(@onNull TypedXmlSerializer serializer, @Nullable Collection<SystemDataTransferRequest> requests)278     private void writeRequestsToXml(@NonNull TypedXmlSerializer serializer,
279             @Nullable Collection<SystemDataTransferRequest> requests) throws IOException {
280         serializer.startTag(null, XML_TAG_REQUESTS);
281 
282         for (SystemDataTransferRequest request : requests) {
283             writeRequestToXml(serializer, request);
284         }
285 
286         serializer.endTag(null, XML_TAG_REQUESTS);
287     }
288 
writeRequestToXml(@onNull TypedXmlSerializer serializer, @NonNull SystemDataTransferRequest request)289     private void writeRequestToXml(@NonNull TypedXmlSerializer serializer,
290             @NonNull SystemDataTransferRequest request) throws IOException {
291         serializer.startTag(null, XML_TAG_REQUEST);
292 
293         writeIntAttribute(serializer, XML_ATTR_ASSOCIATION_ID, request.getAssociationId());
294         writeIntAttribute(serializer, XML_ATTR_DATA_TYPE, request.getDataType());
295         writeIntAttribute(serializer, XML_ATTR_USER_ID, request.getUserId());
296         writeBooleanAttribute(serializer, XML_ATTR_IS_USER_CONSENTED, request.isUserConsented());
297 
298         serializer.endTag(null, XML_TAG_REQUEST);
299     }
300 
301     /**
302      * Creates and caches {@link AtomicFile} object that represents the back-up file for the given
303      * user.
304      * <p>
305      * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it
306      * possible to synchronize reads and writes to the file using the returned object.
307      */
308     @NonNull
getStorageFileForUser(@serIdInt int userId)309     private AtomicFile getStorageFileForUser(@UserIdInt int userId) {
310         return mUserIdToStorageFile.computeIfAbsent(userId,
311                 u -> createStorageFileForUser(userId, FILE_NAME));
312     }
313 }
314