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