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