diff --git a/api/current.txt b/api/current.txt index 7cb19a91566e7..64981ca7be2a9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9502,6 +9502,7 @@ package android.content { method public void dump(java.io.FileDescriptor, java.io.PrintWriter, String[]); method @Nullable public final String getCallingFeatureId(); method @Nullable public final String getCallingPackage(); + method @Nullable public final String getCallingPackageUnchecked(); method @Nullable public final android.content.Context getContext(); method @Nullable public final android.content.pm.PathPermission[] getPathPermissions(); method @Nullable public final String getReadPermission(); @@ -9511,6 +9512,7 @@ package android.content { method @Nullable public abstract android.net.Uri insert(@NonNull android.net.Uri, @Nullable android.content.ContentValues); method @Nullable public android.net.Uri insert(@NonNull android.net.Uri, @Nullable android.content.ContentValues, @Nullable android.os.Bundle); method protected boolean isTemporary(); + method public void onCallingPackageChanged(); method public void onConfigurationChanged(android.content.res.Configuration); method public abstract boolean onCreate(); method public void onLowMemory(); @@ -13321,6 +13323,7 @@ package android.database.sqlite { method @Deprecated public String buildUnionSubQuery(String, String[], java.util.Set, int, String, String, String[], String, String); method public int delete(@NonNull android.database.sqlite.SQLiteDatabase, @Nullable String, @Nullable String[]); method @Nullable public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory(); + method @Nullable public java.util.Collection getProjectionGreylist(); method @Nullable public java.util.Map getProjectionMap(); method @Nullable public String getTables(); method public long insert(@NonNull android.database.sqlite.SQLiteDatabase, @NonNull android.content.ContentValues); @@ -13333,6 +13336,7 @@ package android.database.sqlite { method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, String[], String, String[], String, String, String, String, android.os.CancellationSignal); method public void setCursorFactory(@Nullable android.database.sqlite.SQLiteDatabase.CursorFactory); method public void setDistinct(boolean); + method public void setProjectionGreylist(@Nullable java.util.Collection); method public void setProjectionMap(@Nullable java.util.Map); method public void setStrict(boolean); method public void setStrictColumns(boolean); @@ -36344,15 +36348,22 @@ package android.os.storage { method public boolean isObbMounted(String); method public boolean mountObb(String, String, android.os.storage.OnObbStateChangeListener); method @NonNull public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback, android.os.Handler) throws java.io.IOException; + method public void registerStorageVolumeCallback(@NonNull java.util.concurrent.Executor, @NonNull android.os.storage.StorageManager.StorageVolumeCallback); method public void setCacheBehaviorGroup(java.io.File, boolean) throws java.io.IOException; method public void setCacheBehaviorTombstone(java.io.File, boolean) throws java.io.IOException; method public boolean unmountObb(String, boolean, android.os.storage.OnObbStateChangeListener); + method public void unregisterStorageVolumeCallback(@NonNull android.os.storage.StorageManager.StorageVolumeCallback); field public static final String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; field public static final String EXTRA_REQUESTED_BYTES = "android.os.storage.extra.REQUESTED_BYTES"; field public static final String EXTRA_UUID = "android.os.storage.extra.UUID"; field public static final java.util.UUID UUID_DEFAULT; } + public static class StorageManager.StorageVolumeCallback { + ctor public StorageManager.StorageVolumeCallback(); + method public void onStateChanged(@NonNull android.os.storage.StorageVolume); + } + public final class StorageVolume implements android.os.Parcelable { method @Deprecated @Nullable public android.content.Intent createAccessIntent(String); method @NonNull public android.content.Intent createOpenDocumentTreeIntent(); diff --git a/api/system-current.txt b/api/system-current.txt index 653df732bf6f7..0ca6608d7887a 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -553,6 +553,7 @@ package android.app { } public class DownloadManager { + method @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public void onMediaStoreDownloadsDeleted(@NonNull android.util.LongSparseArray); field public static final String ACTION_DOWNLOAD_COMPLETED = "android.intent.action.DOWNLOAD_COMPLETED"; } @@ -1629,11 +1630,17 @@ package android.content { method @NonNull public final android.os.UserHandle getSendingUser(); } + public abstract class ContentProvider implements android.content.ComponentCallbacks2 { + method public int checkUriPermission(@NonNull android.net.Uri, int, int); + } + public class ContentProviderClient implements java.lang.AutoCloseable { method @RequiresPermission(android.Manifest.permission.REMOVE_TASKS) public void setDetectNotResponding(long); } public abstract class ContentResolver { + method @NonNull public static android.net.Uri decodeFromFile(@NonNull java.io.File); + method @NonNull public static java.io.File encodeToFile(@NonNull android.net.Uri); method @Nullable @RequiresPermission("android.permission.CACHE_CONTENT") public android.os.Bundle getCache(@NonNull android.net.Uri); method @RequiresPermission("android.permission.CACHE_CONTENT") public void putCache(@NonNull android.net.Uri, @Nullable android.os.Bundle); } @@ -6873,6 +6880,10 @@ package android.os { method public boolean hasSingleFileDescriptor(); } + public class ParcelFileDescriptor implements java.io.Closeable android.os.Parcelable { + method @NonNull public static android.os.ParcelFileDescriptor wrap(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.Handler, @NonNull android.os.ParcelFileDescriptor.OnCloseListener) throws java.io.IOException; + } + public final class PowerManager { method @RequiresPermission(allOf={android.Manifest.permission.READ_DREAM_STATE, android.Manifest.permission.WRITE_DREAM_STATE}) public void dream(long); method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public boolean forceSuspend(); @@ -7249,6 +7260,10 @@ package android.os.storage { field @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public static final int FLAG_ALLOCATE_AGGRESSIVE = 1; // 0x1 } + public final class StorageVolume implements android.os.Parcelable { + method @NonNull public String getId(); + } + } package android.permission { diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 49c389a0c4a77..1278ff6817fd6 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -16,7 +16,9 @@ package android.app; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; @@ -40,11 +42,13 @@ import android.os.Environment; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.provider.BaseColumns; import android.provider.Downloads; import android.provider.MediaStore; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; +import android.util.LongSparseArray; import android.util.Pair; import java.io.File; @@ -1068,6 +1072,37 @@ public class DownloadManager { mAccessFilename = accessFilename; } + /** + * Notify {@link DownloadManager} that the given {@link MediaStore} items + * were just deleted so that {@link DownloadManager} internal data + * structures can be cleaned up. + * + * @param idToMime map from {@link BaseColumns#_ID} to + * {@link ContentResolver#getType(Uri)}. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) + public void onMediaStoreDownloadsDeleted(@NonNull LongSparseArray idToMime) { + try (ContentProviderClient client = mResolver + .acquireUnstableContentProviderClient(mBaseUri)) { + final Bundle callExtras = new Bundle(); + final long[] ids = new long[idToMime.size()]; + final String[] mimeTypes = new String[idToMime.size()]; + for (int i = idToMime.size() - 1; i >= 0; --i) { + ids[i] = idToMime.keyAt(i); + mimeTypes[i] = idToMime.valueAt(i); + } + callExtras.putLongArray(android.provider.Downloads.EXTRA_IDS, ids); + callExtras.putStringArray(android.provider.Downloads.EXTRA_MIME_TYPES, + mimeTypes); + client.call(android.provider.Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED, + null, callExtras); + } catch (RemoteException e) { + // Should not happen + } + } + /** * Enqueue a new download. The download will start automatically once the download manager is * ready to execute it and connectivity is available. diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 393d48891d649..3eb0e7c9b504b 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -27,6 +27,7 @@ import static android.os.Trace.TRACE_TAG_DATABASE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.AppOpsManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.PackageManager; @@ -942,7 +943,18 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall return null; } - /** {@hide} */ + /** + * Return the package name of the caller that initiated the request being + * processed on the current thread. The returned package will have + * not been verified to belong to the calling UID. Returns + * {@code null} if not currently processing a request. + *

+ * This will always return {@code null} when processing + * {@link #getType(Uri)} or {@link #getStreamTypes(Uri, String)} requests. + * + * @see Binder#getCallingUid() + * @see Context#grantUriPermission(String, Uri, int) + */ public final @Nullable String getCallingPackageUnchecked() { final Pair pkg = mCallingPackage.get(); if (pkg != null) { @@ -952,7 +964,14 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall return null; } - /** {@hide} */ + /** + * Called whenever the value of {@link #getCallingPackage()} changes, giving + * the provider an opportunity to invalidate any security related caching it + * may be performing. + *

+ * This typically happens when a {@link ContentProvider} makes a nested call + * back into itself when already processing a call from a remote process. + */ public void onCallingPackageChanged() { } @@ -1525,8 +1544,24 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall return false; } - /** {@hide} */ + /** + * Perform a detailed internal check on a {@link Uri} to determine if a UID + * is able to access it with specific mode flags. + *

+ * This method is typically used when the provider implements more dynamic + * access controls that cannot be expressed with {@code } + * style static rules. + * + * @param uri the {@link Uri} to perform an access check on. + * @param uid the UID to check the permission for. + * @param modeFlags the access flags to use for the access check, such as + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}. + * @return {@link PackageManager#PERMISSION_GRANTED} if access is allowed, + * otherwise {@link PackageManager#PERMISSION_DENIED}. + * @hide + */ @Override + @SystemApi public int checkUriPermission(@NonNull Uri uri, int uid, @Intent.AccessUriMode int modeFlags) { return PackageManager.PERMISSION_DENIED; } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 1d3c6505f6772..592190e0ec115 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -3851,15 +3851,47 @@ public abstract class ContentResolver implements ContentInterface { } } + /** + * Decode a path generated by {@link #encodeToFile(Uri)} back into + * the original {@link Uri}. + *

+ * This is used to offer a way to intercept filesystem calls in + * {@link ContentProvider} unaware code and redirect them to a + * {@link ContentProvider} when they attempt to use {@code _DATA} columns + * that are otherwise deprecated. + * + * @hide + */ + @SystemApi + public static @NonNull Uri decodeFromFile(@NonNull File file) { + return translateDeprecatedDataPath(file.getAbsolutePath()); + } + + /** + * Encode a {@link Uri} into an opaque filesystem path which can then be + * resurrected by {@link #decodeFromFile(File)}. + *

+ * This is used to offer a way to intercept filesystem calls in + * {@link ContentProvider} unaware code and redirect them to a + * {@link ContentProvider} when they attempt to use {@code _DATA} columns + * that are otherwise deprecated. + * + * @hide + */ + @SystemApi + public static @NonNull File encodeToFile(@NonNull Uri uri) { + return new File(translateDeprecatedDataPath(uri)); + } + /** {@hide} */ - public static Uri translateDeprecatedDataPath(String path) { + public static @NonNull Uri translateDeprecatedDataPath(@NonNull String path) { final String ssp = "//" + path.substring(DEPRECATE_DATA_PREFIX.length()); return Uri.parse(new Uri.Builder().scheme(SCHEME_CONTENT) .encodedOpaquePart(ssp).build().toString()); } /** {@hide} */ - public static String translateDeprecatedDataPath(Uri uri) { + public static @NonNull String translateDeprecatedDataPath(@NonNull Uri uri) { return DEPRECATE_DATA_PREFIX + uri.getEncodedSchemeSpecificPart().substring(2); } } diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index bba14c39de722..5e4a78439500b 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -35,8 +35,8 @@ import com.android.internal.util.ArrayUtils; import libcore.util.EmptyArray; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -56,7 +56,7 @@ public class SQLiteQueryBuilder { "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); private Map mProjectionMap = null; - private List mProjectionGreylist = null; + private Collection mProjectionGreylist = null; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private String mTables = ""; @@ -196,20 +196,16 @@ public class SQLiteQueryBuilder { * Sets a projection greylist of columns that will be allowed through, even * when {@link #setStrict(boolean)} is enabled. This provides a way for * abusive custom columns like {@code COUNT(*)} to continue working. - * - * @hide */ - public void setProjectionGreylist(@Nullable List projectionGreylist) { + public void setProjectionGreylist(@Nullable Collection projectionGreylist) { mProjectionGreylist = projectionGreylist; } /** * Gets the projection greylist for the query, as last configured by - * {@link #setProjectionGreylist(List)}. - * - * @hide + * {@link #setProjectionGreylist}. */ - public @Nullable List getProjectionGreylist() { + public @Nullable Collection getProjectionGreylist() { return mProjectionGreylist; } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 983053bbe7fd7..89ddf8cbd96a3 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -31,6 +31,9 @@ import static android.system.OsConstants.S_ISLNK; import static android.system.OsConstants.S_ISREG; import static android.system.OsConstants.S_IWOTH; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; @@ -253,6 +256,9 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * be opened with the requested mode. * @see #parseMode(String) */ + // We can't accept a generic Executor here, since we need to use + // MessageQueue.addOnFileDescriptorEventListener() + @SuppressLint("ExecutorRegistration") public static ParcelFileDescriptor open(File file, int mode, Handler handler, final OnCloseListener listener) throws IOException { if (handler == null) { @@ -268,9 +274,22 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { return fromFd(fd, handler, listener); } - /** {@hide} */ - public static ParcelFileDescriptor fromPfd(ParcelFileDescriptor pfd, Handler handler, - final OnCloseListener listener) throws IOException { + /** + * Create a new ParcelFileDescriptor wrapping an already-opened file. + * + * @param pfd The already-opened file. + * @param handler to call listener from. + * @param listener to be invoked when the returned descriptor has been + * closed. + * @return a new ParcelFileDescriptor pointing to the given file. + * @hide + */ + @SystemApi + // We can't accept a generic Executor here, since we need to use + // MessageQueue.addOnFileDescriptorEventListener() + @SuppressLint("ExecutorRegistration") + public static @NonNull ParcelFileDescriptor wrap(@NonNull ParcelFileDescriptor pfd, + @NonNull Handler handler, @NonNull OnCloseListener listener) throws IOException { final FileDescriptor original = new FileDescriptor(); original.setInt$(pfd.detachFd()); return fromFd(original, handler, listener); diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 8959fcf7ac18d..3ea64f13fe6bf 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -31,6 +31,7 @@ import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.annotation.BytesLong; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -72,6 +73,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.SystemProperties; +import android.os.UserHandle; import android.provider.MediaStore; import android.provider.Settings; import android.sysprop.VoldProperties; @@ -93,7 +95,6 @@ import com.android.internal.os.AppFuseMount; import com.android.internal.os.FuseAppLoop; import com.android.internal.os.FuseUnavailableMountException; import com.android.internal.os.RoSystemProperties; -import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; import dalvik.system.BlockGuard; @@ -114,6 +115,7 @@ import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -305,109 +307,85 @@ public class StorageManager { private final Looper mLooper; private final AtomicInteger mNextNonce = new AtomicInteger(0); + @GuardedBy("mDelegates") private final ArrayList mDelegates = new ArrayList<>(); - private static class StorageEventListenerDelegate extends IStorageEventListener.Stub implements - Handler.Callback { - private static final int MSG_STORAGE_STATE_CHANGED = 1; - private static final int MSG_VOLUME_STATE_CHANGED = 2; - private static final int MSG_VOLUME_RECORD_CHANGED = 3; - private static final int MSG_VOLUME_FORGOTTEN = 4; - private static final int MSG_DISK_SCANNED = 5; - private static final int MSG_DISK_DESTROYED = 6; + private class StorageEventListenerDelegate extends IStorageEventListener.Stub { + final Executor mExecutor; + final StorageEventListener mListener; + final StorageVolumeCallback mCallback; - final StorageEventListener mCallback; - final Handler mHandler; - - public StorageEventListenerDelegate(StorageEventListener callback, Looper looper) { + public StorageEventListenerDelegate(@NonNull Executor executor, + @NonNull StorageEventListener listener, @NonNull StorageVolumeCallback callback) { + mExecutor = executor; + mListener = listener; mCallback = callback; - mHandler = new Handler(looper, this); - } - - @Override - public boolean handleMessage(Message msg) { - final SomeArgs args = (SomeArgs) msg.obj; - switch (msg.what) { - case MSG_STORAGE_STATE_CHANGED: - mCallback.onStorageStateChanged((String) args.arg1, (String) args.arg2, - (String) args.arg3); - args.recycle(); - return true; - case MSG_VOLUME_STATE_CHANGED: - mCallback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3); - args.recycle(); - return true; - case MSG_VOLUME_RECORD_CHANGED: - mCallback.onVolumeRecordChanged((VolumeRecord) args.arg1); - args.recycle(); - return true; - case MSG_VOLUME_FORGOTTEN: - mCallback.onVolumeForgotten((String) args.arg1); - args.recycle(); - return true; - case MSG_DISK_SCANNED: - mCallback.onDiskScanned((DiskInfo) args.arg1, args.argi2); - args.recycle(); - return true; - case MSG_DISK_DESTROYED: - mCallback.onDiskDestroyed((DiskInfo) args.arg1); - args.recycle(); - return true; - } - args.recycle(); - return false; } @Override public void onUsbMassStorageConnectionChanged(boolean connected) throws RemoteException { - // Ignored + mExecutor.execute(() -> { + mListener.onUsbMassStorageConnectionChanged(connected); + }); } @Override public void onStorageStateChanged(String path, String oldState, String newState) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = path; - args.arg2 = oldState; - args.arg3 = newState; - mHandler.obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onStorageStateChanged(path, oldState, newState); + + if (path != null) { + for (StorageVolume sv : getStorageVolumes()) { + if (Objects.equals(path, sv.getPath())) { + mCallback.onStateChanged(sv); + } + } + } + }); } @Override public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = vol; - args.argi2 = oldState; - args.argi3 = newState; - mHandler.obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onVolumeStateChanged(vol, oldState, newState); + + final File path = vol.getPathForUser(UserHandle.myUserId()); + if (path != null) { + for (StorageVolume sv : getStorageVolumes()) { + if (Objects.equals(path.getAbsolutePath(), sv.getPath())) { + mCallback.onStateChanged(sv); + } + } + } + }); } @Override public void onVolumeRecordChanged(VolumeRecord rec) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = rec; - mHandler.obtainMessage(MSG_VOLUME_RECORD_CHANGED, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onVolumeRecordChanged(rec); + }); } @Override public void onVolumeForgotten(String fsUuid) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = fsUuid; - mHandler.obtainMessage(MSG_VOLUME_FORGOTTEN, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onVolumeForgotten(fsUuid); + }); } @Override public void onDiskScanned(DiskInfo disk, int volumeCount) { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = disk; - args.argi2 = volumeCount; - mHandler.obtainMessage(MSG_DISK_SCANNED, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onDiskScanned(disk, volumeCount); + }); } @Override public void onDiskDestroyed(DiskInfo disk) throws RemoteException { - final SomeArgs args = SomeArgs.obtain(); - args.arg1 = disk; - mHandler.obtainMessage(MSG_DISK_DESTROYED, args).sendToTarget(); + mExecutor.execute(() -> { + mListener.onDiskDestroyed(disk); + }); } } @@ -525,8 +503,8 @@ public class StorageManager { @UnsupportedAppUsage public void registerListener(StorageEventListener listener) { synchronized (mDelegates) { - final StorageEventListenerDelegate delegate = new StorageEventListenerDelegate(listener, - mLooper); + final StorageEventListenerDelegate delegate = new StorageEventListenerDelegate( + mContext.getMainExecutor(), listener, new StorageVolumeCallback()); try { mStorageManager.registerListener(delegate); } catch (RemoteException e) { @@ -548,7 +526,76 @@ public class StorageManager { synchronized (mDelegates) { for (Iterator i = mDelegates.iterator(); i.hasNext();) { final StorageEventListenerDelegate delegate = i.next(); - if (delegate.mCallback == listener) { + if (delegate.mListener == listener) { + try { + mStorageManager.unregisterListener(delegate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + i.remove(); + } + } + } + } + + /** + * Callback that delivers {@link StorageVolume} related events. + *

+ * For example, this can be used to detect when a volume changes to the + * {@link Environment#MEDIA_MOUNTED} or {@link Environment#MEDIA_UNMOUNTED} + * states. + * + * @see StorageManager#registerStorageVolumeCallback + * @see StorageManager#unregisterStorageVolumeCallback + */ + public static class StorageVolumeCallback { + /** + * Called when {@link StorageVolume#getState()} changes, such as + * changing to the {@link Environment#MEDIA_MOUNTED} or + * {@link Environment#MEDIA_UNMOUNTED} states. + *

+ * The given argument is a snapshot in time and can be used to process + * events in the order they occurred, or you can call + * {@link StorageManager#getStorageVolumes()} to observe the latest + * value. + */ + public void onStateChanged(@NonNull StorageVolume volume) { } + } + + /** + * Registers the given callback to listen for {@link StorageVolume} changes. + *

+ * For example, this can be used to detect when a volume changes to the + * {@link Environment#MEDIA_MOUNTED} or {@link Environment#MEDIA_UNMOUNTED} + * states. + * + * @see StorageManager#unregisterStorageVolumeCallback + */ + public void registerStorageVolumeCallback(@CallbackExecutor @NonNull Executor executor, + @NonNull StorageVolumeCallback callback) { + synchronized (mDelegates) { + final StorageEventListenerDelegate delegate = new StorageEventListenerDelegate( + executor, new StorageEventListener(), callback); + try { + mStorageManager.registerListener(delegate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mDelegates.add(delegate); + } + } + + /** + * Unregisters the given callback from listening for {@link StorageVolume} + * changes. + * + * @see StorageManager#registerStorageVolumeCallback + */ + public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) { + synchronized (mDelegates) { + for (Iterator i = mDelegates.iterator(); i.hasNext();) { + final StorageEventListenerDelegate delegate = i.next(); + if (delegate.mCallback == callback) { try { mStorageManager.unregisterListener(delegate); } catch (RemoteException e) { diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 2ab226f81bb40..e251f8072b1fa 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -18,6 +18,7 @@ package android.os.storage; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -162,9 +163,13 @@ public final class StorageVolume implements Parcelable { mState = in.readString(); } - /** {@hide} */ - @UnsupportedAppUsage - public String getId() { + /** + * Return an opaque ID that can be used to identify this volume. + * + * @hide + */ + @SystemApi + public @NonNull String getId() { return mId; }