Wait for APK to be fully downloaded for full APK digests.

Bug: 160605420
Test: atest ChecksumsTest
Change-Id: Ib9fd591c67290786268b6dcdc57c7db153612e01
diff --git a/core/java/android/os/incremental/IIncrementalService.aidl b/core/java/android/os/incremental/IIncrementalService.aidl
index be8b929..f351c7d 100644
--- a/core/java/android/os/incremental/IIncrementalService.aidl
+++ b/core/java/android/os/incremental/IIncrementalService.aidl
@@ -90,6 +90,14 @@
     int unlink(int storageId, in @utf8InCpp String path);
 
     /**
+     * Checks if a file is fully loaded. File is specified by its path.
+     * 0 - fully loaded
+     * >0 - certain pages missing
+     * <0 - -errcode
+     */
+    int isFileFullyLoaded(int storageId, in @utf8InCpp String path);
+
+    /**
      * Returns overall loading progress of all the files on a storage, progress value between [0,1].
      * Returns a negative value on error.
      */
diff --git a/core/java/android/os/incremental/IncrementalStorage.java b/core/java/android/os/incremental/IncrementalStorage.java
index b8dbfbb..ed386f7 100644
--- a/core/java/android/os/incremental/IncrementalStorage.java
+++ b/core/java/android/os/incremental/IncrementalStorage.java
@@ -304,6 +304,25 @@
     }
 
     /**
+     * Checks whether a file under the current storage directory is fully loaded.
+     *
+     * @param path The relative path of the file.
+     * @return True if the file is fully loaded.
+     */
+    public boolean isFileFullyLoaded(@NonNull String path) throws IOException {
+        try {
+            int res = mService.isFileFullyLoaded(mId, path);
+            if (res < 0) {
+                throw new IOException("isFileFullyLoaded() failed, errno " + -res);
+            }
+            return res == 0;
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return false;
+        }
+    }
+
+    /**
      * Returns the loading progress of a storage
      *
      * @return progress value between [0, 1].
diff --git a/services/core/java/com/android/server/pm/ApkChecksums.java b/services/core/java/com/android/server/pm/ApkChecksums.java
index d8745ab..0338ed8 100644
--- a/services/core/java/com/android/server/pm/ApkChecksums.java
+++ b/services/core/java/com/android/server/pm/ApkChecksums.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.content.pm.PackageManager.EXTRA_CHECKSUMS;
 import static android.content.pm.PackageManager.PARTIAL_MERKLE_ROOT_1M_SHA256;
 import static android.content.pm.PackageManager.PARTIAL_MERKLE_ROOT_1M_SHA512;
 import static android.content.pm.PackageManager.WHOLE_MD5;
@@ -27,11 +28,20 @@
 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
 import android.content.pm.FileChecksum;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageParser;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.incremental.IncrementalManager;
+import android.os.incremental.IncrementalStorage;
 import android.util.ArrayMap;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.apk.ApkSignatureSchemeV2Verifier;
 import android.util.apk.ApkSignatureSchemeV3Verifier;
@@ -43,6 +53,7 @@
 import android.util.apk.SignatureNotFoundException;
 import android.util.apk.VerityBuilder;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.security.VerityUtils;
 
 import java.io.BufferedInputStream;
@@ -72,32 +83,163 @@
     static final String ALGO_SHA512 = "SHA512";
 
     /**
-     * Fetch or calculate checksums for the specific file.
+     * Check back in 1 second after we detected we needed to wait for the APK to be fully available.
+     */
+    private static final long PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS = 1000;
+
+    /**
+     * 24 hours timeout to wait till all files are loaded.
+     */
+    private static final long PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS = 1000 * 3600 * 24;
+
+    /**
+     * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors.
      *
-     * @param split             split name, null for base
-     * @param file              to fetch checksums for
+     * NOTE: All getters should return the same instance for every call.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static class Injector {
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+        interface Producer<T> {
+            /** Produce an instance of type {@link T} */
+            T produce();
+        }
+
+        private final Producer<Context> mContext;
+        private final Producer<Handler> mHandlerProducer;
+        private final Producer<IncrementalManager> mIncrementalManagerProducer;
+
+        Injector(Producer<Context> context, Producer<Handler> handlerProducer,
+                Producer<IncrementalManager> incrementalManagerProducer) {
+            mContext = context;
+            mHandlerProducer = handlerProducer;
+            mIncrementalManagerProducer = incrementalManagerProducer;
+        }
+
+        public Context getContext() {
+            return mContext.produce();
+        }
+
+        public Handler getHandler() {
+            return mHandlerProducer.produce();
+        }
+
+        public IncrementalManager getIncrementalManager() {
+            return mIncrementalManagerProducer.produce();
+        }
+    }
+
+    /**
+     * Fetch or calculate checksums for the collection of files.
+     *
+     * @param filesToChecksum   split name, null for base and File to fetch checksums for
      * @param optional          mask to fetch readily available checksums
      * @param required          mask to forcefully calculate if not available
      * @param trustedInstallers array of certificate to trust, two specific cases:
      *                          null - trust anybody,
      *                          [] - trust nobody.
+     * @param statusReceiver    to receive the resulting checksums
      */
-    public static List<FileChecksum> getFileChecksums(String split, File file,
+    public static void getChecksums(List<Pair<String, File>> filesToChecksum,
             @PackageManager.FileChecksumKind int optional,
             @PackageManager.FileChecksumKind int required,
-            @Nullable Certificate[] trustedInstallers) {
+            @Nullable Certificate[] trustedInstallers,
+            @NonNull IntentSender statusReceiver,
+            @NonNull Injector injector) {
+        List<Map<Integer, FileChecksum>> result = new ArrayList<>(filesToChecksum.size());
+        for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
+            final String split = filesToChecksum.get(i).first;
+            final File file = filesToChecksum.get(i).second;
+            Map<Integer, FileChecksum> checksums = new ArrayMap<>();
+            result.add(checksums);
+
+            try {
+                getAvailableFileChecksums(split, file, optional | required, trustedInstallers,
+                        checksums);
+            } catch (Throwable e) {
+                Slog.e(TAG, "Preferred checksum calculation error", e);
+            }
+        }
+
+        long startTime = SystemClock.uptimeMillis();
+        processRequiredChecksums(filesToChecksum, result, required, statusReceiver, injector,
+                startTime);
+    }
+
+    private static void processRequiredChecksums(List<Pair<String, File>> filesToChecksum,
+            List<Map<Integer, FileChecksum>> result,
+            @PackageManager.FileChecksumKind int required,
+            @NonNull IntentSender statusReceiver,
+            @NonNull Injector injector,
+            long startTime) {
+        final boolean timeout =
+                SystemClock.uptimeMillis() - startTime >= PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS;
+        List<FileChecksum> allChecksums = new ArrayList<>();
+        for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
+            final String split = filesToChecksum.get(i).first;
+            final File file = filesToChecksum.get(i).second;
+            Map<Integer, FileChecksum> checksums = result.get(i);
+
+            try {
+                if (!timeout || required != 0) {
+                    if (needToWait(file, required, checksums, injector)) {
+                        // Not ready, come back later.
+                        injector.getHandler().postDelayed(() -> {
+                            processRequiredChecksums(filesToChecksum, result, required,
+                                    statusReceiver, injector, startTime);
+                        }, PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS);
+                        return;
+                    }
+
+                    getRequiredFileChecksums(split, file, required, checksums);
+                }
+                allChecksums.addAll(checksums.values());
+            } catch (Throwable e) {
+                Slog.e(TAG, "Required checksum calculation error", e);
+            }
+        }
+
+        final Intent intent = new Intent();
+        intent.putExtra(EXTRA_CHECKSUMS,
+                allChecksums.toArray(new FileChecksum[allChecksums.size()]));
+
+        try {
+            statusReceiver.sendIntent(injector.getContext(), 1, intent, null, null);
+        } catch (IntentSender.SendIntentException e) {
+            Slog.w(TAG, e);
+        }
+    }
+
+    /**
+     * Fetch readily available checksums - enforced by kernel or provided by Installer.
+     *
+     * @param split             split name, null for base
+     * @param file              to fetch checksums for
+     * @param kinds             mask to fetch checksums
+     * @param trustedInstallers array of certificate to trust, two specific cases:
+     *                          null - trust anybody,
+     *                          [] - trust nobody.
+     * @param checksums         resulting checksums
+     */
+    private static void getAvailableFileChecksums(String split, File file,
+            @PackageManager.FileChecksumKind int kinds,
+            @Nullable Certificate[] trustedInstallers,
+            Map<Integer, FileChecksum> checksums) {
         final String filePath = file.getAbsolutePath();
-        Map<Integer, FileChecksum> checksums = new ArrayMap<>();
-        final int kinds = (optional | required);
-        // System enforced: FSI or v2/v3/v4 signatures.
-        if ((kinds & WHOLE_MERKLE_ROOT_4K_SHA256) != 0) {
+
+        // Always available: FSI or IncFs.
+        if (isRequired(WHOLE_MERKLE_ROOT_4K_SHA256, kinds, checksums)) {
             // Hashes in fs-verity and IncFS are always verified.
             FileChecksum checksum = extractHashFromFS(split, filePath);
             if (checksum != null) {
                 checksums.put(checksum.getKind(), checksum);
             }
         }
-        if ((kinds & (PARTIAL_MERKLE_ROOT_1M_SHA256 | PARTIAL_MERKLE_ROOT_1M_SHA512)) != 0) {
+
+        // System enforced: v2/v3.
+        if (isRequired(PARTIAL_MERKLE_ROOT_1M_SHA256, kinds, checksums) || isRequired(
+                PARTIAL_MERKLE_ROOT_1M_SHA512, kinds, checksums)) {
             Map<Integer, FileChecksum> v2v3checksums = extractHashFromV2V3Signature(
                     split, filePath, kinds);
             if (v2v3checksums != null) {
@@ -106,11 +248,58 @@
         }
 
         // TODO(b/160605420): Installer provided.
-        // TODO(b/160605420): Wait for Incremental to be fully loaded.
+    }
+
+    /**
+     * Whether the file is available for checksumming or we need to wait.
+     */
+    private static boolean needToWait(File file,
+            @PackageManager.FileChecksumKind int kinds,
+            Map<Integer, FileChecksum> checksums,
+            @NonNull Injector injector) throws IOException {
+        if (!isRequired(WHOLE_MERKLE_ROOT_4K_SHA256, kinds, checksums)
+                && !isRequired(WHOLE_MD5, kinds, checksums)
+                && !isRequired(WHOLE_SHA1, kinds, checksums)
+                && !isRequired(WHOLE_SHA256, kinds, checksums)
+                && !isRequired(WHOLE_SHA512, kinds, checksums)
+                && !isRequired(PARTIAL_MERKLE_ROOT_1M_SHA256, kinds, checksums)
+                && !isRequired(PARTIAL_MERKLE_ROOT_1M_SHA512, kinds, checksums)) {
+            return false;
+        }
+
+        final String filePath = file.getAbsolutePath();
+        if (!IncrementalManager.isIncrementalPath(filePath)) {
+            return false;
+        }
+
+        IncrementalManager manager = injector.getIncrementalManager();
+        if (manager == null) {
+            throw new IllegalStateException("IncrementalManager is missing.");
+        }
+        IncrementalStorage storage = manager.openStorage(filePath);
+        if (storage == null) {
+            throw new IllegalStateException(
+                    "IncrementalStorage is missing for a path on IncFs: " + filePath);
+        }
+
+        return !storage.isFileFullyLoaded(filePath);
+    }
+
+    /**
+     * Fetch or calculate checksums for the specific file.
+     *
+     * @param split     split name, null for base
+     * @param file      to fetch checksums for
+     * @param kinds     mask to forcefully calculate if not available
+     * @param checksums resulting checksums
+     */
+    private static void getRequiredFileChecksums(String split, File file,
+            @PackageManager.FileChecksumKind int kinds,
+            Map<Integer, FileChecksum> checksums) {
+        final String filePath = file.getAbsolutePath();
 
         // Manually calculating required checksums if not readily available.
-        if ((required & WHOLE_MERKLE_ROOT_4K_SHA256) != 0 && !checksums.containsKey(
-                WHOLE_MERKLE_ROOT_4K_SHA256)) {
+        if (isRequired(WHOLE_MERKLE_ROOT_4K_SHA256, kinds, checksums)) {
             try {
                 byte[] generatedRootHash = VerityBuilder.generateFsVerityRootHash(
                         filePath, /*salt=*/null,
@@ -127,14 +316,23 @@
             }
         }
 
-        calculateChecksumIfRequested(checksums, split, file, required, WHOLE_MD5);
-        calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA1);
-        calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA256);
-        calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA512);
+        calculateChecksumIfRequested(checksums, split, file, kinds, WHOLE_MD5);
+        calculateChecksumIfRequested(checksums, split, file, kinds, WHOLE_SHA1);
+        calculateChecksumIfRequested(checksums, split, file, kinds, WHOLE_SHA256);
+        calculateChecksumIfRequested(checksums, split, file, kinds, WHOLE_SHA512);
 
-        calculatePartialChecksumsIfRequested(checksums, split, file, required);
+        calculatePartialChecksumsIfRequested(checksums, split, file, kinds);
+    }
 
-        return new ArrayList<>(checksums.values());
+    private static boolean isRequired(@PackageManager.FileChecksumKind int kind,
+            @PackageManager.FileChecksumKind int kinds, Map<Integer, FileChecksum> checksums) {
+        if ((kinds & kind) == 0) {
+            return false;
+        }
+        if (checksums.containsKey(kind)) {
+            return false;
+        }
+        return true;
     }
 
     private static FileChecksum extractHashFromFS(String split, String filePath) {
@@ -170,7 +368,9 @@
                     PackageParser.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2,
                     false).contentDigests;
         } catch (PackageParser.PackageParserException e) {
-            Slog.e(TAG, "Signature verification error", e);
+            if (!(e.getCause() instanceof SignatureNotFoundException)) {
+                Slog.e(TAG, "Signature verification error", e);
+            }
         }
 
         if (contentDigests == null) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 344f9cf..ca97f31 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -40,7 +40,6 @@
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-import static android.content.pm.PackageManager.EXTRA_CHECKSUMS;
 import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED;
@@ -169,7 +168,6 @@
 import android.content.pm.DataLoaderType;
 import android.content.pm.FallbackCategoryProvider;
 import android.content.pm.FeatureInfo;
-import android.content.pm.FileChecksum;
 import android.content.pm.IDexModuleRegisterCallback;
 import android.content.pm.IPackageChangeObserver;
 import android.content.pm.IPackageDataObserver;
@@ -933,6 +931,7 @@
         private final Object mLock;
         private final Installer mInstaller;
         private final Object mInstallLock;
+        private final Handler mBackgroundHandler;
         private final Executor mBackgroundExecutor;
 
         // ----- producers -----
@@ -955,7 +954,7 @@
 
         Injector(Context context, Object lock, Installer installer,
                 Object installLock, PackageAbiHelper abiHelper,
-                Executor backgroundExecutor,
+                Handler backgroundHandler,
                 Producer<ComponentResolver> componentResolverProducer,
                 Producer<PermissionManagerServiceInternal> permissionManagerProducer,
                 Producer<UserManagerService> userManagerProducer,
@@ -977,7 +976,8 @@
             mInstaller = installer;
             mAbiHelper = abiHelper;
             mInstallLock = installLock;
-            mBackgroundExecutor = backgroundExecutor;
+            mBackgroundHandler = backgroundHandler;
+            mBackgroundExecutor = new HandlerExecutor(backgroundHandler);
             mComponentResolverProducer = new Singleton<>(componentResolverProducer);
             mPermissionManagerProducer = new Singleton<>(permissionManagerProducer);
             mUserManagerProducer = new Singleton<>(userManagerProducer);
@@ -1092,6 +1092,10 @@
             return mPlatformCompatProducer.get(this, mPackageManager);
         }
 
+        public Handler getBackgroundHandler() {
+            return mBackgroundHandler;
+        }
+
         public Executor getBackgroundExecutor() {
             return mBackgroundExecutor;
         }
@@ -2489,29 +2493,14 @@
 
         final Certificate[] trustedCerts = (trustedInstallers != null) ? decodeCertificates(
                 trustedInstallers) : null;
-        final Context context = mContext;
 
         mInjector.getBackgroundExecutor().execute(() -> {
-            final Intent intent = new Intent();
-            List<FileChecksum> result = new ArrayList<>();
-            for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
-                final String split = filesToChecksum.get(i).first;
-                final File file = filesToChecksum.get(i).second;
-                try {
-                    result.addAll(ApkChecksums.getFileChecksums(split, file, optional, required,
-                            trustedCerts));
-                } catch (Throwable e) {
-                    Slog.e(TAG, "Checksum calculation error", e);
-                }
-            }
-            intent.putExtra(EXTRA_CHECKSUMS,
-                    result.toArray(new FileChecksum[result.size()]));
-
-            try {
-                statusReceiver.sendIntent(context, 1, intent, null, null);
-            } catch (SendIntentException e) {
-                Slog.w(TAG, e);
-            }
+            ApkChecksums.Injector injector = new ApkChecksums.Injector(
+                    () -> mContext,
+                    () -> mInjector.getBackgroundHandler(),
+                    () -> mContext.getSystemService(IncrementalManager.class));
+            ApkChecksums.getChecksums(filesToChecksum, optional, required, trustedCerts,
+                    statusReceiver, injector);
         });
     }
 
@@ -2684,7 +2673,7 @@
 
         Injector injector = new Injector(
                 context, lock, installer, installLock, new PackageAbiHelperImpl(),
-                new HandlerExecutor(backgroundHandler),
+                backgroundHandler,
                 (i, pm) ->
                         new ComponentResolver(i.getUserManagerService(), pm.mPmInternal, lock),
                 (i, pm) ->
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index dbe96e6..4858682 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -33,6 +33,9 @@
       "name": "CtsContentTestCases",
       "options": [
         {
+          "include-filter": "android.content.pm.cts.ChecksumsTest"
+        },
+        {
           "include-filter": "android.content.pm.cts.PackageManagerShellCommandTest"
         },
         {
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index 41945a2..87ae4d7 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -237,6 +237,13 @@
     return ok();
 }
 
+binder::Status BinderIncrementalService::isFileFullyLoaded(int32_t storageId,
+                                                           const std::string& path,
+                                                           int32_t* _aidl_return) {
+    *_aidl_return = mImpl.isFileFullyLoaded(storageId, path);
+    return ok();
+}
+
 binder::Status BinderIncrementalService::getLoadingProgress(int32_t storageId,
                                                             float* _aidl_return) {
     *_aidl_return = mImpl.getLoadingProgress(storageId);
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index 8b40350..8478142 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -66,6 +66,8 @@
                             int32_t destStorageId, const std::string& destPath,
                             int32_t* _aidl_return) final;
     binder::Status unlink(int32_t storageId, const std::string& path, int32_t* _aidl_return) final;
+    binder::Status isFileFullyLoaded(int32_t storageId, const std::string& path,
+                                     int32_t* _aidl_return) final;
     binder::Status getLoadingProgress(int32_t storageId, float* _aidl_return) final;
     binder::Status getMetadataByPath(int32_t storageId, const std::string& path,
                                      std::vector<uint8_t>* _aidl_return) final;
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 9836262e..447ee55 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -1603,7 +1603,8 @@
 
     const auto writeFd = mIncFs->openForSpecialOps(ifs->control, libFileId);
     if (!writeFd.ok()) {
-        LOG(ERROR) << "Failed to open write fd for: " << targetLibPath << " errno: " << writeFd;
+        LOG(ERROR) << "Failed to open write fd for: " << targetLibPath
+                   << " errno: " << writeFd.get();
         return;
     }
 
@@ -1673,6 +1674,37 @@
     return mRunning;
 }
 
+int IncrementalService::isFileFullyLoaded(StorageId storage, const std::string& path) const {
+    std::unique_lock l(mLock);
+    const auto ifs = getIfsLocked(storage);
+    if (!ifs) {
+        LOG(ERROR) << "isFileFullyLoaded failed, invalid storageId: " << storage;
+        return -EINVAL;
+    }
+    const auto storageInfo = ifs->storages.find(storage);
+    if (storageInfo == ifs->storages.end()) {
+        LOG(ERROR) << "isFileFullyLoaded failed, no storage: " << storage;
+        return -EINVAL;
+    }
+    l.unlock();
+    return isFileFullyLoadedFromPath(*ifs, path);
+}
+
+int IncrementalService::isFileFullyLoadedFromPath(const IncFsMount& ifs,
+                                                  std::string_view filePath) const {
+    const auto [filledBlocks, totalBlocks] = mIncFs->countFilledBlocks(ifs.control, filePath);
+    if (filledBlocks < 0) {
+        LOG(ERROR) << "isFileFullyLoadedFromPath failed to get filled blocks count for: "
+                   << filePath << " errno: " << filledBlocks;
+        return filledBlocks;
+    }
+    if (totalBlocks < filledBlocks) {
+        LOG(ERROR) << "isFileFullyLoadedFromPath failed to get total num of blocks";
+        return -EINVAL;
+    }
+    return totalBlocks - filledBlocks;
+}
+
 float IncrementalService::getLoadingProgress(StorageId storage) const {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storage);
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index cd6bfed..267458d 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -132,6 +132,7 @@
              std::string_view newPath);
     int unlink(StorageId storage, std::string_view path);
 
+    int isFileFullyLoaded(StorageId storage, const std::string& path) const;
     float getLoadingProgress(StorageId storage) const;
 
     RawMetadata getMetadata(StorageId storage, std::string_view path) const;
@@ -339,6 +340,7 @@
     int makeDirs(const IncFsMount& ifs, StorageId storageId, std::string_view path, int mode);
     binder::Status applyStorageParams(IncFsMount& ifs, bool enableReadLogs);
 
+    int isFileFullyLoadedFromPath(const IncFsMount& ifs, std::string_view filePath) const;
     float getLoadingProgressFromPath(const IncFsMount& ifs, std::string_view path) const;
 
     void registerAppOpsCallback(const std::string& packageName);
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index 1ed46c4..f6d89c5 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -195,8 +195,8 @@
     ErrorCode unlink(const Control& control, std::string_view path) const final {
         return incfs::unlink(control, path);
     }
-    base::unique_fd openForSpecialOps(const Control& control, FileId id) const final {
-        return base::unique_fd{incfs::openForSpecialOps(control, id).release()};
+    incfs::UniqueFd openForSpecialOps(const Control& control, FileId id) const final {
+        return incfs::openForSpecialOps(control, id);
     }
     ErrorCode writeBlocks(std::span<const incfs::DataBlock> blocks) const final {
         return incfs::writeBlocks({blocks.data(), size_t(blocks.size())});
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index 82a1704..6376d86 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -74,6 +74,7 @@
     using Control = incfs::Control;
     using FileId = incfs::FileId;
     using ErrorCode = incfs::ErrorCode;
+    using UniqueFd = incfs::UniqueFd;
     using WaitResult = incfs::WaitResult;
 
     using ExistingMountCallback =
@@ -96,7 +97,7 @@
     virtual ErrorCode link(const Control& control, std::string_view from,
                            std::string_view to) const = 0;
     virtual ErrorCode unlink(const Control& control, std::string_view path) const = 0;
-    virtual base::unique_fd openForSpecialOps(const Control& control, FileId id) const = 0;
+    virtual UniqueFd openForSpecialOps(const Control& control, FileId id) const = 0;
     virtual ErrorCode writeBlocks(std::span<const incfs::DataBlock> blocks) const = 0;
     virtual WaitResult waitForPendingReads(
             const Control& control, std::chrono::milliseconds timeout,
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index d1000e56..a290a17 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -289,7 +289,7 @@
                        ErrorCode(const Control& control, std::string_view from,
                                  std::string_view to));
     MOCK_CONST_METHOD2(unlink, ErrorCode(const Control& control, std::string_view path));
-    MOCK_CONST_METHOD2(openForSpecialOps, base::unique_fd(const Control& control, FileId id));
+    MOCK_CONST_METHOD2(openForSpecialOps, UniqueFd(const Control& control, FileId id));
     MOCK_CONST_METHOD1(writeBlocks, ErrorCode(std::span<const DataBlock> blocks));
     MOCK_CONST_METHOD3(waitForPendingReads,
                        WaitResult(const Control& control, std::chrono::milliseconds timeout,
@@ -304,6 +304,10 @@
         ON_CALL(*this, countFilledBlocks(_, _)).WillByDefault(Return(std::make_pair(1, 2)));
     }
 
+    void countFilledBlocksFullyLoaded() {
+        ON_CALL(*this, countFilledBlocks(_, _)).WillByDefault(Return(std::make_pair(10000, 10000)));
+    }
+
     void countFilledBlocksFails() {
         ON_CALL(*this, countFilledBlocks(_, _)).WillByDefault(Return(std::make_pair(-1, -1)));
     }
@@ -1069,6 +1073,53 @@
     ASSERT_EQ(res, 0);
 }
 
+TEST_F(IncrementalServiceTest, testIsFileFullyLoadedFailsWithNoFile) {
+    mIncFs->countFilledBlocksFails();
+    mFs->hasNoFile();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    ASSERT_EQ(-1, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
+}
+
+TEST_F(IncrementalServiceTest, testIsFileFullyLoadedFailsWithFailedRanges) {
+    mIncFs->countFilledBlocksFails();
+    mFs->hasFiles();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
+    ASSERT_EQ(-1, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
+}
+
+TEST_F(IncrementalServiceTest, testIsFileFullyLoadedSuccessWithEmptyRanges) {
+    mIncFs->countFilledBlocksEmpty();
+    mFs->hasFiles();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
+    ASSERT_EQ(0, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
+}
+
+TEST_F(IncrementalServiceTest, testIsFileFullyLoadedSuccess) {
+    mIncFs->countFilledBlocksFullyLoaded();
+    mFs->hasFiles();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
+    ASSERT_EQ(0, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
+}
+
 TEST_F(IncrementalServiceTest, testGetLoadingProgressSuccessWithNoFile) {
     mIncFs->countFilledBlocksSuccess();
     mFs->hasNoFile();