/*
 * Copyright 2019 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 android.app.blob;

import android.annotation.BytesLong;
import android.annotation.CallbackExecutor;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.IdRes;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
import android.os.LimitExceededException;
import android.os.ParcelFileDescriptor;
import android.os.ParcelableException;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;

import com.android.internal.util.function.pooled.PooledLambda;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

/**
 * This class provides access to the blob store managed by the system.
 *
 * <p> Apps can publish and access a data blob using a {@link BlobHandle} object which can
 * be created with {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)}.
 * This {@link BlobHandle} object encapsulates the following pieces of information used for
 * identifying the blobs:
 * <ul>
 *     <li> {@link BlobHandle#getSha256Digest()}
 *     <li> {@link BlobHandle#getLabel()}
 *     <li> {@link BlobHandle#getExpiryTimeMillis()}
 *     <li> {@link BlobHandle#getTag()}
 * </ul>
 * For two {@link BlobHandle} objects to be considered identical, all these pieces of information
 * must be equal.
 *
 * <p> For contributing a new data blob, an app needs to create a session using
 * {@link BlobStoreManager#createSession(BlobHandle)} and then open this session for writing using
 * {@link BlobStoreManager#openSession(long)}.
 *
 * <p> The following code snippet shows how to create and open a session for writing:
 * <pre class="prettyprint">
 *     final long sessionId = blobStoreManager.createSession(blobHandle);
 *     try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) {
 *         try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
 *                 session.openWrite(offsetBytes, lengthBytes))) {
 *             writeData(out);
 *         }
 *     }
 * </pre>
 *
 * <p> If all the data could not be written in a single attempt, apps can close this session
 * and re-open it again using the session id obtained via
 * {@link BlobStoreManager#createSession(BlobHandle)}. Note that the session data is persisted
 * and can be re-opened for completing the data contribution, even across device reboots.
 *
 * <p> After the data is written to the session, it can be committed using
 * {@link Session#commit(Executor, Consumer)}. Until the session is committed, data written
 * to the session will not be shared with any app.
 *
 * <p class="note"> Once a session is committed using {@link Session#commit(Executor, Consumer)},
 * any data written as part of this session is sealed and cannot be modified anymore.
 *
 * <p> Before committing the session, apps can indicate which apps are allowed to access the
 * contributed data using one or more of the following access modes:
 * <ul>
 *     <li> {@link Session#allowPackageAccess(String, byte[])} which will allow specific packages
 *          to access the blobs.
 *     <li> {@link Session#allowSameSignatureAccess()} which will allow only apps which are signed
 *          with the same certificate as the app which contributed the blob to access it.
 *     <li> {@link Session#allowPublicAccess()} which will allow any app on the device to access
 *          the blob.
 * </ul>
 *
 * <p> The following code snippet shows how to specify the access mode and commit the session:
 * <pre class="prettyprint">
 *     try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) {
 *         try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
 *                 session.openWrite(offsetBytes, lengthBytes))) {
 *             writeData(out);
 *         }
 *         session.allowSameSignatureAccess();
 *         session.allowPackageAccess(packageName, certificate);
 *         session.commit(executor, callback);
 *     }
 * </pre>
 *
 * <p> Apps that satisfy at least one of the access mode constraints specified by the publisher
 * of the data blob will be able to access it.
 *
 * <p> A data blob published without specifying any of
 * these access modes will be considered private and only the app that contributed the data
 * blob will be allowed to access it. This is still useful for overall device system health as
 * the System can try to keep one copy of data blob on disk when multiple apps contribute the
 * same data.
 *
 * <p class="note"> It is strongly recommended that apps use one of
 * {@link Session#allowPackageAccess(String, byte[])} or {@link Session#allowSameSignatureAccess()}
 * when they know, ahead of time, the set of apps they would like to share the blobs with.
 * {@link Session#allowPublicAccess()} is meant for publicly available data committed from
 * libraries and SDKs.
 *
 * <p> Once a data blob is committed with {@link Session#commit(Executor, Consumer)}, it
 * can be accessed using {@link BlobStoreManager#openBlob(BlobHandle)}, assuming the caller
 * satisfies constraints of any of the access modes associated with that data blob. An app may
 * acquire a lease on a blob with {@link BlobStoreManager#acquireLease(BlobHandle, int)} and
 * release the lease with {@link BlobStoreManager#releaseLease(BlobHandle)}. A blob will not be
 * deleted from the system while there is at least one app leasing it.
 *
 * <p> The following code snippet shows how to access the data blob:
 * <pre class="prettyprint">
 *     try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
 *             blobStoreManager.openBlob(blobHandle)) {
 *         useData(in);
 *     }
 * </pre>
 */
@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;

    /** @hide */
    public static final int INVALID_RES_ID = -1;

    private final Context mContext;
    private final IBlobStoreManager mService;

    /** @hide */
    public BlobStoreManager(@NonNull Context context, @NonNull IBlobStoreManager service) {
        mContext = context;
        mService = service;
    }

    /**
     * Create a new session using the given {@link BlobHandle}, returning a unique id
     * that represents the session. Once created, the session can be opened
     * multiple times across multiple device boots.
     *
     * <p> The system may automatically destroy sessions that have not been
     * finalized (either committed or abandoned) within a reasonable period of
     * time, typically about a week.
     *
     * <p> If an app is planning to acquire a lease on this data (using
     * {@link #acquireLease(BlobHandle, int)} or one of it's other variants) after committing
     * this data (using {@link Session#commit(Executor, Consumer)}), it is recommended that
     * the app checks the remaining quota for acquiring a lease first using
     * {@link #getRemainingLeaseQuotaBytes()} and can skip contributing this data if needed.
     *
     * @param blobHandle the {@link BlobHandle} identifier for which a new session
     *                   needs to be created.
     * @return positive, non-zero unique id that represents the created session.
     *         This id remains consistent across device reboots until the
     *         session is finalized. IDs are not reused during a given boot.
     *
     * @throws IOException when there is an I/O error while creating the session.
     * @throws SecurityException when the caller is not allowed to create a session, such
     *                           as when called from an Instant app.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     * @throws LimitExceededException when a new session could not be created, such as when the
     *                                caller is trying to create too many sessions.
     */
    public @IntRange(from = 1) long createSession(@NonNull BlobHandle blobHandle)
            throws IOException {
        try {
            return mService.createSession(blobHandle, mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            e.maybeRethrow(LimitExceededException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Open an existing session to actively perform work.
     *
     * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that
     *                  represents a particular session.
     * @return the {@link Session} object corresponding to the {@code sessionId}.
     *
     * @throws IOException when there is an I/O error while opening the session.
     * @throws SecurityException when the caller does not own the session, or
     *                           the session does not exist or is invalid.
     */
    public @NonNull Session openSession(@IntRange(from = 1) long sessionId) throws IOException {
        try {
            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();
        }
    }

    /**
     * Abandons an existing session and deletes 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 abandonSession(@IntRange(from = 1) long sessionId) throws IOException {
        try {
            mService.abandonSession(sessionId, mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Opens an existing blob for reading from the blob store managed by the system.
     *
     * @param blobHandle the {@link BlobHandle} representing the blob that the caller
     *                   wants to access.
     * @return a {@link ParcelFileDescriptor} that can be used to read the blob content.
     *
     * @throws IOException when there is an I/O while opening the blob for read.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     */
    public @NonNull ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle)
            throws IOException {
        try {
            return mService.openBlob(blobHandle, mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
     * system that the caller wants the blob to be kept around.
     *
     * <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.
     *
     * <p> When an app acquires a lease on a blob, the System will try to keep this
     * blob around but note that it can still be deleted if it was requested by the user.
     *
     * <p> In case the resource name for the {@code descriptionResId} is modified as part of
     * an app update, apps should re-acquire the lease with the new resource id.
     *
     * @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
     *                         to the user explaining what the blob is used for.
     * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be
     *                              automatically released, in {@link System#currentTimeMillis()}
     *                              timebase. If its value is {@code 0}, then the behavior of this
     *                              API is identical to {@link #acquireLease(BlobHandle, int)}
     *                              where clients have to explicitly call
     *                              {@link #releaseLease(BlobHandle)} when they don't
     *                              need the blob anymore.
     *
     * @throws IOException when there is an I/O error while acquiring a lease to the blob.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid or
     *                                  if the {@code leaseExpiryTimeMillis} is greater than the
     *                                  {@link BlobHandle#getExpiryTimeMillis()}.
     * @throws LimitExceededException when a lease could not be acquired, such as when the
     *                                caller is trying to acquire too many leases or acquire
     *                                leases on too much data. Apps can avoid this by checking
     *                                the remaining quota using
     *                                {@link #getRemainingLeaseQuotaBytes()} before trying to
     *                                acquire a lease.
     *
     * @see #acquireLease(BlobHandle, int)
     * @see #acquireLease(BlobHandle, CharSequence)
     */
    public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId,
            @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException {
        try {
            mService.acquireLease(blobHandle, descriptionResId, null, leaseExpiryTimeMillis,
                    mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            e.maybeRethrow(LimitExceededException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
     * system that the caller wants the blob to be kept around.
     *
     * <p> This is a variant of {@link #acquireLease(BlobHandle, int, long)} taking a
     * {@link CharSequence} for {@code description}. It is highly recommended that callers only
     * use this when a valid resource ID for {@code description} could not be provided. Otherwise,
     * apps should prefer using {@link #acquireLease(BlobHandle, int)} which will allow
     * {@code description} to be localized.
     *
     * <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.
     *
     * <p> When an app acquires a lease on a blob, the System will try to keep this
     * blob around but note that it can still be deleted if it was requested by the user.
     *
     * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
     *                   acquire a lease for.
     * @param description a short description string that can be surfaced
     *                    to the user explaining what the blob is used for. It is recommended to
     *                    keep this description brief. This may be truncated and ellipsized
     *                    if it is too long to be displayed to the user.
     * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be
     *                              automatically released, in {@link System#currentTimeMillis()}
     *                              timebase. If its value is {@code 0}, then the behavior of this
     *                              API is identical to {@link #acquireLease(BlobHandle, int)}
     *                              where clients have to explicitly call
     *                              {@link #releaseLease(BlobHandle)} when they don't
     *                              need the blob anymore.
     *
     * @throws IOException when there is an I/O error while acquiring a lease to the blob.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid or
     *                                  if the {@code leaseExpiryTimeMillis} is greater than the
     *                                  {@link BlobHandle#getExpiryTimeMillis()}.
     * @throws LimitExceededException when a lease could not be acquired, such as when the
     *                                caller is trying to acquire too many leases or acquire
     *                                leases on too much data. Apps can avoid this by checking
     *                                the remaining quota using
     *                                {@link #getRemainingLeaseQuotaBytes()} before trying to
     *                                acquire a lease.
     *
     * @see #acquireLease(BlobHandle, int, long)
     * @see #acquireLease(BlobHandle, CharSequence)
     */
    public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description,
            @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException {
        try {
            mService.acquireLease(blobHandle, INVALID_RES_ID, description, leaseExpiryTimeMillis,
                    mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            e.maybeRethrow(LimitExceededException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
     * system that the caller wants the blob to be kept around.
     *
     * <p> This is similar to {@link #acquireLease(BlobHandle, int, long)} except clients don't
     * have to specify the lease expiry time upfront using this API and need to explicitly
     * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep
     * a blob around.
     *
     * <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.
     *
     * <p> When an app acquires a lease on a blob, the System will try to keep this
     * blob around but note that it can still be deleted if it was requested by the user.
     *
     * <p> In case the resource name for the {@code descriptionResId} is modified as part of
     * an app update, apps should re-acquire the lease with the new resource id.
     *
     * @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
     *                         to the user explaining what the blob is used for.
     *
     * @throws IOException when there is an I/O error while acquiring a lease to the blob.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     * @throws LimitExceededException when a lease could not be acquired, such as when the
     *                                caller is trying to acquire too many leases or acquire
     *                                leases on too much data. Apps can avoid this by checking
     *                                the remaining quota using
     *                                {@link #getRemainingLeaseQuotaBytes()} before trying to
     *                                acquire a lease.
     *
     * @see #acquireLease(BlobHandle, int, long)
     * @see #acquireLease(BlobHandle, CharSequence, long)
     */
    public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId)
            throws IOException {
        acquireLease(blobHandle, descriptionResId, 0);
    }

    /**
     * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
     * system that the caller wants the blob to be kept around.
     *
     * <p> This is a variant of {@link #acquireLease(BlobHandle, int)} taking a {@link CharSequence}
     * for {@code description}. It is highly recommended that callers only use this when a valid
     * resource ID for {@code description} could not be provided. Otherwise, apps should prefer
     * using {@link #acquireLease(BlobHandle, int)} which will allow {@code description} to be
     * localized.
     *
     * <p> This is similar to {@link #acquireLease(BlobHandle, CharSequence, long)} except clients
     * don't have to specify the lease expiry time upfront using this API and need to explicitly
     * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep
     * a blob around.
     *
     * <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.
     *
     * <p> When an app acquires a lease on a blob, the System will try to keep this
     * blob around but note that it can still be deleted if it was requested by the user.
     *
     * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
     *                   acquire a lease for.
     * @param description a short description string that can be surfaced
     *                    to the user explaining what the blob is used for. It is recommended to
     *                    keep this description brief. This may be truncated and
     *                    ellipsized if it is too long to be displayed to the user.
     *
     * @throws IOException when there is an I/O error while acquiring a lease to the blob.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     * @throws LimitExceededException when a lease could not be acquired, such as when the
     *                                caller is trying to acquire too many leases or acquire
     *                                leases on too much data. Apps can avoid this by checking
     *                                the remaining quota using
     *                                {@link #getRemainingLeaseQuotaBytes()} before trying to
     *                                acquire a lease.
     *
     * @see #acquireLease(BlobHandle, int)
     * @see #acquireLease(BlobHandle, CharSequence, long)
     */
    public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description)
            throws IOException {
        acquireLease(blobHandle, description, 0);
    }

    /**
     * Release any active lease to the blob represented by {@code blobHandle} which is
     * currently held by the caller.
     *
     * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
     *                   release the lease for.
     *
     * @throws IOException when there is an I/O error while releasing the release to the blob.
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     */
    public void releaseLease(@NonNull BlobHandle blobHandle) throws IOException {
        try {
            mService.releaseLease(blobHandle, mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Release all the leases which are currently held by the caller.
     *
     * @hide
     */
    public void releaseAllLeases() throws Exception {
        try {
            mService.releaseAllLeases(mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the remaining quota size for acquiring a lease (in bytes) which indicates the
     * remaining amount of data that an app can acquire a lease on before the System starts
     * rejecting lease requests.
     *
     * If an app wants to acquire a lease on a blob but the remaining quota size is not sufficient,
     * then it can try releasing leases on any older blobs which are not needed anymore.
     *
     * @return the remaining quota size for acquiring a lease.
     */
    public @IntRange(from = 0) long getRemainingLeaseQuotaBytes() {
        try {
            return mService.getRemainingLeaseQuotaBytes(mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Wait until any pending tasks (like persisting data to disk) have finished.
     *
     * @hide
     */
    @TestApi
    public void waitForIdle(long timeoutMillis) throws InterruptedException, TimeoutException {
        try {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            mService.waitForIdle(new RemoteCallback((result) -> countDownLatch.countDown()));
            if (!countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
                throw new TimeoutException("Timed out waiting for service to become idle");
            }
        } catch (ParcelableException e) {
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    @NonNull
    public List<BlobInfo> queryBlobsForUser(@NonNull UserHandle user) throws IOException {
        try {
            return mService.queryBlobsForUser(user.getIdentifier());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void deleteBlob(@NonNull BlobInfo blobInfo) throws IOException {
        try {
            mService.deleteBlob(blobInfo.getId());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the {@link BlobHandle BlobHandles} corresponding to the data blobs that
     * the calling app currently has a lease on.
     *
     * @return a list of {@link BlobHandle BlobHandles} that the caller has a lease on.
     */
    @NonNull
    public List<BlobHandle> getLeasedBlobs() throws IOException {
        try {
            return mService.getLeasedBlobs(mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return {@link LeaseInfo} representing a lease acquired using
     * {@link #acquireLease(BlobHandle, int)} or one of it's other variants,
     * or {@code null} if there is no lease acquired.
     *
     * @throws SecurityException when the blob represented by the {@code blobHandle} does not
     *                           exist or the caller does not have access to it.
     * @throws IllegalArgumentException when {@code blobHandle} is invalid.
     *
     * @hide
     */
    @TestApi
    @Nullable
    public LeaseInfo getLeaseInfo(@NonNull BlobHandle blobHandle) throws IOException {
        try {
            return mService.getLeaseInfo(blobHandle, mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Represents an ongoing session of a blob's contribution to the blob store managed by the
     * system.
     *
     * <p> Clients that want to contribute a blob need to first create a {@link Session} using
     * {@link #createSession(BlobHandle)} and once the session is created, clients can open and
     * close this session multiple times using {@link #openSession(long)} and
     * {@link Session#close()} before committing it using
     * {@link Session#commit(Executor, Consumer)}, at which point system will take
     * ownership of the blob and the client can no longer make any modifications to the blob's
     * content.
     */
    public static class Session implements Closeable {
        private final IBlobStoreSession mSession;

        private Session(@NonNull IBlobStoreSession session) {
            mSession = session;
        }

        /**
         * Opens a file descriptor to write a blob into the session.
         *
         * <p> The returned file descriptor will start writing data at the requested offset
         * in the underlying file, which can be used to resume a partially
         * written file. If a valid file length is specified, the system will
         * preallocate the underlying disk space to optimize placement on disk.
         * It is strongly recommended to provide a valid file length when known.
         *
         * @param offsetBytes offset into the file to begin writing at, or 0 to
         *                    start at the beginning of the file.
         * @param lengthBytes total size of the file being written, used to
         *                    preallocate the underlying disk space, or -1 if unknown.
         *                    The system may clear various caches as needed to allocate
         *                    this space.
         *
         * @return a {@link ParcelFileDescriptor} for writing to the blob file.
         *
         * @throws IOException when there is an I/O error while opening the file to write.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to write to the file after it is
         *                               abandoned (using {@link #abandon()})
         *                               or committed (using {@link #commit})
         *                               or closed (using {@link #close()}).
         */
        public @NonNull ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
                @BytesLong long lengthBytes) throws IOException {
            try {
                final ParcelFileDescriptor pfd = mSession.openWrite(offsetBytes, lengthBytes);
                pfd.seekTo(offsetBytes);
                return pfd;
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Opens a file descriptor to read the blob content already written into this session.
         *
         * @return a {@link ParcelFileDescriptor} for reading from the blob file.
         *
         * @throws IOException when there is an I/O error while opening the file to read.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to read the file after it is
         *                               abandoned (using {@link #abandon()})
         *                               or closed (using {@link #close()}).
         */
        public @NonNull ParcelFileDescriptor openRead() throws IOException {
            try {
                return mSession.openRead();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Gets the size of the blob file that was written to the session so far.
         *
         * @return the size of the blob file so far.
         *
         * @throws IOException when there is an I/O error while opening the file to read.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to get the file size after it is
         *                               abandoned (using {@link #abandon()})
         *                               or closed (using {@link #close()}).
         */
        public @BytesLong long getSize() throws IOException {
            try {
                return mSession.getSize();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Close this session. It can be re-opened for writing/reading if it has not been
         * abandoned (using {@link #abandon}) or committed (using {@link #commit}).
         *
         * @throws IOException when there is an I/O error while closing the session.
         * @throws SecurityException when the caller is not the owner of the session.
         */
        public void close() throws IOException {
            try {
                mSession.close();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Abandon this session and delete any data that was written to this session so far.
         *
         * @throws IOException when there is an I/O error while abandoning the session.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to abandon a session which was
         *                               already finalized.
         */
        public void abandon() throws IOException {
            try {
                mSession.abandon();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Allow {@code packageName} with a particular signing certificate to access this blob
         * data once it is committed using a {@link BlobHandle} representing the blob.
         *
         * <p> This needs to be called before committing the blob using
         * {@link #commit(Executor, Consumer)}.
         *
         * @param packageName the name of the package which should be allowed to access the blob.
         * @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 changing the access.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to change access for a blob which is
         *                               already committed.
         * @throws LimitExceededException when the caller tries to explicitly allow too
         *                                many packages using this API.
         */
        public void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate)
                throws IOException {
            try {
                mSession.allowPackageAccess(packageName, certificate);
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                e.maybeRethrow(LimitExceededException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * 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.
         *
         * <p> This needs to be called before committing the blob using
         * {@link #commit(Executor, Consumer)}.
         *
         * @throws IOException when there is an I/O error while changing the access.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to change access for a blob which is
         *                               already committed.
         */
        public void allowSameSignatureAccess() throws IOException {
            try {
                mSession.allowSameSignatureAccess();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * 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.
         *
         * <p><strong>Note:</strong> This is only meant to be used from libraries and SDKs where
         * the apps which we want to allow access is not known ahead of time.
         * If a blob is being committed to be shared with a particular set of apps, it is highly
         * recommended to use {@link #allowPackageAccess(String, byte[])} instead.
         *
         * <p> This needs to be called before committing the blob using
         * {@link #commit(Executor, Consumer)}.
         *
         * @throws IOException when there is an I/O error while changing the access.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalStateException when the caller tries to change access for a blob which is
         *                               already committed.
         */
        public void allowPublicAccess() throws IOException {
            try {
                mSession.allowPublicAccess();
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * 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.
         *
         * <p> Once this method is called, the session is finalized and no additional
         * mutations can be performed on the session. If the device reboots
         * before the session has been finalized, you may commit the session again.
         *
         * <p> Note that this commit operation will fail if the hash of the data written so far
         * to this session does not match with the one used for
         * {@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.
         *
         * @throws IOException when there is an I/O error while committing the session.
         * @throws SecurityException when the caller is not the owner of the session.
         * @throws IllegalArgumentException when the passed parameters are not valid.
         * @throws IllegalStateException when the caller tries to commit a session which was
         *                               already finalized.
         */
        public void commit(@NonNull @CallbackExecutor Executor executor,
                @NonNull Consumer<Integer> resultCallback) throws IOException {
            try {
                mSession.commit(new IBlobCommitCallback.Stub() {
                    public void onResult(int result) {
                        executor.execute(PooledLambda.obtainRunnable(
                                Consumer::accept, resultCallback, result));
                    }
                });
            } catch (ParcelableException e) {
                e.maybeRethrow(IOException.class);
                throw new RuntimeException(e);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }
}
