/* * 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. * *

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: *

* For two {@link BlobHandle} objects to be considered identical, all these pieces of information * must be equal. * *

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)}. * *

The following code snippet shows how to create and open a session for writing: *

 *     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);
 *         }
 *     }
 * 
* *

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. * *

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. * *

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. * *

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: *

* *

The following code snippet shows how to specify the access mode and commit the session: *

 *     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);
 *     }
 * 
* *

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. * *

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. * *

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. * *

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. * *

The following code snippet shows how to access the data blob: *

 *     try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
 *             blobStoreManager.openBlob(blobHandle)) {
 *         useData(in);
 *     }
 * 
*/ @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. * *

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. * *

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. * *

Any active leases will be automatically released when the blob's expiry time * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. * *

This lease information is persisted and calling this more than once will result in * latest lease overriding any previous lease. * *

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. * *

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. * *

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. * *

Any active leases will be automatically released when the blob's expiry time * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. * *

This lease information is persisted and calling this more than once will result in * latest lease overriding any previous lease. * *

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. * *

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. * *

Any active leases will be automatically released when the blob's expiry time * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. * *

This lease information is persisted and calling this more than once will result in * latest lease overriding any previous lease. * *

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. * *

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. * *

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. * *

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. * *

Any active leases will be automatically released when the blob's expiry time * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. * *

This lease information is persisted and calling this more than once will result in * latest lease overriding any previous lease. * *

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 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 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. * *

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. * *

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. * *

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. * *

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. * *

Note: 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. * *

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. * *

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. * *

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. * *

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 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(); } } } }