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