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.companion.devicepresence; 18 19 import static com.android.internal.util.XmlUtils.readIntAttribute; 20 import static com.android.internal.util.XmlUtils.readLongAttribute; 21 import static com.android.internal.util.XmlUtils.readStringAttribute; 22 import static com.android.internal.util.XmlUtils.writeIntAttribute; 23 import static com.android.internal.util.XmlUtils.writeLongAttribute; 24 import static com.android.internal.util.XmlUtils.writeStringAttribute; 25 import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser; 26 import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag; 27 import static com.android.server.companion.utils.DataStoreUtils.isStartOfTag; 28 import static com.android.server.companion.utils.DataStoreUtils.writeToFileSafely; 29 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.UserIdInt; 33 import android.os.ParcelUuid; 34 import android.util.AtomicFile; 35 import android.util.Slog; 36 import android.util.SparseArray; 37 import android.util.Xml; 38 39 import com.android.internal.annotations.GuardedBy; 40 import com.android.internal.util.XmlUtils; 41 import com.android.modules.utils.TypedXmlPullParser; 42 import com.android.modules.utils.TypedXmlSerializer; 43 44 import org.xmlpull.v1.XmlPullParserException; 45 46 import java.io.FileInputStream; 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.List; 51 import java.util.concurrent.ConcurrentHashMap; 52 import java.util.concurrent.ConcurrentMap; 53 import java.util.concurrent.ExecutionException; 54 import java.util.concurrent.ExecutorService; 55 import java.util.concurrent.Executors; 56 import java.util.concurrent.Future; 57 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.TimeoutException; 59 60 /** 61 * This store manages the cache and disk data for observable uuids. 62 */ 63 public class ObservableUuidStore { 64 private static final String TAG = "CDM_ObservableUuidStore"; 65 private static final String FILE_NAME = "observing_uuids_presence.xml"; 66 private static final String XML_TAG_UUIDS = "uuids"; 67 private static final String XML_TAG_UUID = "uuid"; 68 private static final String XML_ATTR_UUID = "uuid"; 69 private static final String XML_ATTR_TIME_APPROVED = "time_approved"; 70 private static final String XML_ATTR_USER_ID = "user_id"; 71 private static final String XML_ATTR_PACKAGE = "package_name"; 72 private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds 73 74 75 private final ExecutorService mExecutor; 76 private final ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = 77 new ConcurrentHashMap<>(); 78 79 private final Object mLock = new Object(); 80 81 @GuardedBy("mLock") 82 private final SparseArray<List<ObservableUuid>> mCachedPerUser = 83 new SparseArray<>(); 84 ObservableUuidStore()85 public ObservableUuidStore() { 86 mExecutor = Executors.newSingleThreadExecutor(); 87 } 88 89 /** 90 * Remove the observable uuid. 91 */ removeObservableUuid(@serIdInt int userId, ParcelUuid uuid, String packageName)92 public void removeObservableUuid(@UserIdInt int userId, ParcelUuid uuid, String packageName) { 93 Slog.i(TAG, "Removing uuid=[" + uuid.getUuid() + "] from store..."); 94 95 List<ObservableUuid> cachedObservableUuids; 96 97 synchronized (mLock) { 98 // Remove requests from cache 99 cachedObservableUuids = readObservableUuidsFromCache(userId); 100 cachedObservableUuids.removeIf( 101 uuid1 -> uuid1.getPackageName().equals(packageName) 102 && uuid1.getUuid().equals(uuid)); 103 mCachedPerUser.set(userId, cachedObservableUuids); 104 } 105 // Remove requests from store 106 mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids)); 107 } 108 109 /** 110 * Write the observable uuid. 111 */ writeObservableUuid(@serIdInt int userId, ObservableUuid uuid)112 public void writeObservableUuid(@UserIdInt int userId, ObservableUuid uuid) { 113 Slog.i(TAG, "Writing uuid=[" + uuid.getUuid() + "] to store..."); 114 115 List<ObservableUuid> cachedObservableUuids; 116 synchronized (mLock) { 117 // Write to cache 118 cachedObservableUuids = readObservableUuidsFromCache(userId); 119 cachedObservableUuids.removeIf(uuid1 -> uuid1.getUuid().equals( 120 uuid.getUuid()) && uuid1.getPackageName().equals(uuid.getPackageName())); 121 cachedObservableUuids.add(uuid); 122 mCachedPerUser.set(userId, cachedObservableUuids); 123 } 124 // Write to store 125 mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids)); 126 } 127 writeObservableUuidToStore(@serIdInt int userId, @NonNull List<ObservableUuid> cachedObservableUuids)128 private void writeObservableUuidToStore(@UserIdInt int userId, 129 @NonNull List<ObservableUuid> cachedObservableUuids) { 130 final AtomicFile file = getStorageFileForUser(userId); 131 Slog.i(TAG, "Writing ObservableUuid for user " + userId + " to file=" 132 + file.getBaseFile().getPath()); 133 134 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 135 // accesses to the file on the file system using this AtomicFile object. 136 synchronized (file) { 137 writeToFileSafely(file, out -> { 138 final TypedXmlSerializer serializer = Xml.resolveSerializer(out); 139 serializer.setFeature( 140 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 141 serializer.startDocument(null, true); 142 writeObservableUuidToXml(serializer, cachedObservableUuids); 143 serializer.endDocument(); 144 }); 145 } 146 } 147 writeObservableUuidToXml(@onNull TypedXmlSerializer serializer, @Nullable Collection<ObservableUuid> uuids)148 private void writeObservableUuidToXml(@NonNull TypedXmlSerializer serializer, 149 @Nullable Collection<ObservableUuid> uuids) throws IOException { 150 serializer.startTag(null, XML_TAG_UUIDS); 151 152 for (ObservableUuid uuid : uuids) { 153 writeUuidToXml(serializer, uuid); 154 } 155 156 serializer.endTag(null, XML_TAG_UUIDS); 157 } 158 writeUuidToXml(@onNull TypedXmlSerializer serializer, @NonNull ObservableUuid uuid)159 private void writeUuidToXml(@NonNull TypedXmlSerializer serializer, 160 @NonNull ObservableUuid uuid) throws IOException { 161 serializer.startTag(null, XML_TAG_UUID); 162 163 writeIntAttribute(serializer, XML_ATTR_USER_ID, uuid.getUserId()); 164 writeStringAttribute(serializer, XML_ATTR_UUID, uuid.getUuid().toString()); 165 writeStringAttribute(serializer, XML_ATTR_PACKAGE, uuid.getPackageName()); 166 writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, uuid.getTimeApprovedMs()); 167 168 serializer.endTag(null, XML_TAG_UUID); 169 } 170 171 /** 172 * Read the observable UUIDs from the cache. 173 */ 174 @GuardedBy("mLock") readObservableUuidsFromCache(@serIdInt int userId)175 private List<ObservableUuid> readObservableUuidsFromCache(@UserIdInt int userId) { 176 List<ObservableUuid> cachedObservableUuids = mCachedPerUser.get(userId); 177 if (cachedObservableUuids == null) { 178 Future<List<ObservableUuid>> future = 179 mExecutor.submit(() -> readObservableUuidFromStore(userId)); 180 try { 181 cachedObservableUuids = future.get(READ_FROM_DISK_TIMEOUT, TimeUnit.SECONDS); 182 } catch (InterruptedException e) { 183 Slog.e(TAG, "Thread reading ObservableUuid from disk is " 184 + "interrupted."); 185 } catch (ExecutionException e) { 186 Slog.e(TAG, "Error occurred while reading ObservableUuid " 187 + "from disk."); 188 } catch (TimeoutException e) { 189 Slog.e(TAG, "Reading ObservableUuid from disk timed out."); 190 } 191 mCachedPerUser.set(userId, cachedObservableUuids); 192 } 193 return cachedObservableUuids; 194 } 195 196 /** 197 * Reads previously persisted data for the given user 198 * 199 * @param userId Android UserID 200 * @return a list of ObservableUuid 201 */ 202 @NonNull readObservableUuidFromStore(@serIdInt int userId)203 public List<ObservableUuid> readObservableUuidFromStore(@UserIdInt int userId) { 204 final AtomicFile file = getStorageFileForUser(userId); 205 Slog.i(TAG, "Reading ObservableUuid for user " + userId + " from " 206 + "file=" + file.getBaseFile().getPath()); 207 208 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 209 // accesses to the file on the file system using this AtomicFile object. 210 synchronized (file) { 211 if (!file.getBaseFile().exists()) { 212 Slog.d(TAG, "File does not exist -> Abort"); 213 return new ArrayList<>(); 214 } 215 try (FileInputStream in = file.openRead()) { 216 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 217 XmlUtils.beginDocument(parser, XML_TAG_UUIDS); 218 219 return readObservableUuidFromXml(parser); 220 } catch (XmlPullParserException | IOException e) { 221 Slog.e(TAG, "Error while reading requests file", e); 222 return new ArrayList<>(); 223 } 224 } 225 } 226 227 @NonNull readObservableUuidFromXml( @onNull TypedXmlPullParser parser)228 private List<ObservableUuid> readObservableUuidFromXml( 229 @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException { 230 if (!isStartOfTag(parser, XML_TAG_UUIDS)) { 231 throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_UUIDS); 232 } 233 234 List<ObservableUuid> observableUuids = new ArrayList<>(); 235 236 while (true) { 237 parser.nextTag(); 238 if (isEndOfTag(parser, XML_TAG_UUIDS)) { 239 break; 240 } 241 if (isStartOfTag(parser, XML_TAG_UUID)) { 242 observableUuids.add(readUuidFromXml(parser)); 243 } 244 } 245 246 return observableUuids; 247 } 248 readUuidFromXml(@onNull TypedXmlPullParser parser)249 private ObservableUuid readUuidFromXml(@NonNull TypedXmlPullParser parser) 250 throws XmlPullParserException, IOException { 251 if (!isStartOfTag(parser, XML_TAG_UUID)) { 252 throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_UUID); 253 } 254 255 final int userId = readIntAttribute(parser, XML_ATTR_USER_ID); 256 final ParcelUuid uuid = ParcelUuid.fromString(readStringAttribute(parser, XML_ATTR_UUID)); 257 final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE); 258 final Long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED); 259 260 return new ObservableUuid(userId, uuid, packageName, timeApproved); 261 } 262 263 /** 264 * Creates and caches {@link AtomicFile} object that represents the back-up file for the given 265 * user. 266 * <p> 267 * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it 268 * possible to synchronize reads and writes to the file using the returned object. 269 */ 270 @NonNull getStorageFileForUser(@serIdInt int userId)271 private AtomicFile getStorageFileForUser(@UserIdInt int userId) { 272 return mUserIdToStorageFile.computeIfAbsent(userId, 273 u -> createStorageFileForUser(userId, FILE_NAME)); 274 } 275 276 /** 277 * @return A list of ObservableUuids per package. 278 */ getObservableUuidsForPackage( @serIdInt int userId, @NonNull String packageName)279 public List<ObservableUuid> getObservableUuidsForPackage( 280 @UserIdInt int userId, @NonNull String packageName) { 281 final List<ObservableUuid> uuidsTobeObservedPerPackage = new ArrayList<>(); 282 synchronized (mLock) { 283 final List<ObservableUuid> uuids = readObservableUuidsFromCache(userId); 284 285 for (ObservableUuid uuid : uuids) { 286 if (uuid.getPackageName().equals(packageName)) { 287 uuidsTobeObservedPerPackage.add(uuid); 288 } 289 } 290 } 291 292 return uuidsTobeObservedPerPackage; 293 } 294 295 /** 296 * @return A list of ObservableUuids per user. 297 */ getObservableUuidsForUser(@serIdInt int userId)298 public List<ObservableUuid> getObservableUuidsForUser(@UserIdInt int userId) { 299 synchronized (mLock) { 300 return readObservableUuidsFromCache(userId); 301 } 302 } 303 304 /** 305 * Check if a UUID is being observed by the package. 306 */ isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName)307 public boolean isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName) { 308 final List<ObservableUuid> uuidsBeingObserved = getObservableUuidsForPackage(userId, 309 packageName); 310 for (ObservableUuid observableUuid : uuidsBeingObserved) { 311 if (observableUuid.getUuid().equals(uuid)) { 312 return true; 313 } 314 } 315 return false; 316 } 317 } 318