1 /*
2  * Copyright 2024 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 androidx.appsearch.localstorage;
18 
19 import android.os.ParcelFileDescriptor;
20 
21 
22 import androidx.annotation.GuardedBy;
23 import androidx.annotation.RestrictTo;
24 import androidx.appsearch.app.AppSearchBlobHandle;
25 import androidx.appsearch.app.AppSearchResult;
26 import androidx.appsearch.app.ExperimentalAppSearchApi;
27 import androidx.appsearch.exceptions.AppSearchException;
28 import androidx.collection.ArrayMap;
29 import androidx.collection.ArraySet;
30 import androidx.core.util.Preconditions;
31 
32 import org.jspecify.annotations.NonNull;
33 import org.jspecify.annotations.Nullable;
34 
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Set;
40 
41 /**
42  * The base class for revocable file descriptors storage.
43  *
44  * <p>This store allows wrapping {@link ParcelFileDescriptor} instances into revocable file
45  * descriptors, enabling the ability to close and revoke it's access to the file even if the
46  * {@link ParcelFileDescriptor} has been sent to the client side.
47  *
48  * <p> This class can provide controlled access to resources by associating each file descriptor
49  * with a package and allowing them to be individually revoked by package or revoked all at once.
50  *
51  * <p> The sub-class must define how to wrap a {@link ParcelFileDescriptor} to a
52  * {@link AppSearchRevocableFileDescriptor}.
53  *
54  * <p> This class stores {@link AppSearchBlobHandle} and returned
55  * {@link AppSearchRevocableFileDescriptor} for writing in key-value pairs map. Only one opened
56  * {@link AppSearchRevocableFileDescriptor} for writing will be allowed for each
57  * {@link AppSearchBlobHandle}.
58  *
59  * <p> This class stores {@link AppSearchRevocableFileDescriptor} for reading in a list. There is no
60  * use case to look up and invoke a single {@link AppSearchRevocableFileDescriptor} for reading.
61  *
62  * @exportToFramework:hide
63  */
64 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
65 @ExperimentalAppSearchApi
66 public abstract class RevocableFileDescriptorStore {
67 
68     private final Object mLock  = new Object();
69     private final AppSearchConfig mConfig;
70 
RevocableFileDescriptorStore(@onNull AppSearchConfig config)71     public RevocableFileDescriptorStore(@NonNull AppSearchConfig config) {
72         mConfig = Preconditions.checkNotNull(config);
73     }
74 
75     @GuardedBy("mLock")
76     // Map<package, Map<blob handle, sent rfds>> map to track all sent rfds for writing. We only
77     // allow user to open 1 pfd for write for same file.
78     private final Map<String, Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor>>
79             mSentRevocableFileDescriptorsForWriteLocked = new ArrayMap<>();
80 
81     @GuardedBy("mLock")
82     // <package, List<sent rfds> map to track all sent rfds for reading. We allow opening
83     // multiple pfds for read for the same file.
84     private final Map<String, List<AppSearchRevocableFileDescriptor>>
85             mSentRevocableFileDescriptorsForReadLocked = new ArrayMap<>();
86 
87     /**
88      * Wraps the provided {@link ParcelFileDescriptor} into a revocable file descriptor.
89      *
90      * <p>This allows for controlled access to the file descriptor, making it revocable by the
91      * store.
92      *
93      * @param packageName The package name requesting the revocable file descriptor.
94      * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
95      *                   the mode is READ_WRITE.
96      * @param parcelFileDescriptor The original {@link ParcelFileDescriptor} to be wrapped.
97      * @param mode  The mode of the given {@link ParcelFileDescriptor}. It should be
98      * {@link ParcelFileDescriptor#MODE_READ_ONLY} or {@link ParcelFileDescriptor#MODE_READ_WRITE}.
99      * @return A {@link ParcelFileDescriptor} that can be revoked by the store.
100      * @throws IOException if an I/O error occurs while creating the revocable file descriptor.
101      */
wrapToRevocableFileDescriptor( @onNull String packageName, @Nullable AppSearchBlobHandle blobHandle, @NonNull ParcelFileDescriptor parcelFileDescriptor, int mode)102     public  @NonNull ParcelFileDescriptor wrapToRevocableFileDescriptor(
103             @NonNull String packageName,
104             @Nullable AppSearchBlobHandle blobHandle,
105             @NonNull ParcelFileDescriptor parcelFileDescriptor,
106             int mode) throws IOException {
107         AppSearchRevocableFileDescriptor revocableFileDescriptor =
108                 wrapToRevocableFileDescriptor(parcelFileDescriptor, mode);
109         setCloseListenerToFd(packageName, blobHandle, revocableFileDescriptor);
110         addToSentRevocableFileDescriptorMap(packageName, blobHandle,
111                 revocableFileDescriptor);
112         return revocableFileDescriptor.getRevocableFileDescriptor();
113     }
114 
115     /**
116      * Wraps the provided {@link ParcelFileDescriptor} into a specific type of
117      * {@link AppSearchRevocableFileDescriptor}.
118      */
wrapToRevocableFileDescriptor( @onNull ParcelFileDescriptor parcelFileDescriptor, int mode)119     protected abstract @NonNull AppSearchRevocableFileDescriptor wrapToRevocableFileDescriptor(
120             @NonNull ParcelFileDescriptor parcelFileDescriptor,
121             int mode) throws IOException;
122 
123     /**
124      * Gets the opened revocable file descriptor for write associated with the given
125      * {@link AppSearchBlobHandle}.
126      *
127      * @param packageName The package name associated with the file descriptor.
128      * @param blobHandle The blob handle associated with the file descriptor.
129      * @return The opened revocable file descriptor, or {@code null} if not found.
130      */
getOpenedRevocableFileDescriptorForWrite( @onNull String packageName, @NonNull AppSearchBlobHandle blobHandle)131     public @Nullable ParcelFileDescriptor getOpenedRevocableFileDescriptorForWrite(
132             @NonNull String packageName,
133             @NonNull AppSearchBlobHandle blobHandle) throws IOException {
134         synchronized (mLock) {
135             Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForPackage =
136                     mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
137             if (rfdsForPackage == null) {
138                 return null;
139             }
140             AppSearchRevocableFileDescriptor revocableFileDescriptor =
141                     rfdsForPackage.get(blobHandle);
142             if (revocableFileDescriptor == null) {
143                 return null;
144             }
145             if (!revocableFileDescriptor.getRevocableFileDescriptor().getFileDescriptor().valid()) {
146                 // In Android T and below, even if the sent file descriptor is closed, the resource
147                 // won't be released immediately. We should revoke now and recreate new pfd.
148                 revocableFileDescriptor.revoke();
149                 rfdsForPackage.remove(blobHandle);
150                 return null;
151             }
152             // The revocableFileDescriptor should never be revoked, otherwise it should be removed
153             // from the map.
154             return revocableFileDescriptor.getRevocableFileDescriptor();
155         }
156     }
157 
158     /**
159      * Revokes all revocable file descriptors previously issued by the store.
160      * After calling this method, any access to these file descriptors will fail.
161      *
162      * @throws IOException If an I/O error occurs while revoking file descriptors.
163      */
revokeAll()164     public void revokeAll() throws IOException {
165         synchronized (mLock) {
166             Set<String> packageNames =
167                     new ArraySet<>(mSentRevocableFileDescriptorsForReadLocked.keySet());
168             packageNames.addAll(mSentRevocableFileDescriptorsForWriteLocked.keySet());
169             for (String packageName : packageNames) {
170                 revokeForPackage(packageName);
171             }
172         }
173     }
174 
175     /**
176      * Revokes all revocable file descriptors for a specified package.
177      * Only file descriptors associated with the given package name will be revoked.
178      *
179      * @param packageName The package name whose file descriptors should be revoked.
180      * @throws IOException If an I/O error occurs while revoking file descriptors.
181      */
revokeForPackage(@onNull String packageName)182     public void revokeForPackage(@NonNull String packageName) throws IOException {
183         synchronized (mLock) {
184             List<AppSearchRevocableFileDescriptor> rfdsForRead =
185                     mSentRevocableFileDescriptorsForReadLocked.remove(packageName);
186             if (rfdsForRead != null) {
187                 for (int i = rfdsForRead.size() - 1; i >= 0; i--) {
188                     rfdsForRead.get(i).revoke();
189                 }
190             }
191 
192             Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
193                     mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
194             if (rfdsForWrite != null) {
195                 for (AppSearchRevocableFileDescriptor rfdForWrite : rfdsForWrite.values()) {
196                     rfdForWrite.revoke();
197                 }
198             }
199         }
200     }
201 
202     /**
203      * Revokes the revocable file descriptors for write associated with the given
204      * {@link AppSearchBlobHandle}.
205      *
206      * <p> Once a blob is sealed, we should call this method to revoke the sent file descriptor for
207      * write. Otherwise, the user could keep writing to the committed file.
208      *
209      * @param packageName The package name whose file descriptors should be revoked.
210      * @param blobHandle  The blob handle associated with the file descriptors.
211      * @throws IOException If an I/O error occurs while revoking file descriptors.
212      */
revokeFdForWrite(@onNull String packageName, @NonNull AppSearchBlobHandle blobHandle)213     public void revokeFdForWrite(@NonNull String packageName,
214             @NonNull AppSearchBlobHandle blobHandle) throws IOException {
215         synchronized (mLock) {
216             Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
217                     mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
218             if (rfdsForWrite == null) {
219                 return;
220             }
221             AppSearchRevocableFileDescriptor revocableFileDescriptor =
222                     rfdsForWrite.remove(blobHandle);
223             if (revocableFileDescriptor == null) {
224                 return;
225             }
226             revocableFileDescriptor.revoke();
227             if (rfdsForWrite.isEmpty()) {
228                 mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
229             }
230         }
231     }
232 
233     /**  Checks if the specified package has reached its blob storage limit. */
checkBlobStoreLimit(@onNull String packageName)234     public void checkBlobStoreLimit(@NonNull String packageName) throws AppSearchException {
235         synchronized (mLock) {
236             int totalOpenFdSize = 0;
237             List<AppSearchRevocableFileDescriptor> rfdsForRead =
238                     mSentRevocableFileDescriptorsForReadLocked.get(packageName);
239             if (rfdsForRead != null) {
240                 totalOpenFdSize += rfdsForRead.size();
241             }
242             Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdsForWrite =
243                     mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
244             if (rfdsForWrite != null) {
245                 totalOpenFdSize += rfdsForWrite.size();
246             }
247             if (totalOpenFdSize >= mConfig.getMaxOpenBlobCount()) {
248                 throw new AppSearchException(AppSearchResult.RESULT_OUT_OF_SPACE,
249                         "Package \"" + packageName + "\" exceeded limit of "
250                                 + mConfig.getMaxOpenBlobCount()
251                                 + " opened file descriptors. Some file descriptors "
252                                 + "must be closed to open additional ones.");
253             }
254         }
255     }
256 
257     /**
258      * Sets a close listener to the revocable file descriptor for write.
259      *
260      * <p>The listener will be invoked when the file descriptor is closed.
261      *
262      * @param packageName The package name associated with the file descriptor.
263      * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
264      *                   the mode is READ_WRITE.
265      * @param revocableFileDescriptor The revocable file descriptor to set the listener to.
266      */
setCloseListenerToFd( @onNull String packageName, @Nullable AppSearchBlobHandle blobHandle, @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor)267     private void setCloseListenerToFd(
268             @NonNull String packageName,
269             @Nullable AppSearchBlobHandle blobHandle,
270             @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor) {
271         ParcelFileDescriptor.OnCloseListener closeListener;
272         switch (revocableFileDescriptor.getMode()) {
273             case ParcelFileDescriptor.MODE_READ_ONLY:
274                 closeListener = e -> {
275                     synchronized (mLock) {
276                         List<AppSearchRevocableFileDescriptor> fdsForPackage =
277                                 mSentRevocableFileDescriptorsForReadLocked.get(packageName);
278                         if (fdsForPackage != null) {
279                             fdsForPackage.remove(revocableFileDescriptor);
280                             if (fdsForPackage.isEmpty()) {
281                                 mSentRevocableFileDescriptorsForReadLocked.remove(packageName);
282                             }
283                         }
284                     }
285                 };
286                 break;
287             case ParcelFileDescriptor.MODE_READ_WRITE:
288                 Preconditions.checkNotNull(blobHandle);
289                 closeListener = e -> {
290                     synchronized (mLock) {
291                         Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor>
292                                 rfdsForPackage = mSentRevocableFileDescriptorsForWriteLocked
293                                 .get(packageName);
294                         if (rfdsForPackage != null) {
295                             AppSearchRevocableFileDescriptor rfd =
296                                     rfdsForPackage.get(blobHandle);
297                             if (rfd == revocableFileDescriptor) {
298                                 // In Platform, this close listener will be only called
299                                 // when the resource of sent pfd is released in the sdk
300                                 // side. In T and below this may happened on a delay.
301                                 // If user re-write with the same blob handle before release
302                                 // the resource, the mSentRevocableFileDescriptorsForWrite
303                                 // will be override by new AppSearchRevocableFileDescriptor.
304                                 // We should only revoke and remove from map if there is no
305                                 // other rfd created for this blob handle.
306                                 try {
307                                     rfdsForPackage.remove(blobHandle);
308                                     rfd.revoke();
309                                 } catch (IOException ioException) {
310                                     // ignore, the sent RevocableFileDescriptor should already
311                                     // be closed.
312                                 }
313                             }
314                             if (rfdsForPackage.isEmpty()) {
315                                 mSentRevocableFileDescriptorsForWriteLocked.remove(packageName);
316                             }
317                         }
318                     }
319                 };
320                 break;
321             default:
322                 throw new UnsupportedOperationException(
323                         "Cannot support the AppSearchRevocableFileDescriptor mode: "
324                                 + revocableFileDescriptor.getMode());
325         }
326         revocableFileDescriptor.setOnCloseListener(closeListener);
327     }
328 
329     /**
330      * Adds a revocable file descriptor to the sent revocable file descriptor map.
331      *
332      * @param packageName The package name associated with the file descriptor.
333      * @param blobHandle The blob handle associated with the file descriptor. It cannot be null if
334      *                   the mode is READ_WRITE.
335      * @param revocableFileDescriptor The revocable file descriptor to add.
336      */
addToSentRevocableFileDescriptorMap( @onNull String packageName, @Nullable AppSearchBlobHandle blobHandle, @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor)337     private void addToSentRevocableFileDescriptorMap(
338             @NonNull String packageName,
339             @Nullable AppSearchBlobHandle blobHandle,
340             @NonNull AppSearchRevocableFileDescriptor revocableFileDescriptor) {
341         synchronized (mLock) {
342             switch (revocableFileDescriptor.getMode()) {
343                 case ParcelFileDescriptor.MODE_READ_ONLY:
344                     List<AppSearchRevocableFileDescriptor> rfdListForPackage =
345                             mSentRevocableFileDescriptorsForReadLocked.get(packageName);
346                     if (rfdListForPackage == null) {
347                         rfdListForPackage = new ArrayList<>();
348                         mSentRevocableFileDescriptorsForReadLocked.put(packageName,
349                                 rfdListForPackage);
350                     }
351                     rfdListForPackage.add(revocableFileDescriptor);
352                     break;
353                 case ParcelFileDescriptor.MODE_READ_WRITE:
354                     Preconditions.checkNotNull(blobHandle);
355                     Map<AppSearchBlobHandle, AppSearchRevocableFileDescriptor> rfdMapForPackage =
356                             mSentRevocableFileDescriptorsForWriteLocked.get(packageName);
357                     if (rfdMapForPackage == null) {
358                         rfdMapForPackage = new ArrayMap<>();
359                         mSentRevocableFileDescriptorsForWriteLocked.put(packageName,
360                                 rfdMapForPackage);
361                     }
362                     rfdMapForPackage.put(blobHandle, revocableFileDescriptor);
363                     break;
364                 default:
365                     throw new UnsupportedOperationException(
366                             "Cannot support the AppSearchRevocableFileDescriptor mode: "
367                                     + revocableFileDescriptor.getMode());
368             }
369         }
370     }
371 }
372