Merge "Delete database entry for files deleted by apps that bypass restrictions"
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 282a088..d8c2f46 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -299,6 +299,7 @@
std::recursive_mutex lock;
const string path;
node* const root;
+ struct fuse_session* se;
/*
* Used to make JNI calls to MediaProvider.
@@ -1540,6 +1541,31 @@
return use_fuse;
}
+void FuseDaemon::InvalidateFuseDentryCache(const std::string& path) {
+ TRACE_VERBOSE << "Invalidating dentry for path " << path;
+
+ if (active.load(std::memory_order_acquire)) {
+ string name;
+ fuse_ino_t parent;
+
+ {
+ std::lock_guard<std::recursive_mutex> guard(fuse->lock);
+ const node* node = node::LookupAbsolutePath(fuse->root, path);
+ if (node) {
+ name = node->GetName();
+ parent = fuse->ToInode(node->GetParent());
+ }
+ }
+
+ if (!name.empty() &&
+ fuse_lowlevel_notify_inval_entry(fuse->se, parent, name.c_str(), name.size())) {
+ LOG(ERROR) << "Failed to invalidate dentry for path " << path;
+ }
+ } else {
+ TRACE << "FUSE daemon is inactive. Cannot invalidate dentry for " << path;
+ }
+}
+
FuseDaemon::FuseDaemon(JNIEnv* env, jobject mediaProvider) : mp(env, mediaProvider),
active(false), fuse(nullptr) {}
@@ -1594,6 +1620,7 @@
PLOG(ERROR) << "Failed to create session ";
return;
}
+ fuse_default.se = se;
se->fd = fd;
se->mountpoint = strdup(path.c_str());
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index 1233863..1d25636 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -42,6 +42,11 @@
*/
bool ShouldOpenWithFuse(int fd, bool for_read, const std::string& path);
+ /**
+ * Invalidate FUSE VFS dentry cache entry for path
+ */
+ void InvalidateFuseDentryCache(const std::string& path);
+
private:
FuseDaemon(const FuseDaemon&) = delete;
void operator=(const FuseDaemon&) = delete;
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 821dc8a..59588db 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-#define LOG_TAG "MediaProviderWrapper"
-
#include "MediaProviderWrapper.h"
#include "libfuse_jni/ReaddirHelper.h"
@@ -39,7 +37,6 @@
namespace {
constexpr const char* kPropRedactionEnabled = "persist.sys.fuse.redaction-enabled";
-constexpr const char* kPropShellBypass = "persist.sys.fuse.shell-bypass";
constexpr uid_t ROOT_UID = 0;
constexpr uid_t SHELL_UID = 2000;
@@ -47,7 +44,7 @@
/** Private helper functions **/
inline bool shouldBypassMediaProvider(uid_t uid) {
- return (uid == SHELL_UID && GetBoolProperty(kPropShellBypass, false)) || uid == ROOT_UID;
+ return uid == SHELL_UID || uid == ROOT_UID;
}
bool CheckForJniException(JNIEnv* env) {
@@ -62,7 +59,6 @@
std::unique_ptr<RedactionInfo> getRedactionInfoInternal(JNIEnv* env, jobject media_provider_object,
jmethodID mid_get_redaction_ranges,
uid_t uid, const string& path) {
- LOG(DEBUG) << "Computing redaction ranges for uid = " << uid << " file = " << path;
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
ScopedLongArrayRO redaction_ranges(
env, static_cast<jlongArray>(env->CallObjectMethod(
@@ -78,11 +74,9 @@
LOG(ERROR) << "Error while calculating redaction ranges: array length is uneven";
} else if (redaction_ranges.size() > 0) {
ri = std::make_unique<RedactionInfo>(redaction_ranges.size() / 2, redaction_ranges.get());
- LOG(DEBUG) << "Redaction ranges computed. Number of ranges = " << ri->size();
} else {
// No ranges to redact
ri = std::make_unique<RedactionInfo>();
- LOG(DEBUG) << "Redaction ranges computed. No ranges to redact.";
}
return ri;
@@ -90,77 +84,72 @@
int insertFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_insert_file,
const string& path, uid_t uid) {
- LOG(DEBUG) << "Inserting file for UID = " << uid << ". Path = " << path;
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
int res = env->CallIntMethod(media_provider_object, mid_insert_file, j_path.get(), uid);
if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception while creating file";
return EFAULT;
}
- LOG(DEBUG) << "res = " << res;
return res;
}
int deleteFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_delete_file,
const string& path, uid_t uid) {
- LOG(DEBUG) << "Delete file for UID = " << uid << ". Path = " << path;
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
int res = env->CallIntMethod(media_provider_object, mid_delete_file, j_path.get(), uid);
if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception while deleting file";
return EFAULT;
}
- LOG(DEBUG) << "res = " << res;
return res;
}
int isOpenAllowedInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_is_open_allowed,
const string& path, uid_t uid, bool for_write) {
- LOG(DEBUG) << "Checking if UID = " << uid << " can open file " << path << " for "
- << (for_write ? "write" : "read only");
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
int res = env->CallIntMethod(media_provider_object, mid_is_open_allowed, j_path.get(), uid,
for_write);
if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception while checking permissions for file";
return EFAULT;
}
- LOG(DEBUG) << "res = " << res;
return res;
}
void scanFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_scan_file,
const string& path) {
- LOG(DEBUG) << "Notifying MediaProvider that a file has been modified. path = " << path;
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
env->CallVoidMethod(media_provider_object, mid_scan_file, j_path.get());
- if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception while checking permissions for file";
- }
- LOG(DEBUG) << "MediaProvider has been notified";
+ CheckForJniException(env);
}
-int isDirectoryOperationAllowedInternal(JNIEnv* env, jobject media_provider_object,
- jmethodID mid_is_dir_op_allowed, const string& path,
- uid_t uid) {
+int isMkdirOrRmdirAllowedInternal(JNIEnv* env, jobject media_provider_object,
+ jmethodID mid_is_mkdir_or_rmdir_allowed, const string& path,
+ uid_t uid, bool forCreate) {
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
- int res = env->CallIntMethod(media_provider_object, mid_is_dir_op_allowed, j_path.get(), uid);
+ int res = env->CallIntMethod(media_provider_object, mid_is_mkdir_or_rmdir_allowed, j_path.get(),
+ uid, forCreate);
if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception while checking permissions for creating/deleting/opening dir";
return EFAULT;
}
- LOG(DEBUG) << "res = " << res;
+ return res;
+}
+
+int isOpendirAllowedInternal(JNIEnv* env, jobject media_provider_object,
+ jmethodID mid_is_opendir_allowed, const string& path, uid_t uid) {
+ ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+ int res = env->CallIntMethod(media_provider_object, mid_is_opendir_allowed, j_path.get(), uid);
+
+ if (CheckForJniException(env)) {
+ return EFAULT;
+ }
return res;
}
std::vector<std::shared_ptr<DirectoryEntry>> getFilesInDirectoryInternal(
JNIEnv* env, jobject media_provider_object, jmethodID mid_get_files_in_dir, uid_t uid,
const string& path) {
- LOG(DEBUG) << "Getting file names in path " << path << " for UID = " << uid;
std::vector<std::shared_ptr<DirectoryEntry>> directory_entries;
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
@@ -169,7 +158,6 @@
media_provider_object, mid_get_files_in_dir, j_path.get(), uid)));
if (CheckForJniException(env)) {
- LOG(ERROR) << "Exception occurred while calling MediaProvider#getFilesInDirectoryForFuse";
directory_entries.push_back(std::make_shared<DirectoryEntry>("", EFAULT));
return directory_entries;
}
@@ -208,16 +196,13 @@
int renameInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_rename,
const string& old_path, const string& new_path, uid_t uid) {
- LOG(DEBUG) << "Renaming " << old_path << " to " << new_path << ", uid: " << uid;
ScopedLocalRef<jstring> j_old_path(env, env->NewStringUTF(old_path.c_str()));
ScopedLocalRef<jstring> j_new_path(env, env->NewStringUTF(new_path.c_str()));
int res = env->CallIntMethod(media_provider_object, mid_rename, j_old_path.get(),
j_new_path.get(), uid);
if (CheckForJniException(env)) {
- LOG(DEBUG) << "Java exception occurred while renaming a file or directory";
return EFAULT;
}
- LOG(DEBUG) << "res = " << res;
return res;
}
} // namespace
@@ -251,8 +236,8 @@
/*is_static*/ false);
mid_scan_file_ = CacheMethod(env, "scanFile", "(Ljava/lang/String;)V",
/*is_static*/ false);
- mid_is_dir_op_allowed_ = CacheMethod(env, "isDirectoryOperationAllowed",
- "(Ljava/lang/String;I)I", /*is_static*/ false);
+ mid_is_mkdir_or_rmdir_allowed_ = CacheMethod(env, "isDirectoryCreationOrDeletionAllowed",
+ "(Ljava/lang/String;IZ)I", /*is_static*/ false);
mid_is_opendir_allowed_ = CacheMethod(env, "isOpendirAllowed", "(Ljava/lang/String;I)I",
/*is_static*/ false);
mid_get_files_in_dir_ =
@@ -278,7 +263,7 @@
// because the flag value has already been changed. This ensures that the
// termination task is the last task in the queue.
- LOG(DEBUG) << "Posting task to terminate JNI thread";
+ LOG(INFO) << "Posting task to terminate JNI thread";
// async task doesn't check jni_tasks_welcome_ - but we will wait for the thread to terminate
// anyway
PostAsyncTask([this](JNIEnv* env) {
@@ -371,9 +356,9 @@
int res = EIO; // Default value in case JNI thread was being terminated
PostAndWaitForTask([this, &path, uid, &res](JNIEnv* env) {
- LOG(DEBUG) << "Checking if UID = " << uid << " can create dir " << path;
- res = isDirectoryOperationAllowedInternal(env, media_provider_object_,
- mid_is_dir_op_allowed_, path, uid);
+ res = isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
+ mid_is_mkdir_or_rmdir_allowed_, path, uid,
+ /*forCreate*/ true);
});
return res;
@@ -387,9 +372,9 @@
int res = EIO; // Default value in case JNI thread was being terminated
PostAndWaitForTask([this, &path, uid, &res](JNIEnv* env) {
- LOG(DEBUG) << "Checking if UID = " << uid << " can delete dir " << path;
- res = isDirectoryOperationAllowedInternal(env, media_provider_object_,
- mid_is_dir_op_allowed_, path, uid);
+ res = isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
+ mid_is_mkdir_or_rmdir_allowed_, path, uid,
+ /*forCreate*/ false);
});
return res;
}
@@ -428,9 +413,8 @@
int res = EIO; // Default value in case JNI thread was being terminated
PostAndWaitForTask([this, &path, uid, &res](JNIEnv* env) {
- LOG(DEBUG) << "Checking if UID = " << uid << " can open dir " << path;
- res = isDirectoryOperationAllowedInternal(env, media_provider_object_,
- mid_is_opendir_allowed_, path, uid);
+ res = isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path,
+ uid);
});
return res;
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index b3a8f68..64c27f6 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -164,7 +164,7 @@
jmethodID mid_delete_file_;
jmethodID mid_is_open_allowed_;
jmethodID mid_scan_file_;
- jmethodID mid_is_dir_op_allowed_;
+ jmethodID mid_is_mkdir_or_rmdir_allowed_;
jmethodID mid_is_opendir_allowed_;
jmethodID mid_get_files_in_dir_;
jmethodID mid_rename_;
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
index caf00b2..87861ca 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -73,6 +73,22 @@
return JNI_FALSE;
}
+void com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache(JNIEnv* env, jobject self,
+ jlong java_daemon,
+ jstring java_path) {
+ fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+ if (daemon) {
+ ScopedUtfChars utf_chars_path(env, java_path);
+ if (!utf_chars_path.c_str()) {
+ // TODO(b/145741152): Throw exception
+ return;
+ }
+
+ daemon->InvalidateFuseDentryCache(utf_chars_path.c_str());
+ }
+ // TODO(b/145741152): Throw exception
+}
+
const JNINativeMethod methods[] = {
{"native_new", "(Lcom/android/providers/media/MediaProvider;)J",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_new)},
@@ -81,8 +97,10 @@
{"native_delete", "(J)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete)},
{"native_should_open_with_fuse", "(JLjava/lang/String;ZI)Z",
- reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_should_open_with_fuse)}};
-
+ reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_should_open_with_fuse)},
+ {"native_invalidate_fuse_dentry_cache", "(JLjava/lang/String;)V",
+ reinterpret_cast<void*>(
+ com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache)}};
} // namespace
void register_android_providers_media_FuseDaemon(JNIEnv* env) {
diff --git a/jni/node-inl.h b/jni/node-inl.h
index 76d1ba9..e21ae54 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -160,6 +160,11 @@
return name_;
}
+ node* GetParent() const {
+ std::lock_guard<std::recursive_mutex> guard(*lock_);
+ return parent_;
+ }
+
inline void AddHandle(handle* h) {
std::lock_guard<std::recursive_mutex> guard(*lock_);
handles_.emplace_back(std::unique_ptr<handle>(h));
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 4943d6b..e95bc83 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -1269,4 +1269,8 @@
"SELECT COUNT(_id) FROM files WHERE " + FileColumns.MIME_TYPE + " IS NOT NULL",
null);
}
+
+ public boolean isExternal() {
+ return !mInternal;
+ }
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index b152515..2dafd9d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -252,6 +252,7 @@
private static final String DIRECTORY_DOCUMENTS = "Documents";
private static final String DIRECTORY_AUDIOBOOKS = "Audiobooks";
private static final String DIRECTORY_ANDROID = "Android";
+
private static final String DIRECTORY_MEDIA = "media";
/**
@@ -441,12 +442,50 @@
}
};
+ private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
+ File file;
+ try {
+ file = queryForDataFile(uri, null);
+ } catch (FileNotFoundException e) {
+ // Ignore
+ return;
+ }
+ try {
+ switch (mediaType) {
+ case FileColumns.MEDIA_TYPE_AUDIO:
+ mStorageManager.updateExternalStorageFileQuotaType(file,
+ StorageManager.QUOTA_TYPE_MEDIA_AUDIO);
+ break;
+ case FileColumns.MEDIA_TYPE_VIDEO:
+ mStorageManager.updateExternalStorageFileQuotaType(file,
+ StorageManager.QUOTA_TYPE_MEDIA_VIDEO);
+ break;
+ case FileColumns.MEDIA_TYPE_IMAGE:
+ mStorageManager.updateExternalStorageFileQuotaType(file,
+ StorageManager.QUOTA_TYPE_MEDIA_IMAGE);
+ break;
+ default:
+ mStorageManager.updateExternalStorageFileQuotaType(file,
+ StorageManager.QUOTA_TYPE_MEDIA_NONE);
+ break;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to update quota type for " + file.getPath(), e);
+ }
+ }
+
private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
@Override
public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
int mediaType, boolean isDownload) {
acceptWithExpansion(helper::notifyChange, volumeName, id, mediaType, isDownload);
+ if (helper.isExternal()) {
+ // Update the quota type on the filesystem
+ Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
+ updateQuotaTypeForUri(fileUri, mediaType);
+ }
+
// Tell our SAF provider so it knows when views are no longer empty
MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
}
@@ -461,8 +500,12 @@
// When media type changes, notify both old and new collections and
// invalidate any thumbnails
if (newMediaType != oldMediaType) {
+ Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
+ if (helper.isExternal()) {
+ updateQuotaTypeForUri(fileUri, newMediaType);
+ }
acceptWithExpansion(helper::notifyChange, volumeName, id, newMediaType, isDownload);
- invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id));
+ invalidateThumbnails(fileUri);
}
}
@@ -533,20 +576,27 @@
}
}
-
-
private static final String[] sDefaultFolderNames = {
- Environment.DIRECTORY_MUSIC,
- Environment.DIRECTORY_PODCASTS,
- Environment.DIRECTORY_RINGTONES,
- Environment.DIRECTORY_ALARMS,
- Environment.DIRECTORY_NOTIFICATIONS,
- Environment.DIRECTORY_PICTURES,
- Environment.DIRECTORY_MOVIES,
- Environment.DIRECTORY_DOWNLOADS,
- Environment.DIRECTORY_DCIM,
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RINGTONES,
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PICTURES,
+ Environment.DIRECTORY_MOVIES,
+ Environment.DIRECTORY_DOWNLOADS,
+ Environment.DIRECTORY_DCIM,
};
+ private static boolean isDefaultDirectoryName(@Nullable String dirName) {
+ for (String defaultDirName : sDefaultFolderNames) {
+ if (defaultDirName.equals(dirName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Ensure that default folders are created on mounted primary storage
* devices. We only do this once per volume so we don't annoy the user if
@@ -557,7 +607,12 @@
final File path = getVolumePath(volumeName);
final StorageVolume vol = mStorageManager.getStorageVolume(path);
final String key;
- if (vol == null || vol.isPrimary()) {
+ if (vol == null) {
+ Log.w(TAG, "Failed to ensure default folders for " + volumeName);
+ return;
+ }
+
+ if (vol.isPrimary()) {
key = "created_default_folders";
} else {
key = "created_default_folders_" + vol.getMediaStoreVolumeName();
@@ -4775,6 +4830,14 @@
return new File(filePath);
}
+ private FuseDaemon getFuseDaemonForFile(File file) {
+ StorageVolume volume = mStorageManager.getStorageVolume(file);
+ if (volume == null) {
+ return null;
+ }
+ return ExternalStorageServiceImpl.getFuseDaemon(volume.getId());
+ }
+
/**
* Replacement for {@link #openFileHelper(Uri, String)} which enforces any
* permissions applicable to the path before returning.
@@ -4882,12 +4945,7 @@
redactionInfo.freeOffsets);
}
} else {
- FuseDaemon daemon = null;
-
- StorageVolume volume = mStorageManager.getStorageVolume(file);
- if (volume != null) {
- daemon = ExternalStorageServiceImpl.getFuseDaemon(volume.getId());
- }
+ FuseDaemon daemon = getFuseDaemonForFile(file);
ParcelFileDescriptor lowerFsFd = ParcelFileDescriptor.open(file, modeBits);
boolean forRead = (modeBits & ParcelFileDescriptor.MODE_READ_ONLY) != 0;
boolean shouldOpenWithFuse = daemon != null
@@ -5582,6 +5640,7 @@
*
* @param path File path of the directory that the app wants to create/delete
* @param uid UID of the app that wants to create/delete the directory
+ * @param forCreate denotes whether the operation is directory creation or deletion
* @return 0 if the operation is allowed, or the following {@code errno} values:
* <ul>
* <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's
@@ -5593,7 +5652,8 @@
* Called from JNI in jni/MediaProviderWrapper.cpp
*/
@Keep
- public int isDirectoryOperationAllowedForFuse(@NonNull String path, int uid) {
+ public int isDirectoryCreationOrDeletionAllowedForFuse(
+ @NonNull String path, int uid, boolean forCreate) {
final LocalCallingIdentity token =
clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
try {
@@ -5621,8 +5681,17 @@
}
final String[] relativePath = sanitizePath(extractRelativePath(path));
- if (relativePath.length == 1 && TextUtils.isEmpty(relativePath[0])) {
- Log.e(TAG, "Creating or deleting a top level directory is not allowed!");
+ final boolean isTopLevelDir =
+ relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
+ if (isTopLevelDir) {
+ // We allow creating the default top level directories only, all other oprations on
+ // top level directories are not allowed.
+ if (forCreate && isDefaultDirectoryName(extractDisplayName(path))) {
+ return 0;
+ }
+ Log.e(TAG,
+ "Creating a non-default top level directory or deleting an existing"
+ + " one is not allowed!");
return OsConstants.EPERM;
}
return 0;
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 19ebc03..3ce2160 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -99,9 +99,17 @@
return native_should_open_with_fuse(mPtr, path, readLock, fd);
}
+ /**
+ * Invalidates FUSE VFS dentry cache for {@code path}
+ */
+ public void invalidateFuseDentryCache(String path) {
+ native_invalidate_fuse_dentry_cache(mPtr, path);
+ }
+
private native long native_new(MediaProvider mediaProvider);
private native void native_start(long daemon, int deviceFd, String path);
private native void native_delete(long daemon);
private native boolean native_should_open_with_fuse(long daemon, String path, boolean readLock,
int fd);
+ private native void native_invalidate_fuse_dentry_cache(long daemon, String path);
}
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index eea937d..6af8a3a 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -225,6 +225,11 @@
}
@Test
+ public void testCanCreateDefaultDirectory() throws Exception {
+ runDeviceTest("testCanCreateDefaultDirectory");
+ }
+
+ @Test
public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
runDeviceTest("testManageExternalStorageQueryOtherAppsFile");
}
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 1201e03..8f47edd 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -107,6 +107,7 @@
static final File MOVIES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MOVIES);
static final File DOWNLOAD_DIR = new File(EXTERNAL_STORAGE_DIR,
Environment.DIRECTORY_DOWNLOADS);
+ static final File PODCASTS_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PODCASTS);
static final File ANDROID_DATA_DIR = new File(EXTERNAL_STORAGE_DIR, "Android/data");
static final File ANDROID_MEDIA_DIR = new File(EXTERNAL_STORAGE_DIR, "Android/media");
static final String TEST_DIRECTORY_NAME = "FilePathAccessTestDirectory";
@@ -1381,6 +1382,20 @@
}
@Test
+ public void testCanCreateDefaultDirectory() throws Exception {
+ try {
+ if (PODCASTS_DIR.exists()) {
+ // Apps can't delete top level directories, not even default directories, so we let
+ // shell do the deed for us.
+ executeShellCommand("rm -r " + PODCASTS_DIR);
+ }
+ assertThat(PODCASTS_DIR.mkdir()).isTrue();
+ } finally {
+ executeShellCommand("mkdir " + PODCASTS_DIR);
+ }
+ }
+
+ @Test
public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);