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