Per package read timeouts.

Bug: 162345970
Test: atest PackageManagerShellCommandTest PackageManagerShellCommandIncrementalTest IncrementalServiceTest PackageManagerServiceTest

Change-Id: I2599db1ed8827fff16387c11254a5d607f27ea46
diff --git a/services/core/java/com/android/server/pm/DumpState.java b/services/core/java/com/android/server/pm/DumpState.java
index 520871f..4f986bd 100644
--- a/services/core/java/com/android/server/pm/DumpState.java
+++ b/services/core/java/com/android/server/pm/DumpState.java
@@ -44,6 +44,7 @@
     public static final int DUMP_APEX = 1 << 25;
     public static final int DUMP_QUERIES = 1 << 26;
     public static final int DUMP_KNOWN_PACKAGES = 1 << 27;
+    public static final int DUMP_PER_UID_READ_TIMEOUTS = 1 << 28;
 
     public static final int OPTION_SHOW_FILTERS = 1 << 0;
     public static final int OPTION_DUMP_ALL_COMPONENTS = 1 << 1;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f97a5ee..3d04b56 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -111,6 +111,7 @@
 import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.IncrementalFileStorages;
 import android.os.incremental.IncrementalManager;
+import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.StorageManager;
 import android.provider.Settings.Secure;
@@ -3006,7 +3007,7 @@
         final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID);
         if (isInstallerShell && isIncrementalInstallation() && mIncrementalFileStorages != null) {
             if (!packageLite.debuggable && !packageLite.profilableByShell) {
-                mIncrementalFileStorages.disableReadLogs();
+                mIncrementalFileStorages.disallowReadLogs();
             }
         }
     }
@@ -3720,12 +3721,16 @@
         };
 
         if (!manualStartAndDestroy) {
+            final PerUidReadTimeouts[] perUidReadTimeouts = mPm.getPerUidReadTimeouts();
+
             final StorageHealthCheckParams healthCheckParams = new StorageHealthCheckParams();
             healthCheckParams.blockedTimeoutMs = INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS;
             healthCheckParams.unhealthyTimeoutMs = INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS;
             healthCheckParams.unhealthyMonitoringMs = INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS;
+
             final boolean systemDataLoader =
                     params.getComponentName().getPackageName() == SYSTEM_DATA_LOADER_PACKAGE;
+
             final IStorageHealthListener healthListener = new IStorageHealthListener.Stub() {
                 @Override
                 public void onHealthStatus(int storageId, int status) {
@@ -3760,7 +3765,8 @@
 
             try {
                 mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext, stageDir,
-                        params, statusListener, healthCheckParams, healthListener, addedFiles);
+                        params, statusListener, healthCheckParams, healthListener, addedFiles,
+                        perUidReadTimeouts);
                 return false;
             } catch (IOException e) {
                 throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(),
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 27008d8..4467b51 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -107,6 +107,7 @@
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
+import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
@@ -287,6 +288,7 @@
 import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.IncrementalStorage;
+import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.DiskInfo;
 import android.os.storage.IStorageManager;
@@ -512,6 +514,7 @@
     public static final boolean DEBUG_COMPRESSION = Build.IS_DEBUGGABLE;
     public static final boolean DEBUG_CACHES = false;
     public static final boolean TRACE_CACHES = false;
+    private static final boolean DEBUG_PER_UID_READ_TIMEOUTS = false;
 
     // Debug output for dexopting. This is shared between PackageManagerService, OtaDexoptService
     // and PackageDexOptimizer. All these classes have their own flag to allow switching a single
@@ -647,6 +650,24 @@
     private static final long DEFAULT_ENABLE_ROLLBACK_TIMEOUT_MILLIS = 10 * 1000;
 
     /**
+     * Default IncFs timeouts. Maximum values in IncFs is 1hr.
+     *
+     * <p>If flag value is empty, the default value will be assigned.
+     *
+     * Flag type: {@code String}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_INCFS_DEFAULT_TIMEOUTS = "incfs_default_timeouts";
+
+    /**
+     * Known digesters with optional timeouts.
+     *
+     * Flag type: {@code String}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list";
+
+    /**
      * The default response for package verification timeout.
      *
      * This can be either PackageManager.VERIFICATION_ALLOW or
@@ -909,6 +930,11 @@
     final private ArrayList<IPackageChangeObserver> mPackageChangeObservers =
         new ArrayList<>();
 
+    // Cached parsed flag value. Invalidated on each flag change.
+    private PerUidReadTimeouts[] mPerUidReadTimeoutsCache;
+
+    private static final PerUidReadTimeouts[] EMPTY_PER_UID_READ_TIMEOUTS_ARRAY = {};
+
     /**
      * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors.
      *
@@ -23738,6 +23764,17 @@
         mInstallerService.restoreAndApplyStagedSessionIfNeeded();
 
         mExistingPackages = null;
+
+        // Clear cache on flags changes.
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_PACKAGE_MANAGER_SERVICE, FgThread.getExecutor(),
+                properties -> {
+                    final Set<String> keyset = properties.getKeyset();
+                    if (keyset.contains(PROPERTY_INCFS_DEFAULT_TIMEOUTS) || keyset.contains(
+                            PROPERTY_KNOWN_DIGESTERS_LIST)) {
+                        mPerUidReadTimeoutsCache = null;
+                    }
+                });
     }
 
     public void waitForAppDataPrepared() {
@@ -23828,6 +23865,7 @@
                 pw.println("    v[erifiers]: print package verifier info");
                 pw.println("    d[omain-preferred-apps]: print domains preferred apps");
                 pw.println("    i[ntent-filter-verifiers]|ifv: print intent filter verifier info");
+                pw.println("    t[imeouts]: print read timeouts for known digesters");
                 pw.println("    version: print database version info");
                 pw.println("    write: write current settings now");
                 pw.println("    installs: details about install sessions");
@@ -23982,6 +24020,8 @@
                 dumpState.setDump(DumpState.DUMP_SERVICE_PERMISSIONS);
             } else if ("known-packages".equals(cmd)) {
                 dumpState.setDump(DumpState.DUMP_KNOWN_PACKAGES);
+            } else if ("t".equals(cmd) || "timeouts".equals(cmd)) {
+                dumpState.setDump(DumpState.DUMP_PER_UID_READ_TIMEOUTS);
             } else if ("write".equals(cmd)) {
                 synchronized (mLock) {
                     writeSettingsLPrTEMP();
@@ -24380,6 +24420,25 @@
         if (!checkin && dumpState.isDumping(DumpState.DUMP_APEX)) {
             mApexManager.dump(pw, packageName);
         }
+
+        if (!checkin && dumpState.isDumping(DumpState.DUMP_PER_UID_READ_TIMEOUTS)
+                && packageName == null) {
+            pw.println();
+            pw.println("Per UID read timeouts:");
+            pw.println("    Default timeouts flag: " + getDefaultTimeouts());
+            pw.println("    Known digesters list flag: " + getKnownDigestersList());
+
+            PerUidReadTimeouts[] items = getPerUidReadTimeouts();
+            pw.println("    Timeouts (" + items.length + "):");
+            for (PerUidReadTimeouts item : items) {
+                pw.print("        (");
+                pw.print("uid=" + item.uid + ", ");
+                pw.print("minTimeUs=" + item.minTimeUs + ", ");
+                pw.print("minPendingTimeUs=" + item.minPendingTimeUs + ", ");
+                pw.print("maxPendingTimeUs=" + item.maxPendingTimeUs);
+                pw.println(")");
+            }
+        }
     }
 
     //TODO: b/111402650
@@ -27967,6 +28026,88 @@
             SystemClock.sleep(durationMs);
         }
     }
+
+    private static String getDefaultTimeouts() {
+        return DeviceConfig.getString(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                PROPERTY_INCFS_DEFAULT_TIMEOUTS, "");
+    }
+
+    private static String getKnownDigestersList() {
+        return DeviceConfig.getString(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                PROPERTY_KNOWN_DIGESTERS_LIST, "");
+    }
+
+    /**
+     * Returns the array containing per-uid timeout configuration.
+     * This is derived from DeviceConfig flags.
+     */
+    public @NonNull PerUidReadTimeouts[] getPerUidReadTimeouts() {
+        PerUidReadTimeouts[] result = mPerUidReadTimeoutsCache;
+        if (result == null) {
+            result = parsePerUidReadTimeouts();
+            mPerUidReadTimeoutsCache = result;
+        }
+        return result;
+    }
+
+    private @NonNull PerUidReadTimeouts[] parsePerUidReadTimeouts() {
+        final String defaultTimeouts = getDefaultTimeouts();
+        final String knownDigestersList = getKnownDigestersList();
+        final List<PerPackageReadTimeouts> perPackageReadTimeouts =
+                PerPackageReadTimeouts.parseDigestersList(defaultTimeouts, knownDigestersList);
+
+        if (perPackageReadTimeouts.size() == 0) {
+            return EMPTY_PER_UID_READ_TIMEOUTS_ARRAY;
+        }
+
+        final int[] allUsers = mInjector.getUserManagerService().getUserIds();
+
+        List<PerUidReadTimeouts> result = new ArrayList<>(perPackageReadTimeouts.size());
+        synchronized (mLock) {
+            for (int i = 0, size = perPackageReadTimeouts.size(); i < size; ++i) {
+                final PerPackageReadTimeouts perPackage = perPackageReadTimeouts.get(i);
+                final PackageSetting ps = mSettings.mPackages.get(perPackage.packageName);
+                if (ps == null) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: package not found = "
+                                + perPackage.packageName);
+                    }
+                    continue;
+                }
+                final AndroidPackage pkg = ps.getPkg();
+                if (pkg.getLongVersionCode() < perPackage.versionCodes.minVersionCode
+                        || pkg.getLongVersionCode() > perPackage.versionCodes.maxVersionCode) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: version code is not in range = "
+                                + perPackage.packageName + ":" + pkg.getLongVersionCode());
+                    }
+                    continue;
+                }
+                if (perPackage.sha256certificate != null
+                        && !pkg.getSigningDetails().hasSha256Certificate(
+                        perPackage.sha256certificate)) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: invalid certificate = "
+                                + perPackage.packageName + ":" + pkg.getLongVersionCode());
+                    }
+                    continue;
+                }
+                for (int userId : allUsers) {
+                    if (!ps.getInstalled(userId)) {
+                        continue;
+                    }
+                    final int uid = UserHandle.getUid(userId, ps.appId);
+                    final PerUidReadTimeouts perUid = new PerUidReadTimeouts();
+                    perUid.uid = uid;
+                    perUid.minTimeUs = perPackage.timeouts.minTimeUs;
+                    perUid.minPendingTimeUs = perPackage.timeouts.minPendingTimeUs;
+                    perUid.maxPendingTimeUs = perPackage.timeouts.maxPendingTimeUs;
+                    result.add(perUid);
+                }
+            }
+        }
+        return result.toArray(new PerUidReadTimeouts[result.size()]);
+    }
 }
 
 interface PackageSender {
diff --git a/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
new file mode 100644
index 0000000..3b306a8
--- /dev/null
+++ b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;;
+import android.text.TextUtils;
+
+import com.android.internal.util.HexDump;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class PerPackageReadTimeouts {
+    static long tryParseLong(String str, long defaultValue) {
+        try {
+            return Long.parseLong(str);
+        } catch (NumberFormatException nfe) {
+            return defaultValue;
+        }
+    }
+
+    static byte[] tryParseSha256(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return null;
+        }
+        try {
+            return HexDump.hexStringToByteArray(str);
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    static class Timeouts {
+        public final long minTimeUs;
+        public final long minPendingTimeUs;
+        public final long maxPendingTimeUs;
+
+        // 3600000000us == 1hr
+        public static final Timeouts DEFAULT = new Timeouts(3600000000L, 3600000000L, 3600000000L);
+
+        private Timeouts(long minTimeUs, long minPendingTimeUs, long maxPendingTimeUs) {
+            this.minTimeUs = minTimeUs;
+            this.minPendingTimeUs = minPendingTimeUs;
+            this.maxPendingTimeUs = maxPendingTimeUs;
+        }
+
+        static Timeouts parse(String timeouts) {
+            String[] splits = timeouts.split(":", 3);
+            if (splits.length != 3) {
+                return DEFAULT;
+            }
+            final long minTimeUs = tryParseLong(splits[0], DEFAULT.minTimeUs);
+            final long minPendingTimeUs = tryParseLong(splits[1], DEFAULT.minPendingTimeUs);
+            final long maxPendingTimeUs = tryParseLong(splits[2], DEFAULT.maxPendingTimeUs);
+            if (0 <= minTimeUs && minTimeUs <= minPendingTimeUs
+                    && minPendingTimeUs <= maxPendingTimeUs) {
+                // validity check
+                return new Timeouts(minTimeUs, minPendingTimeUs, maxPendingTimeUs);
+            }
+            return DEFAULT;
+        }
+    }
+
+    static class VersionCodes {
+        public final long minVersionCode;
+        public final long maxVersionCode;
+
+        public static final VersionCodes ALL_VERSION_CODES = new VersionCodes(Long.MIN_VALUE,
+                Long.MAX_VALUE);
+
+        private VersionCodes(long minVersionCode, long maxVersionCode) {
+            this.minVersionCode = minVersionCode;
+            this.maxVersionCode = maxVersionCode;
+        }
+
+        static VersionCodes parse(String codes) {
+            if (TextUtils.isEmpty(codes)) {
+                return ALL_VERSION_CODES;
+            }
+            String[] splits = codes.split("-", 2);
+            switch (splits.length) {
+                case 1: {
+                    // single version code
+                    try {
+                        final long versionCode = Long.parseLong(splits[0]);
+                        return new VersionCodes(versionCode, versionCode);
+                    } catch (NumberFormatException nfe) {
+                        return ALL_VERSION_CODES;
+                    }
+                }
+                case 2: {
+                    final long minVersionCode = tryParseLong(splits[0],
+                            ALL_VERSION_CODES.minVersionCode);
+                    final long maxVersionCode = tryParseLong(splits[1],
+                            ALL_VERSION_CODES.maxVersionCode);
+                    if (minVersionCode <= maxVersionCode) {
+                        return new VersionCodes(minVersionCode, maxVersionCode);
+                    }
+                    break;
+                }
+            }
+            return ALL_VERSION_CODES;
+        }
+    }
+
+    public final String packageName;
+    public final byte[] sha256certificate;
+    public final VersionCodes versionCodes;
+    public final Timeouts timeouts;
+
+    private PerPackageReadTimeouts(String packageName, byte[] sha256certificate,
+            VersionCodes versionCodes, Timeouts timeouts) {
+        this.packageName = packageName;
+        this.sha256certificate = sha256certificate;
+        this.versionCodes = versionCodes;
+        this.timeouts = timeouts;
+    }
+
+    @SuppressWarnings("fallthrough")
+    static PerPackageReadTimeouts parse(String timeoutsStr, VersionCodes defaultVersionCodes,
+            Timeouts defaultTimeouts) {
+        String packageName = null;
+        byte[] sha256certificate = null;
+        VersionCodes versionCodes = defaultVersionCodes;
+        Timeouts timeouts = defaultTimeouts;
+
+        final String[] splits = timeoutsStr.split(":", 4);
+        switch (splits.length) {
+            case 4:
+                timeouts = Timeouts.parse(splits[3]);
+                // fall through
+            case 3:
+                versionCodes = VersionCodes.parse(splits[2]);
+                // fall through
+            case 2:
+                sha256certificate = tryParseSha256(splits[1]);
+                // fall through
+            case 1:
+                packageName = splits[0];
+                break;
+            default:
+                return null;
+        }
+        if (TextUtils.isEmpty(packageName)) {
+            return null;
+        }
+
+        return new PerPackageReadTimeouts(packageName, sha256certificate, versionCodes,
+                timeouts);
+    }
+
+    static @NonNull List<PerPackageReadTimeouts> parseDigestersList(String defaultTimeoutsStr,
+            String knownDigestersList) {
+        if (TextUtils.isEmpty(knownDigestersList)) {
+            return Collections.emptyList();
+        }
+
+        final VersionCodes defaultVersionCodes = VersionCodes.ALL_VERSION_CODES;
+        final Timeouts defaultTimeouts = Timeouts.parse(defaultTimeoutsStr);
+
+        String[] packages = knownDigestersList.split(",");
+        List<PerPackageReadTimeouts> result = new ArrayList<>(packages.length);
+        for (int i = 0, size = packages.length; i < size; ++i) {
+            PerPackageReadTimeouts timeouts = PerPackageReadTimeouts.parse(packages[i],
+                    defaultVersionCodes, defaultTimeouts);
+            if (timeouts != null) {
+                result.add(timeouts);
+            }
+        }
+        return result;
+    }
+}
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index a31aac9..d224428 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -122,13 +122,14 @@
         const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
         const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
         const ::android::sp<::android::os::incremental::IStorageHealthListener>& healthListener,
+        const ::std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts,
         int32_t* _aidl_return) {
     *_aidl_return =
             mImpl.createStorage(path, const_cast<content::pm::DataLoaderParamsParcel&&>(params),
                                 android::incremental::IncrementalService::CreateOptions(createMode),
                                 statusListener,
                                 const_cast<StorageHealthCheckParams&&>(healthCheckParams),
-                                healthListener);
+                                healthListener, perUidReadTimeouts);
     return ok();
 }
 
@@ -164,8 +165,8 @@
     return ok();
 }
 
-binder::Status BinderIncrementalService::disableReadLogs(int32_t storageId) {
-    mImpl.disableReadLogs(storageId);
+binder::Status BinderIncrementalService::disallowReadLogs(int32_t storageId) {
+    mImpl.disallowReadLogs(storageId);
     return ok();
 }
 
@@ -254,7 +255,7 @@
 
 binder::Status BinderIncrementalService::getLoadingProgress(int32_t storageId,
                                                             float* _aidl_return) {
-    *_aidl_return = mImpl.getLoadingProgress(storageId);
+    *_aidl_return = mImpl.getLoadingProgress(storageId).getProgress();
     return ok();
 }
 
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index 8afa0f7..9a4537a 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -45,6 +45,7 @@
             const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
             const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
             const ::android::sp<IStorageHealthListener>& healthListener,
+            const ::std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts,
             int32_t* _aidl_return) final;
     binder::Status createLinkedStorage(const std::string& path, int32_t otherStorageId,
                                        int32_t createMode, int32_t* _aidl_return) final;
@@ -77,7 +78,7 @@
                                    std::vector<uint8_t>* _aidl_return) final;
     binder::Status startLoading(int32_t storageId, bool* _aidl_return) final;
     binder::Status deleteStorage(int32_t storageId) final;
-    binder::Status disableReadLogs(int32_t storageId) final;
+    binder::Status disallowReadLogs(int32_t storageId) final;
     binder::Status configureNativeBinaries(int32_t storageId, const std::string& apkFullPath,
                                            const std::string& libDirRelativePath,
                                            const std::string& abi, bool extractNativeLibs,
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index eb6b325..45c9ad9 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -63,6 +63,10 @@
     static constexpr auto libSuffix = ".so"sv;
     static constexpr auto blockSize = 4096;
     static constexpr auto systemPackage = "android"sv;
+
+    static constexpr auto progressUpdateInterval = 1000ms;
+    static constexpr auto perUidTimeoutOffset = progressUpdateInterval * 2;
+    static constexpr auto minPerUidTimeout = progressUpdateInterval * 3;
 };
 
 static const Constants& constants() {
@@ -350,7 +354,8 @@
             dprintf(fd, "    storages (%d): {\n", int(mnt.storages.size()));
             for (auto&& [storageId, storage] : mnt.storages) {
                 dprintf(fd, "      [%d] -> [%s] (%d %% loaded) \n", storageId, storage.name.c_str(),
-                        (int)(getLoadingProgressFromPath(mnt, storage.name.c_str()) * 100));
+                        (int)(getLoadingProgressFromPath(mnt, storage.name.c_str()).getProgress() *
+                              100));
             }
             dprintf(fd, "    }\n");
 
@@ -419,12 +424,11 @@
     }
 }
 
-StorageId IncrementalService::createStorage(std::string_view mountPoint,
-                                            content::pm::DataLoaderParamsParcel&& dataLoaderParams,
-                                            CreateOptions options,
-                                            const DataLoaderStatusListener& statusListener,
-                                            StorageHealthCheckParams&& healthCheckParams,
-                                            const StorageHealthListener& healthListener) {
+StorageId IncrementalService::createStorage(
+        std::string_view mountPoint, content::pm::DataLoaderParamsParcel&& dataLoaderParams,
+        CreateOptions options, const DataLoaderStatusListener& statusListener,
+        StorageHealthCheckParams&& healthCheckParams, const StorageHealthListener& healthListener,
+        const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
     LOG(INFO) << "createStorage: " << mountPoint << " | " << int(options);
     if (!path::isAbsolute(mountPoint)) {
         LOG(ERROR) << "path is not absolute: " << mountPoint;
@@ -553,13 +557,14 @@
     if (auto err = addBindMount(*ifs, storageIt->first, storageIt->second.name,
                                 std::string(storageIt->second.name), std::move(mountNorm), bk, l);
         err < 0) {
-        LOG(ERROR) << "adding bind mount failed: " << -err;
+        LOG(ERROR) << "Adding bind mount failed: " << -err;
         return kInvalidStorageId;
     }
 
     // Done here as well, all data structures are in good state.
     secondCleanupOnFailure.release();
 
+    // DataLoader.
     auto dataLoaderStub = prepareDataLoader(*ifs, std::move(dataLoaderParams), &statusListener,
                                             std::move(healthCheckParams), &healthListener);
     CHECK(dataLoaderStub);
@@ -567,6 +572,11 @@
     mountIt->second = std::move(ifs);
     l.unlock();
 
+    // Per Uid timeouts.
+    if (!perUidReadTimeouts.empty()) {
+        setUidReadTimeouts(mountId, perUidReadTimeouts);
+    }
+
     if (mSystemReady.load(std::memory_order_relaxed) && !dataLoaderStub->requestCreate()) {
         // failed to create data loader
         LOG(ERROR) << "initializeDataLoader() failed";
@@ -634,17 +644,17 @@
     return it->second->second.storage;
 }
 
-void IncrementalService::disableReadLogs(StorageId storageId) {
+void IncrementalService::disallowReadLogs(StorageId storageId) {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storageId);
     if (!ifs) {
-        LOG(ERROR) << "disableReadLogs failed, invalid storageId: " << storageId;
+        LOG(ERROR) << "disallowReadLogs failed, invalid storageId: " << storageId;
         return;
     }
-    if (!ifs->readLogsEnabled()) {
+    if (!ifs->readLogsAllowed()) {
         return;
     }
-    ifs->disableReadLogs();
+    ifs->disallowReadLogs();
     l.unlock();
 
     const auto metadata = constants().readLogsDisabledMarkerName;
@@ -669,7 +679,7 @@
 
     const auto& params = ifs->dataLoaderStub->params();
     if (enableReadLogs) {
-        if (!ifs->readLogsEnabled()) {
+        if (!ifs->readLogsAllowed()) {
             LOG(ERROR) << "setStorageParams failed, readlogs disabled for storageId: " << storageId;
             return -EPERM;
         }
@@ -704,7 +714,12 @@
     }
 
     std::lock_guard l(mMountOperationLock);
-    return mVold->setIncFsMountOptions(control, enableReadLogs);
+    const auto status = mVold->setIncFsMountOptions(control, enableReadLogs);
+    if (status.isOk()) {
+        // Store enabled state.
+        ifs.setReadLogsEnabled(enableReadLogs);
+    }
+    return status;
 }
 
 void IncrementalService::deleteStorage(StorageId storageId) {
@@ -1052,6 +1067,74 @@
     return true;
 }
 
+void IncrementalService::setUidReadTimeouts(
+        StorageId storage, const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    using microseconds = std::chrono::microseconds;
+    using milliseconds = std::chrono::milliseconds;
+
+    auto maxPendingTimeUs = microseconds(0);
+    for (const auto& timeouts : perUidReadTimeouts) {
+        maxPendingTimeUs = std::max(maxPendingTimeUs, microseconds(timeouts.maxPendingTimeUs));
+    }
+    if (maxPendingTimeUs < Constants::minPerUidTimeout) {
+        return;
+    }
+
+    const auto ifs = getIfs(storage);
+    if (!ifs) {
+        return;
+    }
+
+    if (auto err = mIncFs->setUidReadTimeouts(ifs->control, perUidReadTimeouts); err < 0) {
+        LOG(ERROR) << "Setting read timeouts failed: " << -err;
+        return;
+    }
+
+    const auto timeout = std::chrono::duration_cast<milliseconds>(maxPendingTimeUs) -
+            Constants::perUidTimeoutOffset;
+    updateUidReadTimeouts(storage, Clock::now() + timeout);
+}
+
+void IncrementalService::clearUidReadTimeouts(StorageId storage) {
+    const auto ifs = getIfs(storage);
+    if (!ifs) {
+        return;
+    }
+
+    mIncFs->setUidReadTimeouts(ifs->control, {});
+}
+
+void IncrementalService::updateUidReadTimeouts(StorageId storage, Clock::time_point timeLimit) {
+    // Reached maximum timeout.
+    if (Clock::now() >= timeLimit) {
+        return clearUidReadTimeouts(storage);
+    }
+
+    // Still loading?
+    const auto progress = getLoadingProgress(storage);
+    if (progress.isError()) {
+        // Something is wrong, abort.
+        return clearUidReadTimeouts(storage);
+    }
+
+    if (progress.started() && progress.fullyLoaded()) {
+        // Fully loaded, check readLogs collection.
+        const auto ifs = getIfs(storage);
+        if (!ifs->readLogsEnabled()) {
+            return clearUidReadTimeouts(storage);
+        }
+    }
+
+    const auto timeLeft = timeLimit - Clock::now();
+    if (timeLeft < Constants::progressUpdateInterval) {
+        // Don't bother.
+        return clearUidReadTimeouts(storage);
+    }
+
+    addTimedJob(*mTimedQueue, storage, Constants::progressUpdateInterval,
+                [this, storage, timeLimit]() { updateUidReadTimeouts(storage, timeLimit); });
+}
+
 std::unordered_set<std::string_view> IncrementalService::adoptMountedInstances() {
     std::unordered_set<std::string_view> mountedRootNames;
     mIncFs->listExistingMounts([this, &mountedRootNames](auto root, auto backingDir, auto binds) {
@@ -1125,7 +1208,7 @@
 
         // Check if marker file present.
         if (checkReadLogsDisabledMarker(root)) {
-            ifs->disableReadLogs();
+            ifs->disallowReadLogs();
         }
 
         std::vector<std::pair<std::string, metadata::BindPoint>> permanentBindPoints;
@@ -1301,7 +1384,7 @@
 
     // Check if marker file present.
     if (checkReadLogsDisabledMarker(mountTarget)) {
-        ifs->disableReadLogs();
+        ifs->disallowReadLogs();
     }
 
     // DataLoader params
@@ -1705,7 +1788,7 @@
     return 0;
 }
 
-int IncrementalService::isFileFullyLoaded(StorageId storage, const std::string& path) const {
+int IncrementalService::isFileFullyLoaded(StorageId storage, std::string_view filePath) const {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storage);
     if (!ifs) {
@@ -1718,7 +1801,7 @@
         return -EINVAL;
     }
     l.unlock();
-    return isFileFullyLoadedFromPath(*ifs, path);
+    return isFileFullyLoadedFromPath(*ifs, filePath);
 }
 
 int IncrementalService::isFileFullyLoadedFromPath(const IncFsMount& ifs,
@@ -1736,25 +1819,26 @@
     return totalBlocks - filledBlocks;
 }
 
-float IncrementalService::getLoadingProgress(StorageId storage) const {
+IncrementalService::LoadingProgress IncrementalService::getLoadingProgress(
+        StorageId storage) const {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storage);
     if (!ifs) {
         LOG(ERROR) << "getLoadingProgress failed, invalid storageId: " << storage;
-        return -EINVAL;
+        return {-EINVAL, -EINVAL};
     }
     const auto storageInfo = ifs->storages.find(storage);
     if (storageInfo == ifs->storages.end()) {
         LOG(ERROR) << "getLoadingProgress failed, no storage: " << storage;
-        return -EINVAL;
+        return {-EINVAL, -EINVAL};
     }
     l.unlock();
     return getLoadingProgressFromPath(*ifs, storageInfo->second.name);
 }
 
-float IncrementalService::getLoadingProgressFromPath(const IncFsMount& ifs,
-                                                     std::string_view storagePath) const {
-    size_t totalBlocks = 0, filledBlocks = 0;
+IncrementalService::LoadingProgress IncrementalService::getLoadingProgressFromPath(
+        const IncFsMount& ifs, std::string_view storagePath) const {
+    ssize_t totalBlocks = 0, filledBlocks = 0;
     const auto filePaths = mFs->listFilesRecursive(storagePath);
     for (const auto& filePath : filePaths) {
         const auto [filledBlocksCount, totalBlocksCount] =
@@ -1762,33 +1846,29 @@
         if (filledBlocksCount < 0) {
             LOG(ERROR) << "getLoadingProgress failed to get filled blocks count for: " << filePath
                        << " errno: " << filledBlocksCount;
-            return filledBlocksCount;
+            return {filledBlocksCount, filledBlocksCount};
         }
         totalBlocks += totalBlocksCount;
         filledBlocks += filledBlocksCount;
     }
 
-    if (totalBlocks == 0) {
-        // No file in the storage or files are empty; regarded as fully loaded
-        return 1;
-    }
-    return (float)filledBlocks / (float)totalBlocks;
+    return {filledBlocks, totalBlocks};
 }
 
 bool IncrementalService::updateLoadingProgress(
         StorageId storage, const StorageLoadingProgressListener& progressListener) {
     const auto progress = getLoadingProgress(storage);
-    if (progress < 0) {
+    if (progress.isError()) {
         // Failed to get progress from incfs, abort.
         return false;
     }
-    progressListener->onStorageLoadingProgressChanged(storage, progress);
-    if (progress > 1 - 0.001f) {
+    progressListener->onStorageLoadingProgressChanged(storage, progress.getProgress());
+    if (progress.fullyLoaded()) {
         // Stop updating progress once it is fully loaded
         return true;
     }
-    static constexpr auto kProgressUpdateInterval = 1000ms;
-    addTimedJob(*mProgressUpdateJobQueue, storage, kProgressUpdateInterval /* repeat after 1s */,
+    addTimedJob(*mProgressUpdateJobQueue, storage,
+                Constants::progressUpdateInterval /* repeat after 1s */,
                 [storage, progressListener, this]() {
                     updateLoadingProgress(storage, progressListener);
                 });
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index eb69470..3066121 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -23,6 +23,7 @@
 #include <android/os/incremental/BnIncrementalServiceConnector.h>
 #include <android/os/incremental/BnStorageHealthListener.h>
 #include <android/os/incremental/BnStorageLoadingProgressListener.h>
+#include <android/os/incremental/PerUidReadTimeouts.h>
 #include <android/os/incremental/StorageHealthCheckParams.h>
 #include <binder/IAppOpsCallback.h>
 #include <utils/String16.h>
@@ -69,6 +70,8 @@
 using IStorageLoadingProgressListener = ::android::os::incremental::IStorageLoadingProgressListener;
 using StorageLoadingProgressListener = ::android::sp<IStorageLoadingProgressListener>;
 
+using PerUidReadTimeouts = ::android::os::incremental::PerUidReadTimeouts;
+
 class IncrementalService final {
 public:
     explicit IncrementalService(ServiceManagerWrapper&& sm, std::string_view rootDir);
@@ -98,7 +101,23 @@
     };
 
     enum StorageFlags {
-        ReadLogsEnabled = 1,
+        ReadLogsAllowed = 1 << 0,
+        ReadLogsEnabled = 1 << 1,
+    };
+
+    struct LoadingProgress {
+        ssize_t filledBlocks;
+        ssize_t totalBlocks;
+
+        bool isError() const { return totalBlocks < 0; }
+        bool started() const { return totalBlocks > 0; }
+        bool fullyLoaded() const { return !isError() && (totalBlocks == filledBlocks); }
+
+        float getProgress() const {
+            return totalBlocks < 0
+                    ? totalBlocks
+                    : totalBlocks > 0 ? double(filledBlocks) / double(totalBlocks) : 1.f;
+        }
     };
 
     static FileId idFromMetadata(std::span<const uint8_t> metadata);
@@ -114,7 +133,8 @@
                             content::pm::DataLoaderParamsParcel&& dataLoaderParams,
                             CreateOptions options, const DataLoaderStatusListener& statusListener,
                             StorageHealthCheckParams&& healthCheckParams,
-                            const StorageHealthListener& healthListener);
+                            const StorageHealthListener& healthListener,
+                            const std::vector<PerUidReadTimeouts>& perUidReadTimeouts);
     StorageId createLinkedStorage(std::string_view mountPoint, StorageId linkedStorage,
                                   CreateOptions options = CreateOptions::Default);
     StorageId openStorage(std::string_view path);
@@ -123,7 +143,7 @@
     int unbind(StorageId storage, std::string_view target);
     void deleteStorage(StorageId storage);
 
-    void disableReadLogs(StorageId storage);
+    void disallowReadLogs(StorageId storage);
     int setStorageParams(StorageId storage, bool enableReadLogs);
 
     int makeFile(StorageId storage, std::string_view path, int mode, FileId id,
@@ -135,8 +155,8 @@
              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;
+    int isFileFullyLoaded(StorageId storage, std::string_view filePath) const;
+    LoadingProgress getLoadingProgress(StorageId storage) const;
     bool registerLoadingProgressListener(StorageId storage,
                                          const StorageLoadingProgressListener& progressListener);
     bool unregisterLoadingProgressListener(StorageId storage);
@@ -282,7 +302,7 @@
         const std::string root;
         Control control;
         /*const*/ MountId mountId;
-        int32_t flags = StorageFlags::ReadLogsEnabled;
+        int32_t flags = StorageFlags::ReadLogsAllowed;
         StorageMap storages;
         BindMap bindPoints;
         DataLoaderStubPtr dataLoaderStub;
@@ -301,7 +321,15 @@
 
         StorageMap::iterator makeStorage(StorageId id);
 
-        void disableReadLogs() { flags &= ~StorageFlags::ReadLogsEnabled; }
+        void disallowReadLogs() { flags &= ~StorageFlags::ReadLogsAllowed; }
+        int32_t readLogsAllowed() const { return (flags & StorageFlags::ReadLogsAllowed); }
+
+        void setReadLogsEnabled(bool value) {
+            if (value)
+                flags |= StorageFlags::ReadLogsEnabled;
+            else
+                flags &= ~StorageFlags::ReadLogsEnabled;
+        }
         int32_t readLogsEnabled() const { return (flags & StorageFlags::ReadLogsEnabled); }
 
         static void cleanupFilesystem(std::string_view root);
@@ -313,6 +341,11 @@
 
     static bool perfLoggingEnabled();
 
+    void setUidReadTimeouts(StorageId storage,
+                            const std::vector<PerUidReadTimeouts>& perUidReadTimeouts);
+    void clearUidReadTimeouts(StorageId storage);
+    void updateUidReadTimeouts(StorageId storage, Clock::time_point timeLimit);
+
     std::unordered_set<std::string_view> adoptMountedInstances();
     void mountExistingImages(const std::unordered_set<std::string_view>& mountedRootNames);
     bool mountExistingImage(std::string_view root);
@@ -355,7 +388,7 @@
     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;
+    LoadingProgress getLoadingProgressFromPath(const IncFsMount& ifs, std::string_view path) const;
 
     int setFileContent(const IfsMountPtr& ifs, const incfs::FileId& fileId,
                        std::string_view debugFilePath, std::span<const uint8_t> data) const;
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index dfe9684..b1521b0 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -206,6 +206,11 @@
                                    std::vector<incfs::ReadInfo>* pendingReadsBuffer) const final {
         return incfs::waitForPendingReads(control, timeout, pendingReadsBuffer);
     }
+    ErrorCode setUidReadTimeouts(const Control& control,
+                                 const std::vector<android::os::incremental::PerUidReadTimeouts>&
+                                         perUidReadTimeouts) const final {
+        return -ENOTSUP;
+    }
 };
 
 static JNIEnv* getOrAttachJniEnv(JavaVM* jvm);
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index f2d0073..fad8d67 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -21,6 +21,7 @@
 #include <android/content/pm/FileSystemControlParcel.h>
 #include <android/content/pm/IDataLoader.h>
 #include <android/content/pm/IDataLoaderStatusListener.h>
+#include <android/os/incremental/PerUidReadTimeouts.h>
 #include <binder/IAppOpsCallback.h>
 #include <binder/IServiceManager.h>
 #include <binder/Status.h>
@@ -103,6 +104,10 @@
     virtual WaitResult waitForPendingReads(
             const Control& control, std::chrono::milliseconds timeout,
             std::vector<incfs::ReadInfo>* pendingReadsBuffer) const = 0;
+    virtual ErrorCode setUidReadTimeouts(
+            const Control& control,
+            const std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts)
+            const = 0;
 };
 
 class AppOpsManagerWrapper {
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index 9b8cf40..02eaa2f 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -42,6 +42,7 @@
 
 using namespace android::incfs;
 using namespace android::content::pm;
+using PerUidReadTimeouts = android::os::incremental::PerUidReadTimeouts;
 
 namespace android::os::incremental {
 
@@ -307,6 +308,9 @@
     MOCK_CONST_METHOD3(waitForPendingReads,
                        WaitResult(const Control& control, std::chrono::milliseconds timeout,
                                   std::vector<incfs::ReadInfo>* pendingReadsBuffer));
+    MOCK_CONST_METHOD2(setUidReadTimeouts,
+                       ErrorCode(const Control& control,
+                                 const std::vector<PerUidReadTimeouts>& perUidReadTimeouts));
 
     MockIncFs() { ON_CALL(*this, listExistingMounts(_)).WillByDefault(Return()); }
 
@@ -665,7 +669,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -676,7 +680,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -689,7 +693,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -703,7 +707,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -721,7 +725,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -735,7 +739,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mIncrementalService->deleteStorage(storageId);
 }
@@ -750,7 +754,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     // Simulated crash/other connection breakage.
     mDataLoaderManager->setDataLoaderStatusDestroyed();
@@ -767,7 +771,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusCreated();
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
@@ -785,7 +789,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
     mDataLoaderManager->setDataLoaderStatusCreated();
@@ -802,7 +806,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
 }
@@ -823,7 +827,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
     ASSERT_NE(nullptr, mLooper->mCallback);
@@ -877,7 +881,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, std::move(params), listener);
+                                                       {}, std::move(params), listener, {});
     ASSERT_GE(storageId, 0);
 
     // Healthy state, registered for pending reads.
@@ -972,7 +976,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
 }
@@ -993,11 +997,11 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     // Now disable.
-    mIncrementalService->disableReadLogs(storageId);
+    mIncrementalService->disallowReadLogs(storageId);
     ASSERT_EQ(mDataLoader->setStorageParams(true), -EPERM);
 }
 
@@ -1019,7 +1023,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get());
@@ -1038,7 +1042,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -1057,7 +1061,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -1066,7 +1070,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     std::string dir_path("test");
 
     // Expecting incfs to call makeDir on a path like:
@@ -1085,7 +1089,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     auto first = "first"sv;
     auto second = "second"sv;
     auto third = "third"sv;
@@ -1108,7 +1112,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_EQ(-1, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
 }
 
@@ -1119,7 +1123,7 @@
     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"));
 }
@@ -1131,7 +1135,7 @@
     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"));
 }
@@ -1143,7 +1147,7 @@
     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"));
 }
@@ -1155,8 +1159,8 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
-    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId));
+                                                       {}, {}, {}, {});
+    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressFailsWithFailedRanges) {
@@ -1166,9 +1170,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
-    ASSERT_EQ(-1, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(-1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressSuccessWithEmptyRanges) {
@@ -1178,9 +1182,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(3);
-    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressSuccess) {
@@ -1190,9 +1194,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(3);
-    ASSERT_EQ(0.5, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(0.5, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testRegisterLoadingProgressListenerSuccess) {
@@ -1202,7 +1206,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     sp<NiceMock<MockStorageLoadingProgressListener>> listener{
             new NiceMock<MockStorageLoadingProgressListener>};
     NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
@@ -1227,7 +1231,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     sp<NiceMock<MockStorageLoadingProgressListener>> listener{
             new NiceMock<MockStorageLoadingProgressListener>};
     NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
@@ -1242,9 +1246,10 @@
     NiceMock<MockStorageHealthListener>* newListenerMock = newListener.get();
 
     TemporaryDir tempDir;
-    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
-                                                       IncrementalService::CreateOptions::CreateNew,
-                                                       {}, StorageHealthCheckParams{}, listener);
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {},
+                                               StorageHealthCheckParams{}, listener, {});
     ASSERT_GE(storageId, 0);
     StorageHealthCheckParams newParams;
     newParams.blockedTimeoutMs = 10000;
@@ -1313,4 +1318,123 @@
     mTimedQueue->clearJob(storageId);
 }
 
+static std::vector<PerUidReadTimeouts> createPerUidTimeouts(
+        std::initializer_list<std::tuple<int, int, int, int>> tuples) {
+    std::vector<PerUidReadTimeouts> result;
+    for (auto&& tuple : tuples) {
+        result.emplace_back();
+        auto& timeouts = result.back();
+        timeouts.uid = std::get<0>(tuple);
+        timeouts.minTimeUs = std::get<1>(tuple);
+        timeouts.minPendingTimeUs = std::get<2>(tuple);
+        timeouts.maxPendingTimeUs = std::get<3>(tuple);
+    }
+    return result;
+}
+
+static ErrorCode checkPerUidTimeouts(const Control& control,
+                                     const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    std::vector<PerUidReadTimeouts> expected =
+            createPerUidTimeouts({{0, 1, 2, 3}, {1, 2, 3, 4}, {2, 3, 4, 100000000}});
+    EXPECT_EQ(expected, perUidReadTimeouts);
+    return 0;
+}
+
+static ErrorCode checkPerUidTimeoutsEmpty(
+        const Control& control, const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    EXPECT_EQ(0u, perUidReadTimeouts.size());
+    return 0;
+}
+
+TEST_F(IncrementalServiceTest, testPerUidTimeoutsTooShort) {
+    EXPECT_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _)).Times(1);
+    EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(1);
+    EXPECT_CALL(*mDataLoader, create(_, _, _, _)).Times(1);
+    EXPECT_CALL(*mDataLoader, start(_)).Times(0);
+    EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
+    EXPECT_CALL(*mIncFs, setUidReadTimeouts(_, _)).Times(0);
+    EXPECT_CALL(*mTimedQueue, addJob(_, _, _)).Times(0);
+    EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
+    TemporaryDir tempDir;
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {}, {},
+                                               {},
+                                               createPerUidTimeouts(
+                                                       {{0, 1, 2, 3}, {1, 2, 3, 4}, {2, 3, 4, 5}}));
+    ASSERT_GE(storageId, 0);
+}
+
+TEST_F(IncrementalServiceTest, testPerUidTimeoutsSuccess) {
+    mVold->setIncFsMountOptionsSuccess();
+    mAppOpsManager->checkPermissionSuccess();
+    mFs->hasFiles();
+
+    EXPECT_CALL(*mIncFs, setUidReadTimeouts(_, _))
+            // First call.
+            .WillOnce(Invoke(&checkPerUidTimeouts))
+            // Fully loaded and no readlogs.
+            .WillOnce(Invoke(&checkPerUidTimeoutsEmpty));
+    EXPECT_CALL(*mTimedQueue, addJob(_, _, _)).Times(3);
+
+    // Empty storage.
+    mIncFs->countFilledBlocksEmpty();
+
+    TemporaryDir tempDir;
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {}, {},
+                                               {},
+                                               createPerUidTimeouts({{0, 1, 2, 3},
+                                                                     {1, 2, 3, 4},
+                                                                     {2, 3, 4, 100000000}}));
+    ASSERT_GE(storageId, 0);
+
+    {
+        // Timed callback present -> 0 progress.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Still loading.
+        mIncFs->countFilledBlocksSuccess();
+
+        // Call it again.
+        timedCallback();
+    }
+
+    {
+        // Still present -> 0.5 progress.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Fully loaded but readlogs collection enabled.
+        mIncFs->countFilledBlocksFullyLoaded();
+        ASSERT_GE(mDataLoader->setStorageParams(true), 0);
+
+        // Call it again.
+        timedCallback();
+    }
+
+    {
+        // Still present -> fully loaded + readlogs.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Now disable readlogs.
+        ASSERT_GE(mDataLoader->setStorageParams(false), 0);
+
+        // Call it again.
+        timedCallback();
+    }
+
+    // No callbacks anymore -> fully loaded and no readlogs.
+    ASSERT_EQ(mTimedQueue->mAfter, Milliseconds());
+}
+
 } // namespace android::os::incremental
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
index d54a40e..c010e19 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
@@ -29,6 +29,10 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.HexDump;
+import com.android.server.pm.PerPackageReadTimeouts.Timeouts;
+import com.android.server.pm.PerPackageReadTimeouts.VersionCodes;
+
 import com.google.android.collect.Lists;
 
 import org.junit.After;
@@ -45,6 +49,7 @@
 import java.util.List;
 import java.util.regex.Pattern;
 
+// atest PackageManagerServiceTest
 // runtest -c com.android.server.pm.PackageManagerServiceTest frameworks-services
 // bit FrameworksServicesTests:com.android.server.pm.PackageManagerServiceTest
 @RunWith(AndroidJUnit4.class)
@@ -182,6 +187,219 @@
         }
     }
 
+    @Test
+    public void testTimeouts() {
+        Timeouts defaults = Timeouts.parse("3600000001:3600000002:3600000003");
+        Assert.assertEquals(3600000001L, defaults.minTimeUs);
+        Assert.assertEquals(3600000002L, defaults.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, defaults.maxPendingTimeUs);
+
+        Timeouts empty = Timeouts.parse("");
+        Assert.assertEquals(3600000000L, empty.minTimeUs);
+        Assert.assertEquals(3600000000L, empty.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, empty.maxPendingTimeUs);
+
+        Timeouts partial0 = Timeouts.parse("10000::");
+        Assert.assertEquals(10000L, partial0.minTimeUs);
+        Assert.assertEquals(3600000000L, partial0.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, partial0.maxPendingTimeUs);
+
+        Timeouts partial1 = Timeouts.parse("10000:10001:");
+        Assert.assertEquals(10000L, partial1.minTimeUs);
+        Assert.assertEquals(10001L, partial1.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, partial1.maxPendingTimeUs);
+
+        Timeouts fullDefault = Timeouts.parse("3600000000:3600000000:3600000000");
+        Assert.assertEquals(3600000000L, fullDefault.minTimeUs);
+        Assert.assertEquals(3600000000L, fullDefault.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, fullDefault.maxPendingTimeUs);
+
+        Timeouts full = Timeouts.parse("10000:10001:10002");
+        Assert.assertEquals(10000L, full.minTimeUs);
+        Assert.assertEquals(10001L, full.minPendingTimeUs);
+        Assert.assertEquals(10002L, full.maxPendingTimeUs);
+
+        Timeouts invalid0 = Timeouts.parse(":10000");
+        Assert.assertEquals(3600000000L, invalid0.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid0.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid0.maxPendingTimeUs);
+
+        Timeouts invalid1 = Timeouts.parse(":10000::");
+        Assert.assertEquals(3600000000L, invalid1.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid1.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid1.maxPendingTimeUs);
+
+        Timeouts invalid2 = Timeouts.parse("10000:10001:abcd");
+        Assert.assertEquals(10000L, invalid2.minTimeUs);
+        Assert.assertEquals(10001L, invalid2.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid2.maxPendingTimeUs);
+
+        Timeouts invalid3 = Timeouts.parse(":10000:");
+        Assert.assertEquals(3600000000L, invalid3.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid3.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid3.maxPendingTimeUs);
+
+        Timeouts invalid4 = Timeouts.parse("abcd:10001:10002");
+        Assert.assertEquals(3600000000L, invalid4.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid4.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid4.maxPendingTimeUs);
+
+        Timeouts invalid5 = Timeouts.parse("::1000000000000000000000000");
+        Assert.assertEquals(3600000000L, invalid5.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid5.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid5.maxPendingTimeUs);
+
+        Timeouts invalid6 = Timeouts.parse("-10000:10001:10002");
+        Assert.assertEquals(3600000000L, invalid6.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid6.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid6.maxPendingTimeUs);
+    }
+
+    @Test
+    public void testVersionCodes() {
+        final VersionCodes defaults = VersionCodes.parse("");
+        Assert.assertEquals(Long.MIN_VALUE, defaults.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, defaults.maxVersionCode);
+
+        VersionCodes single = VersionCodes.parse("191000070");
+        Assert.assertEquals(191000070, single.minVersionCode);
+        Assert.assertEquals(191000070, single.maxVersionCode);
+
+        VersionCodes single2 = VersionCodes.parse("191000070-191000070");
+        Assert.assertEquals(191000070, single2.minVersionCode);
+        Assert.assertEquals(191000070, single2.maxVersionCode);
+
+        VersionCodes upto = VersionCodes.parse("-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, upto.minVersionCode);
+        Assert.assertEquals(191000070, upto.maxVersionCode);
+
+        VersionCodes andabove = VersionCodes.parse("191000070-");
+        Assert.assertEquals(191000070, andabove.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, andabove.maxVersionCode);
+
+        VersionCodes range = VersionCodes.parse("191000070-201000070");
+        Assert.assertEquals(191000070, range.minVersionCode);
+        Assert.assertEquals(201000070, range.maxVersionCode);
+
+        VersionCodes invalid0 = VersionCodes.parse("201000070-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, invalid0.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid0.maxVersionCode);
+
+        VersionCodes invalid1 = VersionCodes.parse("abcd-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, invalid1.minVersionCode);
+        Assert.assertEquals(191000070, invalid1.maxVersionCode);
+
+        VersionCodes invalid2 = VersionCodes.parse("abcd");
+        Assert.assertEquals(Long.MIN_VALUE, invalid2.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid2.maxVersionCode);
+
+        VersionCodes invalid3 = VersionCodes.parse("191000070-abcd");
+        Assert.assertEquals(191000070, invalid3.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid3.maxVersionCode);
+    }
+
+    @Test
+    public void testPerPackageReadTimeouts() {
+        final String sha256 = "336faefc91bb2dddf9b21829106fbc607b862132fecd273e1b6b3ea55f09d4e1";
+        final VersionCodes defVCs = VersionCodes.parse("");
+        final Timeouts defTs = Timeouts.parse("3600000001:3600000002:3600000003");
+
+        PerPackageReadTimeouts empty = PerPackageReadTimeouts.parse("", defVCs, defTs);
+        Assert.assertNull(empty);
+
+        PerPackageReadTimeouts packageOnly = PerPackageReadTimeouts.parse("package.com", defVCs,
+                defTs);
+        Assert.assertEquals("package.com", packageOnly.packageName);
+        Assert.assertEquals(null, packageOnly.sha256certificate);
+        Assert.assertEquals(Long.MIN_VALUE, packageOnly.versionCodes.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, packageOnly.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageOnly.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageOnly.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageOnly.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts packageHash = PerPackageReadTimeouts.parse(
+                "package.com:" + sha256, defVCs, defTs);
+        Assert.assertEquals("package.com", packageHash.packageName);
+        Assert.assertEquals(sha256, bytesToHexString(packageHash.sha256certificate));
+        Assert.assertEquals(Long.MIN_VALUE, packageHash.versionCodes.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, packageHash.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageHash.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageHash.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageHash.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts packageVersionCode = PerPackageReadTimeouts.parse(
+                "package.com::191000070", defVCs, defTs);
+        Assert.assertEquals("package.com", packageVersionCode.packageName);
+        Assert.assertEquals(null, packageVersionCode.sha256certificate);
+        Assert.assertEquals(191000070, packageVersionCode.versionCodes.minVersionCode);
+        Assert.assertEquals(191000070, packageVersionCode.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageVersionCode.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageVersionCode.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageVersionCode.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts full = PerPackageReadTimeouts.parse(
+                "package.com:" + sha256 + ":191000070-201000070:10001:10002:10003", defVCs, defTs);
+        Assert.assertEquals("package.com", full.packageName);
+        Assert.assertEquals(sha256, bytesToHexString(full.sha256certificate));
+        Assert.assertEquals(191000070, full.versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, full.versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, full.timeouts.minTimeUs);
+        Assert.assertEquals(10002L, full.timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, full.timeouts.maxPendingTimeUs);
+    }
+
+    @Test
+    public void testGetPerPackageReadTimeouts() {
+        Assert.assertEquals(0, getPerPackageReadTimeouts(null).length);
+        Assert.assertEquals(0, getPerPackageReadTimeouts("").length);
+        Assert.assertEquals(0, getPerPackageReadTimeouts(",,,,").length);
+
+        final String sha256 = "0fae93f1a7925b4c68bbea80ad3eaa41acfc9bc6f10bf1054f5d93a2bd556093";
+
+        PerPackageReadTimeouts[] singlePackage = getPerPackageReadTimeouts(
+                "package.com:" + sha256 + ":191000070-201000070:10001:10002:10003");
+        Assert.assertEquals(1, singlePackage.length);
+        Assert.assertEquals("package.com", singlePackage[0].packageName);
+        Assert.assertEquals(sha256, bytesToHexString(singlePackage[0].sha256certificate));
+        Assert.assertEquals(191000070, singlePackage[0].versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, singlePackage[0].versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, singlePackage[0].timeouts.minTimeUs);
+        Assert.assertEquals(10002L, singlePackage[0].timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, singlePackage[0].timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts[] multiPackage = getPerPackageReadTimeouts("package.com:" + sha256
+                + ":191000070-201000070:10001:10002:10003,package1.com::123456");
+        Assert.assertEquals(2, multiPackage.length);
+        Assert.assertEquals("package.com", multiPackage[0].packageName);
+        Assert.assertEquals(sha256, bytesToHexString(multiPackage[0].sha256certificate));
+        Assert.assertEquals(191000070, multiPackage[0].versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, multiPackage[0].versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, multiPackage[0].timeouts.minTimeUs);
+        Assert.assertEquals(10002L, multiPackage[0].timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, multiPackage[0].timeouts.maxPendingTimeUs);
+        Assert.assertEquals("package1.com", multiPackage[1].packageName);
+        Assert.assertEquals(null, multiPackage[1].sha256certificate);
+        Assert.assertEquals(123456, multiPackage[1].versionCodes.minVersionCode);
+        Assert.assertEquals(123456, multiPackage[1].versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, multiPackage[1].timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, multiPackage[1].timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, multiPackage[1].timeouts.maxPendingTimeUs);
+    }
+
+    private static PerPackageReadTimeouts[] getPerPackageReadTimeouts(String knownDigestersList) {
+        final String defaultTimeouts = "3600000001:3600000002:3600000003";
+        List<PerPackageReadTimeouts> result = PerPackageReadTimeouts.parseDigestersList(
+                defaultTimeouts, knownDigestersList);
+        if (result == null) {
+            return null;
+        }
+        return result.toArray(new PerPackageReadTimeouts[result.size()]);
+    }
+
+    private static String bytesToHexString(byte[] bytes) {
+        return HexDump.toHexString(bytes, 0, bytes.length, /*upperCase=*/ false);
+    }
+
     private List<Integer> getKnownPackageIdsList() throws IllegalAccessException {
         final ArrayList<Integer> knownPackageIds = new ArrayList<>();
         final Field[] allFields = PackageManagerInternal.class.getDeclaredFields();