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