Final push to build against SDK.

The bulk of the work needed to get MediaProvider building against
the "system_current" SDK surface has been slowly merged over the
last few months, and this change makes the last few adjustments.

This adds a new StorageVolumeCallback which is simpler version of
StorageEventListener that simply delivers the changed StorageVolume.

Move DownloadManager logic into a onMediaStoreDownloadsDeleted()
method which hides the implementation details of how the OS connects
with that implementation.

Make local copies of some ExifInterface parsing logic; they could
be added to the androidx version in an unbundled release.  Make a
local copy of RedactingFileDescriptor, since it's only needed for
the next few weeks until FUSE is globally enabled.

Bug: 137890034
Test: atest --test-mapping packages/providers/MediaProvider
Change-Id: Ib416eb8724781bdd234c8b7d728dee8b695ad6ac
This commit is contained in:
Jeff Sharkey
2020-01-07 22:06:37 -07:00
parent cb8823d980
commit 197fe1f90f
9 changed files with 290 additions and 95 deletions

View File

@@ -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<java.lang.String>, 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<java.util.regex.Pattern> getProjectionGreylist();
method @Nullable public java.util.Map<java.lang.String,java.lang.String> 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<java.util.regex.Pattern>);
method public void setProjectionMap(@Nullable java.util.Map<java.lang.String,java.lang.String>);
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();

View File

@@ -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<java.lang.String>);
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 {

View File

@@ -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<String> 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.

View File

@@ -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
* <em>not</em> been verified to belong to the calling UID. Returns
* {@code null} if not currently processing a request.
* <p>
* 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<String, String> 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.
* <p>
* 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.
* <p>
* This method is typically used when the provider implements more dynamic
* access controls that cannot be expressed with {@code <path-permission>}
* 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;
}

View File

@@ -3851,15 +3851,47 @@ public abstract class ContentResolver implements ContentInterface {
}
}
/**
* Decode a path generated by {@link #encodeToFile(Uri)} back into
* the original {@link Uri}.
* <p>
* 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)}.
* <p>
* 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);
}
}

View File

@@ -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<String, String> mProjectionMap = null;
private List<Pattern> mProjectionGreylist = null;
private Collection<Pattern> 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<Pattern> projectionGreylist) {
public void setProjectionGreylist(@Nullable Collection<Pattern> projectionGreylist) {
mProjectionGreylist = projectionGreylist;
}
/**
* Gets the projection greylist for the query, as last configured by
* {@link #setProjectionGreylist(List)}.
*
* @hide
* {@link #setProjectionGreylist}.
*/
public @Nullable List<Pattern> getProjectionGreylist() {
public @Nullable Collection<Pattern> getProjectionGreylist() {
return mProjectionGreylist;
}

View File

@@ -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);

View File

@@ -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<StorageEventListenerDelegate> 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<StorageEventListenerDelegate> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) {
final StorageEventListenerDelegate delegate = i.next();
if (delegate.mCallback == callback) {
try {
mStorageManager.unregisterListener(delegate);
} catch (RemoteException e) {

View File

@@ -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;
}