Merge "Implement BlobStoreManagerService."
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
index 6aca4a1..60c3136 100644
--- a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
+++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
@@ -22,6 +22,9 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * An identifier to represent a blob.
  */
@@ -173,6 +176,27 @@
         dest.writeString(tag);
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || !(obj instanceof BlobHandle)) {
+            return false;
+        }
+        final BlobHandle other = (BlobHandle) obj;
+        return this.algorithm.equals(other.algorithm)
+                && Arrays.equals(this.digest, other.digest)
+                && this.label.equals(other.label)
+                && this.expiryTimeMillis == other.expiryTimeMillis
+                && this.tag.equals(tag);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag);
+    }
+
     public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
         @Override
         public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
index 4395e5a..47af7c0 100644
--- a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
+++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
@@ -45,6 +45,11 @@
  */
 @SystemService(Context.BLOB_STORE_SERVICE)
 public class BlobStoreManager {
+    /** @hide */
+    public static final int COMMIT_RESULT_SUCCESS = 0;
+    /** @hide */
+    public static final int COMMIT_RESULT_ERROR = 1;
+
     private final Context mContext;
     private final IBlobStoreManager mService;
 
@@ -102,7 +107,28 @@
      */
     public @NonNull Session openSession(@IntRange(from = 1) long sessionId) throws IOException {
         try {
-            return new Session(mService.openSession(sessionId));
+            return new Session(mService.openSession(sessionId, mContext.getOpPackageName()));
+        } catch (ParcelableException e) {
+            e.maybeRethrow(IOException.class);
+            throw new RuntimeException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Delete an existing session and any data that was written to that session so far.
+     *
+     * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that
+     *                  represents a particular session.
+     *
+     * @throws IOException when there is an I/O error while deleting the session.
+     * @throws SecurityException when the caller does not own the session, or
+     *                           the session does not exist or is invalid.
+     */
+    public void deleteSession(@IntRange(from = 1) long sessionId) throws IOException {
+        try {
+            mService.deleteSession(sessionId, mContext.getOpPackageName());
         } catch (ParcelableException e) {
             e.maybeRethrow(IOException.class);
             throw new RuntimeException(e);
@@ -142,6 +168,9 @@
      * <p> Any active leases will be automatically released when the blob's expiry time
      * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
      *
+     * <p> This lease information is persisted and calling this more than once will result in
+     * latest lease overriding any previous lease.
+     *
      * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
      *                   acquire a lease for.
      * @param descriptionResId the resource id for a short description string that can be surfaced
@@ -190,6 +219,9 @@
      * <p> Any active leases will be automatically released when the blob's expiry time
      * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
      *
+     * <p> This lease information is persisted and calling this more than once will result in
+     * latest lease overriding any previous lease.
+     *
      * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
      *                   acquire a lease for.
      * @param descriptionResId the resource id for a short description string that can be surfaced
@@ -279,7 +311,9 @@
         public @NonNull ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
                 @BytesLong long lengthBytes) throws IOException {
             try {
-                return mSession.openWrite(offsetBytes, lengthBytes);
+                final ParcelFileDescriptor pfd = mSession.openWrite(offsetBytes, lengthBytes);
+                pfd.seekTo(offsetBytes);
+                return pfd;
             } catch (ParcelableException e) {
                 e.maybeRethrow(IOException.class);
                 throw new RuntimeException(e);
@@ -376,6 +410,31 @@
         }
 
         /**
+         * Returns {@code true} if access has been allowed for a {@code packageName} using either
+         * {@link #allowPackageAccess(String, byte[])}.
+         * Otherwise, {@code false}.
+         *
+         * @param packageName the name of the package to check the access for.
+         * @param certificate the input bytes representing a certificate of type
+         *                    {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+         *
+         * @throws IOException when there is an I/O error while getting the access type.
+         * @throws IllegalStateException when the caller tries to get access type from a session
+         *                               which is closed or abandoned.
+         */
+        public boolean isPackageAccessAllowed(@NonNull String packageName,
+                @NonNull byte[] certificate) throws IOException {
+            try {
+                return mSession.isPackageAccessAllowed(packageName, certificate);
+            } catch (ParcelableException e) {
+                e.maybeRethrow(IOException.class);
+                throw new RuntimeException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Allow packages which are signed with the same certificate as the caller to access this
          * blob data once it is committed using a {@link BlobHandle} representing the blob.
          *
@@ -399,6 +458,26 @@
         }
 
         /**
+         * Returns {@code true} if access has been allowed for packages signed with the same
+         * certificate as the caller by using {@link #allowSameSignatureAccess()}.
+         * Otherwise, {@code false}.
+         *
+         * @throws IOException when there is an I/O error while getting the access type.
+         * @throws IllegalStateException when the caller tries to get access type from a session
+         *                               which is closed or abandoned.
+         */
+        public boolean isSameSignatureAccessAllowed() throws IOException {
+            try {
+                return mSession.isSameSignatureAccessAllowed();
+            } catch (ParcelableException e) {
+                e.maybeRethrow(IOException.class);
+                throw new RuntimeException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Allow any app on the device to access this blob data once it is committed using
          * a {@link BlobHandle} representing the blob.
          *
@@ -427,6 +506,25 @@
         }
 
         /**
+         * Returns {@code true} if public access has been allowed by using
+         * {@link #allowPublicAccess()}. Otherwise, {@code false}.
+         *
+         * @throws IOException when there is an I/O error while getting the access type.
+         * @throws IllegalStateException when the caller tries to get access type from a session
+         *                               which is closed or abandoned.
+         */
+        public boolean isPublicAccessAllowed() throws IOException {
+            try {
+                return mSession.isPublicAccessAllowed();
+            } catch (ParcelableException e) {
+                e.maybeRethrow(IOException.class);
+                throw new RuntimeException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Commit the file that was written so far to this session to the blob store maintained by
          * the system.
          *
@@ -439,6 +537,10 @@
          * {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)}  BlobHandle}
          * associated with this session.
          *
+         * <p> Committing the same data more than once will result in replacing the corresponding
+         * access mode (via calling one of {@link #allowPackageAccess(String, byte[])},
+         * {@link #allowSameSignatureAccess()}, etc) with the latest one.
+         *
          * @param executor the executor on which result callback will be invoked.
          * @param resultCallback a callback to receive the commit result. when the result is
          *                       {@code 0}, it indicates success. Otherwise, failure.
diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
index b7a2f1a..dfbf78f 100644
--- a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
@@ -21,8 +21,9 @@
 /** {@hide} */
 interface IBlobStoreManager {
     long createSession(in BlobHandle handle, in String packageName);
-    IBlobStoreSession openSession(long sessionId);
+    IBlobStoreSession openSession(long sessionId, in String packageName);
     ParcelFileDescriptor openBlob(in BlobHandle handle, in String packageName);
+    void deleteSession(long sessionId, in String packageName);
 
     void acquireLease(in BlobHandle handle, int descriptionResId, long leaseTimeout,
             in String packageName);
diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
index bb5ef3b..4ae919b 100644
--- a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
@@ -26,6 +26,10 @@
     void allowSameSignatureAccess();
     void allowPublicAccess();
 
+    boolean isPackageAccessAllowed(in String packageName, in byte[] certificate);
+    boolean isSameSignatureAccessAllowed();
+    boolean isPublicAccessAllowed();
+
     long getSize();
     void close();
     void abandon();
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
new file mode 100644
index 0000000..357250a
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 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.blob;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.ArraySet;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Class for representing how a blob can be shared.
+ *
+ * Note that this class is not thread-safe, callers need to take of synchronizing access.
+ */
+class BlobAccessMode {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            ACCESS_TYPE_PRIVATE,
+            ACCESS_TYPE_PUBLIC,
+            ACCESS_TYPE_SAME_SIGNATURE,
+            ACCESS_TYPE_WHITELIST,
+    })
+    @interface AccessType {}
+    static final int ACCESS_TYPE_PRIVATE = 1 << 0;
+    static final int ACCESS_TYPE_PUBLIC = 1 << 1;
+    static final int ACCESS_TYPE_SAME_SIGNATURE = 1 << 2;
+    static final int ACCESS_TYPE_WHITELIST = 1 << 3;
+
+    private int mAccessType = ACCESS_TYPE_PRIVATE;
+
+    private final ArraySet<PackageIdentifier> mWhitelistedPackages = new ArraySet<>();
+
+    void allow(BlobAccessMode other) {
+        if ((other.mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+            mWhitelistedPackages.addAll(other.mWhitelistedPackages);
+        }
+        mAccessType |= other.mAccessType;
+    }
+
+    void allowPublicAccess() {
+        mAccessType |= ACCESS_TYPE_PUBLIC;
+    }
+
+    void allowSameSignatureAccess() {
+        mAccessType |= ACCESS_TYPE_SAME_SIGNATURE;
+    }
+
+    void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) {
+        mAccessType |= ACCESS_TYPE_WHITELIST;
+        mWhitelistedPackages.add(PackageIdentifier.create(packageName, certificate));
+    }
+
+    boolean isPublicAccessAllowed() {
+        return (mAccessType & ACCESS_TYPE_PUBLIC) != 0;
+    }
+
+    boolean isSameSignatureAccessAllowed() {
+        return (mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0;
+    }
+
+    boolean isPackageAccessAllowed(@NonNull String packageName, @NonNull byte[] certificate) {
+        if ((mAccessType & ACCESS_TYPE_WHITELIST) == 0) {
+            return false;
+        }
+        return mWhitelistedPackages.contains(PackageIdentifier.create(packageName, certificate));
+    }
+
+    boolean isAccessAllowedForCaller(Context context,
+            @NonNull String callingPackage, @NonNull String committerPackage) {
+        if ((mAccessType & ACCESS_TYPE_PUBLIC) != 0) {
+            return true;
+        }
+
+        final PackageManager pm = context.getPackageManager();
+        if ((mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0) {
+            if (pm.checkSignatures(committerPackage, callingPackage)
+                    == PackageManager.SIGNATURE_MATCH) {
+                return true;
+            }
+        }
+
+        if ((mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+            for (int i = 0; i < mWhitelistedPackages.size(); ++i) {
+                final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i);
+                if (packageIdentifier.packageName.equals(callingPackage)
+                        && pm.hasSigningCertificate(callingPackage, packageIdentifier.certificate,
+                                PackageManager.CERT_INPUT_SHA256)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private static final class PackageIdentifier {
+        public final String packageName;
+        public final byte[] certificate;
+
+        private PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) {
+            this.packageName = packageName;
+            this.certificate = certificate;
+        }
+
+        public static PackageIdentifier create(@NonNull String packageName,
+                @NonNull byte[] certificate) {
+            return new PackageIdentifier(packageName, certificate);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || !(obj instanceof PackageIdentifier)) {
+                return false;
+            }
+            final PackageIdentifier other = (PackageIdentifier) obj;
+            return this.packageName.equals(other.packageName)
+                    && Arrays.equals(this.certificate, other.certificate);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(packageName, Arrays.hashCode(certificate));
+        }
+    }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
new file mode 100644
index 0000000..d3a22715
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2020 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.blob;
+
+import static android.system.OsConstants.O_RDONLY;
+
+import android.annotation.NonNull;
+import android.app.blob.BlobHandle;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RevocableFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+class BlobMetadata {
+    private final Object mMetadataLock = new Object();
+
+    private final Context mContext;
+    private final long mBlobId;
+    private final BlobHandle mBlobHandle;
+
+    @GuardedBy("mMetadataLock")
+    private final ArraySet<Committer> mCommitters = new ArraySet<>();
+
+    @GuardedBy("mMetadataLock")
+    private final ArraySet<Leasee> mLeasees = new ArraySet<>();
+
+    /**
+     * Contains packageName -> {RevocableFileDescriptors}.
+     *
+     * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so
+     * that when clients access is revoked or the blob gets deleted, we can be sure that clients
+     * do not have any reference to the blob and the space occupied by the blob can be freed.
+     */
+    @GuardedBy("mRevocableFds")
+    private final ArrayMap<String, ArraySet<RevocableFileDescriptor>> mRevocableFds =
+            new ArrayMap<>();
+
+    BlobMetadata(Context context, long blobId, BlobHandle blobHandle) {
+        mContext = context;
+        mBlobId = blobId;
+        mBlobHandle = blobHandle;
+    }
+
+    void addCommitter(String packageName, int uid, BlobAccessMode blobAccessMode) {
+        synchronized (mMetadataLock) {
+            mCommitters.add(new Committer(packageName, uid, blobAccessMode));
+        }
+    }
+
+    void addLeasee(String callingPackage, int callingUid,
+            int descriptionResId, long leaseExpiryTimeMillis) {
+        synchronized (mMetadataLock) {
+            mLeasees.add(new Leasee(callingPackage, callingUid,
+                    descriptionResId, leaseExpiryTimeMillis));
+        }
+    }
+
+    void removeLeasee(String packageName, int uid) {
+        synchronized (mMetadataLock) {
+            mLeasees.remove(new Accessor(packageName, uid));
+        }
+    }
+
+    boolean isAccessAllowedForCaller(String callingPackage, int callingUid) {
+        // TODO: verify blob is still valid (expiryTime is not elapsed)
+        synchronized (mMetadataLock) {
+            // Check if packageName already holds a lease on the blob.
+            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+                final Leasee leasee = mLeasees.valueAt(i);
+                if (leasee.equals(callingPackage, callingUid)
+                        && leasee.isStillValid()) {
+                    return true;
+                }
+            }
+
+            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+                final Committer committer = mCommitters.valueAt(i);
+
+                // Check if the caller is the same package that committed the blob.
+                if (committer.equals(callingPackage, callingUid)) {
+                    return true;
+                }
+
+                // Check if the caller is allowed access as per the access mode specified
+                // by the committer.
+                if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
+                        callingPackage, committer.packageName)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    ParcelFileDescriptor openForRead(String callingPackage) throws IOException {
+        // TODO: Add limit on opened fds
+        FileDescriptor fd;
+        try {
+            fd = Os.open(BlobStoreConfig.getBlobFile(mBlobId).getPath(), O_RDONLY, 0);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+        synchronized (mMetadataLock) {
+            return createRevocableFdLocked(fd, callingPackage);
+        }
+    }
+
+    @GuardedBy("mMetadataLock")
+    @NonNull
+    private ParcelFileDescriptor createRevocableFdLocked(FileDescriptor fd,
+            String callingPackage) throws IOException {
+        final RevocableFileDescriptor revocableFd =
+                new RevocableFileDescriptor(mContext, fd);
+        synchronized (mRevocableFds) {
+            ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+                    mRevocableFds.get(callingPackage);
+            if (revocableFdsForPkg == null) {
+                revocableFdsForPkg = new ArraySet<>();
+                mRevocableFds.put(callingPackage, revocableFdsForPkg);
+            }
+            revocableFdsForPkg.add(revocableFd);
+        }
+        revocableFd.addOnCloseListener((e) -> {
+            synchronized (mRevocableFds) {
+                final ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+                        mRevocableFds.get(callingPackage);
+                if (revocableFdsForPkg != null) {
+                    revocableFdsForPkg.remove(revocableFd);
+                }
+            }
+        });
+        return revocableFd.getRevocableFileDescriptor();
+    }
+
+    static final class Committer extends Accessor {
+        public final BlobAccessMode blobAccessMode;
+
+        Committer(String packageName, int uid, BlobAccessMode blobAccessMode) {
+            super(packageName, uid);
+            this.blobAccessMode = blobAccessMode;
+        }
+    }
+
+    static final class Leasee extends Accessor {
+        public final int descriptionResId;
+        public final long expiryTimeMillis;
+
+        Leasee(String packageName, int uid, int descriptionResId, long expiryTimeMillis) {
+            super(packageName, uid);
+            this.descriptionResId = descriptionResId;
+            this.expiryTimeMillis = expiryTimeMillis;
+        }
+
+        boolean isStillValid() {
+            return expiryTimeMillis == 0 || expiryTimeMillis <= System.currentTimeMillis();
+        }
+    }
+
+    static class Accessor {
+        public final String packageName;
+        public final int uid;
+
+        Accessor(String packageName, int uid) {
+            this.packageName = packageName;
+            this.uid = uid;
+        }
+
+        public boolean equals(String packageName, int uid) {
+            return this.uid == uid && this.packageName.equals(packageName);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || !(obj instanceof Accessor)) {
+                return false;
+            }
+            final Accessor other = (Accessor) obj;
+            return this.uid == other.uid && this.packageName.equals(other.packageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(packageName, uid);
+        }
+    }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
new file mode 100644
index 0000000..b9a4b17
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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.blob;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Environment;
+import android.util.Slog;
+
+import java.io.File;
+
+class BlobStoreConfig {
+    public static final String TAG = "BlobStore";
+
+    @Nullable
+    public static File prepareBlobFile(long sessionId) {
+        final File blobsDir = prepareBlobsDir();
+        return blobsDir == null ? null : getBlobFile(blobsDir, sessionId);
+    }
+
+    @NonNull
+    public static File getBlobFile(long sessionId) {
+        return getBlobFile(getBlobsDir(), sessionId);
+    }
+
+    @NonNull
+    private static File getBlobFile(File blobsDir, long sessionId) {
+        return new File(blobsDir, String.valueOf(sessionId));
+    }
+
+    @Nullable
+    public static File prepareBlobsDir() {
+        final File blobsDir = getBlobsDir(prepareBlobStoreRootDir());
+        if (!blobsDir.exists() && !blobsDir.mkdir()) {
+            Slog.e(TAG, "Failed to mkdir(): " + blobsDir);
+            return null;
+        }
+        return blobsDir;
+    }
+
+    @NonNull
+    public static File getBlobsDir() {
+        return getBlobsDir(getBlobStoreRootDir());
+    }
+
+    @NonNull
+    private static File getBlobsDir(File blobsRootDir) {
+        return new File(blobsRootDir, "blobs");
+    }
+
+    @Nullable
+    public static File prepareBlobStoreRootDir() {
+        final File blobStoreRootDir = getBlobStoreRootDir();
+        if (!blobStoreRootDir.exists() && !blobStoreRootDir.mkdir()) {
+            Slog.e(TAG, "Failed to mkdir(): " + blobStoreRootDir);
+            return null;
+        }
+        return blobStoreRootDir;
+    }
+
+    @NonNull
+    public static File getBlobStoreRootDir() {
+        return new File(Environment.getDataSystemDirectory(), "blobstore");
+    }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index b204fee..9d60f86 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -15,58 +15,350 @@
  */
 package com.android.server.blob;
 
+import static android.app.blob.BlobStoreManager.COMMIT_RESULT_SUCCESS;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
+import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_VALID;
+import static com.android.server.blob.BlobStoreSession.stateToString;
+
 import android.annotation.CurrentTimeSecondsLong;
 import android.annotation.IdRes;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.app.blob.BlobHandle;
 import android.app.blob.IBlobStoreManager;
 import android.app.blob.IBlobStoreSession;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ExceptionUtils;
+import android.util.LongSparseArray;
+import android.util.Slog;
+import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
 import com.android.server.SystemService;
+import com.android.server.Watchdog;
+
+import java.io.IOException;
 
 /**
  * Service responsible for maintaining and facilitating access to data blobs published by apps.
  */
 public class BlobStoreManagerService extends SystemService {
 
+    private final Object mBlobsLock = new Object();
+
+    // Contains data of userId -> {sessionId -> {BlobStoreSession}}.
+    @GuardedBy("mBlobsLock")
+    private final SparseArray<LongSparseArray<BlobStoreSession>> mSessions = new SparseArray<>();
+
+    @GuardedBy("mBlobsLock")
+    private long mCurrentMaxSessionId;
+
+    // Contains data of userId -> {BlobHandle -> {BlobMetadata}}
+    @GuardedBy("mBlobsLock")
+    private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>();
+
     private final Context mContext;
+    private final Handler mHandler;
+    private final SessionStateChangeListener mSessionStateChangeListener =
+            new SessionStateChangeListener();
+
+    private PackageManagerInternal mPackageManagerInternal;
 
     public BlobStoreManagerService(Context context) {
         super(context);
         mContext = context;
+
+        final HandlerThread handlerThread = new ServiceThread(TAG,
+                Process.THREAD_PRIORITY_BACKGROUND, true /* allowIo */);
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+        Watchdog.getInstance().addThread(mHandler);
     }
 
     @Override
     public void onStart() {
         publishBinderService(Context.BLOB_STORE_SERVICE, new Stub());
+
+        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+    }
+
+
+    @GuardedBy("mBlobsLock")
+    private long generateNextSessionIdLocked() {
+        return ++mCurrentMaxSessionId;
+    }
+
+    @GuardedBy("mBlobsLock")
+    private LongSparseArray<BlobStoreSession> getUserSessionsLocked(int userId) {
+        LongSparseArray<BlobStoreSession> userSessions = mSessions.get(userId);
+        if (userSessions == null) {
+            userSessions = new LongSparseArray<>();
+            mSessions.put(userId, userSessions);
+        }
+        return userSessions;
+    }
+
+    @GuardedBy("mBlobsLock")
+    private ArrayMap<BlobHandle, BlobMetadata> getUserBlobsLocked(int userId) {
+        ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.get(userId);
+        if (userBlobs == null) {
+            userBlobs = new ArrayMap<>();
+            mBlobsMap.put(userId, userBlobs);
+        }
+        return userBlobs;
+    }
+
+    private long createSessionInternal(BlobHandle blobHandle,
+            int callingUid, String callingPackage) {
+        synchronized (mBlobsLock) {
+            // TODO: throw if there is already an active session associated with blobHandle.
+            final long sessionId = generateNextSessionIdLocked();
+            final BlobStoreSession session = new BlobStoreSession(mContext,
+                    sessionId, blobHandle, callingUid, callingPackage,
+                    mSessionStateChangeListener);
+            getUserSessionsLocked(UserHandle.getUserId(callingUid)).put(sessionId, session);
+            // TODO: persist sessions data
+            return sessionId;
+        }
+    }
+
+    private BlobStoreSession openSessionInternal(long sessionId,
+            int callingUid, String callingPackage) {
+        final BlobStoreSession session;
+        synchronized (mBlobsLock) {
+            session = getUserSessionsLocked(
+                    UserHandle.getUserId(callingUid)).get(sessionId);
+            if (session == null || !session.hasAccess(callingUid, callingPackage)
+                    || session.isFinalized()) {
+                throw new SecurityException("Session not found: " + sessionId);
+            }
+        }
+        session.open();
+        return session;
+    }
+
+    private void deleteSessionInternal(long sessionId,
+            int callingUid, String callingPackage) {
+        synchronized (mBlobsLock) {
+            final BlobStoreSession session = openSessionInternal(sessionId,
+                    callingUid, callingPackage);
+            session.open();
+            session.abandon();
+            // TODO: persist sessions data
+        }
+    }
+
+    private ParcelFileDescriptor openBlobInternal(BlobHandle blobHandle, int callingUid,
+            String callingPackage) throws IOException {
+        synchronized (mBlobsLock) {
+            final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+                    .get(blobHandle);
+            if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+                    callingPackage, callingUid)) {
+                throw new SecurityException("Caller not allowed to access " + blobHandle
+                        + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+            }
+            return blobMetadata.openForRead(callingPackage);
+        }
+    }
+
+    private void acquireLeaseInternal(BlobHandle blobHandle, int descriptionResId,
+            long leaseExpiryTimeMillis, int callingUid, String callingPackage) {
+        synchronized (mBlobsLock) {
+            final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+                    .get(blobHandle);
+            if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+                    callingPackage, callingUid)) {
+                throw new SecurityException("Caller not allowed to access " + blobHandle
+                        + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+            }
+            if (leaseExpiryTimeMillis != 0 && leaseExpiryTimeMillis > blobHandle.expiryTimeMillis) {
+                throw new IllegalArgumentException(
+                        "Lease expiry cannot be later than blobs expiry time");
+            }
+            blobMetadata.addLeasee(callingPackage, callingUid,
+                    descriptionResId, leaseExpiryTimeMillis);
+            // TODO: persist blobs data
+        }
+    }
+
+    private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid,
+            String callingPackage) {
+        synchronized (mBlobsLock) {
+            final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+                    .get(blobHandle);
+            if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+                    callingPackage, callingUid)) {
+                throw new SecurityException("Caller not allowed to access " + blobHandle
+                        + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+            }
+            blobMetadata.removeLeasee(callingPackage, callingUid);
+        }
+    }
+
+    private void verifyCallingPackage(int callingUid, String callingPackage) {
+        if (mPackageManagerInternal.getPackageUid(
+                callingPackage, 0, UserHandle.getUserId(callingUid)) != callingUid) {
+            throw new SecurityException("Specified calling package [" + callingPackage
+                    + "] does not match the calling uid " + callingUid);
+        }
+    }
+
+    class SessionStateChangeListener {
+        public void onStateChanged(@NonNull BlobStoreSession session) {
+            mHandler.post(PooledLambda.obtainRunnable(
+                    BlobStoreManagerService::onStateChangedInternal,
+                    BlobStoreManagerService.this, session));
+        }
+    }
+
+    private void onStateChangedInternal(@NonNull BlobStoreSession session) {
+        synchronized (mBlobsLock) {
+            switch (session.getState()) {
+                case STATE_ABANDONED:
+                case STATE_VERIFIED_INVALID:
+                    session.getSessionFile().delete();
+                    getUserSessionsLocked(UserHandle.getUserId(session.ownerUid))
+                            .remove(session.sessionId);
+                    break;
+                case STATE_COMMITTED:
+                    session.verifyBlobData();
+                    break;
+                case STATE_VERIFIED_VALID:
+                    final ArrayMap<BlobHandle, BlobMetadata> userBlobs =
+                            getUserBlobsLocked(UserHandle.getUserId(session.ownerUid));
+                    BlobMetadata blob = userBlobs.get(session.blobHandle);
+                    if (blob == null) {
+                        blob = new BlobMetadata(mContext,
+                                session.sessionId, session.blobHandle);
+                        userBlobs.put(session.blobHandle, blob);
+                    }
+                    blob.addCommitter(session.ownerPackageName, session.ownerUid,
+                            session.getBlobAccessMode());
+                    // TODO: Persist blobs data.
+                    session.sendCommitCallbackResult(COMMIT_RESULT_SUCCESS);
+                    getUserSessionsLocked(UserHandle.getUserId(session.ownerUid))
+                            .remove(session.sessionId);
+                    break;
+                default:
+                    Slog.wtf(TAG, "Invalid session state: "
+                            + stateToString(session.getState()));
+            }
+            // TODO: Persist sessions data.
+        }
     }
 
     private class Stub extends IBlobStoreManager.Stub {
         @Override
-        public long createSession(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
-            return 0;
+        @IntRange(from = 1)
+        public long createSession(@NonNull BlobHandle blobHandle,
+                @NonNull String packageName) {
+            Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+            // TODO: verify blobHandle.algorithm is sha-256
+            // TODO: assert blobHandle is valid.
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+                    packageName, UserHandle.getUserId(callingUid))) {
+                throw new SecurityException("Caller not allowed to create session; "
+                        + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+            }
+
+            // TODO: Verify caller request is within limits (no. of calls/blob sessions/blobs)
+            return createSessionInternal(blobHandle, callingUid, packageName);
         }
 
         @Override
-        public IBlobStoreSession openSession(long sessionId) {
-            return null;
+        @NonNull
+        public IBlobStoreSession openSession(@IntRange(from = 1) long sessionId,
+                @NonNull String packageName) {
+            Preconditions.checkArgumentPositive(sessionId,
+                    "sessionId must be positive: " + sessionId);
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            return openSessionInternal(sessionId, callingUid, packageName);
+        }
+
+        @Override
+        public void deleteSession(@IntRange(from = 1) long sessionId,
+                @NonNull String packageName) {
+            Preconditions.checkArgumentPositive(sessionId,
+                    "sessionId must be positive: " + sessionId);
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            deleteSessionInternal(sessionId, callingUid, packageName);
         }
 
         @Override
         public ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle,
                 @NonNull String packageName) {
-            return null;
+            Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+                    packageName, UserHandle.getUserId(callingUid))) {
+                throw new SecurityException("Caller not allowed to open blob; "
+                        + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+            }
+
+            try {
+                return openBlobInternal(blobHandle, callingUid, packageName);
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
         }
 
         @Override
         public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId,
-                @CurrentTimeSecondsLong long leaseTimeout, @NonNull String packageName) {
+                @CurrentTimeSecondsLong long leaseTimeoutSecs, @NonNull String packageName) {
+            Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            acquireLeaseInternal(blobHandle, descriptionResId, leaseTimeoutSecs,
+                    callingUid, packageName);
         }
 
         @Override
         public void releaseLease(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
+            Preconditions.checkNotNull(blobHandle, "blobHandle must not be null");
+            Preconditions.checkNotNull(packageName, "packageName must not be null");
+
+
+            final int callingUid = Binder.getCallingUid();
+            verifyCallingPackage(callingUid, packageName);
+
+            releaseLeaseInternal(blobHandle, callingUid, packageName);
         }
     }
 }
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
index 2c38e76..29092b3 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
@@ -15,20 +15,175 @@
  */
 package com.android.server.blob;
 
+import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.SEEK_SET;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+
 import android.annotation.BytesLong;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.blob.BlobHandle;
 import android.app.blob.IBlobCommitCallback;
 import android.app.blob.IBlobStoreSession;
+import android.content.Context;
+import android.os.Binder;
+import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.RevocableFileDescriptor;
+import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ExceptionUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
 
 /** TODO: add doc */
 public class BlobStoreSession extends IBlobStoreSession.Stub {
 
+    static final int STATE_OPENED = 1;
+    static final int STATE_CLOSED = 0;
+    static final int STATE_ABANDONED = 2;
+    static final int STATE_COMMITTED = 3;
+    static final int STATE_VERIFIED_VALID = 4;
+    static final int STATE_VERIFIED_INVALID = 5;
+
+    private final Object mSessionLock = new Object();
+
+    private final Context mContext;
+    private final SessionStateChangeListener mListener;
+
+    public final BlobHandle blobHandle;
+    public final long sessionId;
+    public final int ownerUid;
+    public final String ownerPackageName;
+
+    // Do not access this directly, instead use getSessionFile().
+    private File mSessionFile;
+
+    @GuardedBy("mRevocableFds")
+    private ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>();
+
+    @GuardedBy("mSessionLock")
+    private int mState = STATE_CLOSED;
+
+    @GuardedBy("mSessionLock")
+    private final BlobAccessMode mBlobAccessMode = new BlobAccessMode();
+
+    @GuardedBy("mSessionLock")
+    private IBlobCommitCallback mBlobCommitCallback;
+
+    BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
+            int ownerUid, String ownerPackageName, SessionStateChangeListener listener) {
+        this.mContext = context;
+        this.blobHandle = blobHandle;
+        this.sessionId = sessionId;
+        this.ownerUid = ownerUid;
+        this.ownerPackageName = ownerPackageName;
+        this.mListener = listener;
+    }
+
+    boolean hasAccess(int callingUid, String callingPackageName) {
+        return ownerUid == callingUid && ownerPackageName.equals(callingPackageName);
+    }
+
+    void open() {
+        synchronized (mSessionLock) {
+            if (isFinalized()) {
+                throw new IllegalStateException("Not allowed to open session with state: "
+                        + stateToString(mState));
+            }
+            mState = STATE_OPENED;
+        }
+    }
+
+    int getState() {
+        synchronized (mSessionLock) {
+            return mState;
+        }
+    }
+
+    void sendCommitCallbackResult(int result) {
+        synchronized (mSessionLock) {
+            try {
+                mBlobCommitCallback.onResult(result);
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Error sending the callback result", e);
+            }
+            mBlobCommitCallback = null;
+        }
+    }
+
+    BlobAccessMode getBlobAccessMode() {
+        synchronized (mSessionLock) {
+            return mBlobAccessMode;
+        }
+    }
+
+    boolean isFinalized() {
+        synchronized (mSessionLock) {
+            return mState == STATE_COMMITTED || mState == STATE_ABANDONED;
+        }
+    }
+
     @Override
     @NonNull
     public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
             @BytesLong long lengthBytes) {
-        return null;
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to write in state: "
+                        + stateToString(mState));
+            }
+
+            try {
+                return openWriteLocked(offsetBytes, lengthBytes);
+            } catch (IOException e) {
+                throw ExceptionUtils.wrap(e);
+            }
+        }
+    }
+
+    @GuardedBy("mSessionLock")
+    @NonNull
+    private ParcelFileDescriptor openWriteLocked(@BytesLong long offsetBytes,
+            @BytesLong long lengthBytes) throws IOException {
+        // TODO: Add limit on active open sessions/writes/reads
+        FileDescriptor fd = null;
+        try {
+            final File sessionFile = getSessionFile();
+            if (sessionFile == null) {
+                throw new IllegalStateException("Couldn't get the file for this session");
+            }
+            fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600);
+            if (offsetBytes > 0) {
+                final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET);
+                if (curOffset != offsetBytes) {
+                    throw new IllegalStateException("Failed to seek " + offsetBytes
+                            + "; curOffset=" + offsetBytes);
+                }
+            }
+            if (lengthBytes > 0) {
+                mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes);
+            }
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        }
+        return createRevocableFdLocked(fd);
     }
 
     @Override
@@ -40,25 +195,192 @@
     @Override
     public void allowPackageAccess(@NonNull String packageName,
             @NonNull byte[] certificate) {
+        assertCallerIsOwner();
+        Preconditions.checkNotNull(packageName, "packageName must not be null");
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to change access type in state: "
+                        + stateToString(mState));
+            }
+            mBlobAccessMode.allowPackageAccess(packageName, certificate);
+        }
     }
 
     @Override
     public void allowSameSignatureAccess() {
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to change access type in state: "
+                        + stateToString(mState));
+            }
+            mBlobAccessMode.allowSameSignatureAccess();
+        }
     }
 
     @Override
     public void allowPublicAccess() {
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to change access type in state: "
+                        + stateToString(mState));
+            }
+            mBlobAccessMode.allowPublicAccess();
+        }
+    }
+
+    @Override
+    public boolean isPackageAccessAllowed(@NonNull String packageName,
+            @NonNull byte[] certificate) {
+        assertCallerIsOwner();
+        Preconditions.checkNotNull(packageName, "packageName must not be null");
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to get access type in state: "
+                        + stateToString(mState));
+            }
+            return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate);
+        }
+    }
+
+    @Override
+    public boolean isSameSignatureAccessAllowed() {
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to get access type in state: "
+                        + stateToString(mState));
+            }
+            return mBlobAccessMode.isSameSignatureAccessAllowed();
+        }
+    }
+
+    @Override
+    public boolean isPublicAccessAllowed() {
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                throw new IllegalStateException("Not allowed to get access type in state: "
+                        + stateToString(mState));
+            }
+            return mBlobAccessMode.isPublicAccessAllowed();
+        }
     }
 
     @Override
     public void close() {
+        closeSession(STATE_CLOSED);
     }
 
     @Override
     public void abandon() {
+        closeSession(STATE_ABANDONED);
     }
 
     @Override
     public void commit(IBlobCommitCallback callback) {
+        synchronized (mSessionLock) {
+            mBlobCommitCallback = callback;
+
+            closeSession(STATE_COMMITTED);
+        }
+    }
+
+    private void closeSession(int state) {
+        assertCallerIsOwner();
+        synchronized (mSessionLock) {
+            if (mState != STATE_OPENED) {
+                if (state == STATE_CLOSED) {
+                    // Just trying to close the session which is already deleted or abandoned,
+                    // ignore.
+                    return;
+                } else {
+                    throw new IllegalStateException("Not allowed to delete or abandon a session"
+                            + " with state: " + stateToString(mState));
+                }
+            }
+
+            mState = state;
+            revokeAllFdsLocked();
+
+            mListener.onStateChanged(this);
+        }
+    }
+
+    void verifyBlobData() {
+        byte[] actualDigest = null;
+        try {
+            actualDigest = FileUtils.digest(getSessionFile(), blobHandle.algorithm);
+        } catch (IOException | NoSuchAlgorithmException e) {
+            Slog.e(TAG, "Error computing the digest", e);
+        }
+        synchronized (mSessionLock) {
+            if (actualDigest != null && Arrays.equals(actualDigest, blobHandle.digest)) {
+                mState = STATE_VERIFIED_VALID;
+                // Commit callback will be sent once the data is persisted.
+            } else {
+                mState = STATE_VERIFIED_INVALID;
+                sendCommitCallbackResult(COMMIT_RESULT_ERROR);
+            }
+            mListener.onStateChanged(this);
+        }
+    }
+
+    @GuardedBy("mSessionLock")
+    private void revokeAllFdsLocked() {
+        for (int i = mRevocableFds.size() - 1; i >= 0; --i) {
+            mRevocableFds.get(i).revoke();
+            mRevocableFds.remove(i);
+        }
+    }
+
+    @GuardedBy("mSessionLock")
+    @NonNull
+    private ParcelFileDescriptor createRevocableFdLocked(FileDescriptor fd)
+            throws IOException {
+        final RevocableFileDescriptor revocableFd =
+                new RevocableFileDescriptor(mContext, fd);
+        synchronized (mRevocableFds) {
+            mRevocableFds.add(revocableFd);
+        }
+        revocableFd.addOnCloseListener((e) -> {
+            synchronized (mRevocableFds) {
+                mRevocableFds.remove(revocableFd);
+            }
+        });
+        return revocableFd.getRevocableFileDescriptor();
+    }
+
+    @Nullable
+    File getSessionFile() {
+        if (mSessionFile == null) {
+            mSessionFile = BlobStoreConfig.prepareBlobFile(sessionId);
+        }
+        return mSessionFile;
+    }
+
+    @NonNull
+    static String stateToString(int state) {
+        switch (state) {
+            case STATE_OPENED:
+                return "<opened>";
+            case STATE_CLOSED:
+                return "<closed>";
+            case STATE_ABANDONED:
+                return "<abandoned>";
+            case STATE_COMMITTED:
+                return "<committed>";
+            default:
+                Slog.wtf(TAG, "Unknown state: " + state);
+                return "<unknown>";
+        }
+    }
+
+    private void assertCallerIsOwner() {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != ownerUid) {
+            throw new SecurityException(ownerUid + " is not the session owner");
+        }
     }
 }
diff --git a/api/current.txt b/api/current.txt
index d70e6ca..be0936a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -7511,6 +7511,7 @@
     method public void acquireLease(@NonNull android.app.blob.BlobHandle, @IdRes int, long) throws java.io.IOException;
     method public void acquireLease(@NonNull android.app.blob.BlobHandle, @IdRes int) throws java.io.IOException;
     method @IntRange(from=1) public long createSession(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
+    method public void deleteSession(@IntRange(from=1) long) throws java.io.IOException;
     method @NonNull public android.os.ParcelFileDescriptor openBlob(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
     method @NonNull public android.app.blob.BlobStoreManager.Session openSession(@IntRange(from=1) long) throws java.io.IOException;
     method public void releaseLease(@NonNull android.app.blob.BlobHandle) throws java.io.IOException;
@@ -7524,6 +7525,9 @@
     method public void close() throws java.io.IOException;
     method public void commit(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>) throws java.io.IOException;
     method public long getSize() throws java.io.IOException;
+    method public boolean isPackageAccessAllowed(@NonNull String, @NonNull byte[]) throws java.io.IOException;
+    method public boolean isPublicAccessAllowed() throws java.io.IOException;
+    method public boolean isSameSignatureAccessAllowed() throws java.io.IOException;
     method @NonNull public android.os.ParcelFileDescriptor openWrite(long, long) throws java.io.IOException;
   }
 
diff --git a/core/java/android/os/RevocableFileDescriptor.java b/core/java/android/os/RevocableFileDescriptor.java
index a750ce6..ac2cd60 100644
--- a/core/java/android/os/RevocableFileDescriptor.java
+++ b/core/java/android/os/RevocableFileDescriptor.java
@@ -48,6 +48,8 @@
 
     private volatile boolean mRevoked;
 
+    private ParcelFileDescriptor.OnCloseListener mOnCloseListener;
+
     /** {@hide} */
     public RevocableFileDescriptor() {
     }
@@ -97,6 +99,14 @@
         IoUtils.closeQuietly(mInner);
     }
 
+    /**
+     * Callback for indicating that {@link ParcelFileDescriptor} passed to the client
+     * process ({@link #getRevocableFileDescriptor()}) has been closed.
+     */
+    public void addOnCloseListener(ParcelFileDescriptor.OnCloseListener onCloseListener) {
+        mOnCloseListener = onCloseListener;
+    }
+
     public boolean isRevoked() {
         return mRevoked;
     }
@@ -156,6 +166,9 @@
             if (DEBUG) Slog.v(TAG, "onRelease()");
             mRevoked = true;
             IoUtils.closeQuietly(mInner);
+            if (mOnCloseListener != null) {
+                mOnCloseListener.onClose(null);
+            }
         }
     };
 }