Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6caa2d15 authored by Martijn Coenen's avatar Martijn Coenen
Browse files

Add API for updating quota type of files on external storage.

In recent releases, Android has used the GID of files on external
storage to compute quota for various media types. This was implemented
by the kernel sdcard filesystem, which kept a mapping of file
extension->GID, and automatically set the correct GID on the lower
filesystem. We then simply asked the kernel "how much space is used by
the GID corresponding to image files" to determine how much space image
files took up on external storage.

sdcardfs will be removed starting with devices launching with R; this
means that the quota tracking implementation can no longer live in the
kernel on these devices. MediaProvider is a logical place to initiate
the quote handling in userspace, since it is responsible for all files
on external storage.

But since MediaProvider is now a mainline module, we don't want the
quota tracking implementation to live in MediaProvider itself. Instead,
provide a new @SystemAPI on StorageManager that can be called whenever
we need to set the quota type for a file on external storage. This
allows MediaProvider to call this API whenever a file is created on
external storage, or whenever its type is changed such that it requires
using a different quota type (eg going from an image file to an audio
file, which is presumably a rare case).

The API doesn't require a special permission, because the current
implementation is not a binder call, but an in-process call to modify
filesystem attributes. That means that the caller must already be in an
SELinux domain that allows these attribute modifications. Currently,
only MediaProvider and vold are allowed to modify these attributes.
Therefore, this API is effectively protected by SELinux.

Bug: 146419093
Test: builds. Functionality will be tested with existing
      StorageHostTest CTS test, but running that tests depends on other
      changes (eg a device without sdcardfs). This is tracked in the
      test plan for removing sdcardfs.
Change-Id: I9ffb11a89b17e5596fce70e96c06a8af2142e41f
parent f7258237
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -9173,7 +9173,12 @@ package android.os.storage {
    method @WorkerThread public void allocateBytes(java.io.FileDescriptor, long, @RequiresPermission int) throws java.io.IOException;
    method @WorkerThread public long getAllocatableBytes(@NonNull java.util.UUID, @RequiresPermission int) throws java.io.IOException;
    method public static boolean hasIsolatedStorage();
    method public void updateExternalStorageFileQuotaType(@NonNull java.io.File, int) throws java.io.IOException;
    field @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public static final int FLAG_ALLOCATE_AGGRESSIVE = 1; // 0x1
    field public static final int QUOTA_TYPE_MEDIA_AUDIO = 2; // 0x2
    field public static final int QUOTA_TYPE_MEDIA_IMAGE = 1; // 0x1
    field public static final int QUOTA_TYPE_MEDIA_NONE = 0; // 0x0
    field public static final int QUOTA_TYPE_MEDIA_VIDEO = 3; // 0x3
  }
  public final class StorageVolume implements android.os.Parcelable {
+115 −0
Original line number Diff line number Diff line
@@ -2309,6 +2309,121 @@ public class StorageManager {
    private static final String XATTR_CACHE_GROUP = "user.cache_group";
    private static final String XATTR_CACHE_TOMBSTONE = "user.cache_tombstone";


   // Matches AID_MEDIA_RW in android_filesystem_config.h
    private static final int QUOTA_PROJECT_ID_MEDIA_NONE = 1023;

    // Matches AID_MEDIA_IMAGE in android_filesystem_config.h
    private static final int QUOTA_PROJECT_ID_MEDIA_IMAGE = 1057;

    // Matches AID_MEDIA_AUDIO in android_filesystem_config.h
    private static final int QUOTA_PROJECT_ID_MEDIA_AUDIO = 1055;

    // Matches AID_MEDIA_VIDEO in android_filesystem_config.h
    private static final int QUOTA_PROJECT_ID_MEDIA_VIDEO = 1056;

    /**
     * Constant for use with
     * {@link #updateExternalStorageFileQuotaType(String, int)} (String, int)}, to indicate the file
     * is not a media file.
     *
     * @hide
     */
    @SystemApi
    public static final int QUOTA_TYPE_MEDIA_NONE = 0;

    /**
     * Constant for use with
     * {@link #updateExternalStorageFileQuotaType(String, int)} (String, int)}, to indicate the file
     * is an image file.
     *
     * @hide
     */
    @SystemApi
    public static final int QUOTA_TYPE_MEDIA_IMAGE = 1;

    /**
     * Constant for use with
     * {@link #updateExternalStorageFileQuotaType(String, int)} (String, int)}, to indicate the file
     * is an audio file.
     *
     * @hide
     */
    @SystemApi
    public static final int QUOTA_TYPE_MEDIA_AUDIO = 2;

    /**
     * Constant for use with
     * {@link #updateExternalStorageFileQuotaType(String, int)} (String, int)}, to indicate the file
     * is a video file.
     *
     * @hide
     */
    @SystemApi
    public static final int QUOTA_TYPE_MEDIA_VIDEO = 3;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "QUOTA_TYPE_" }, value = {
            QUOTA_TYPE_MEDIA_NONE,
            QUOTA_TYPE_MEDIA_AUDIO,
            QUOTA_TYPE_MEDIA_VIDEO,
            QUOTA_TYPE_MEDIA_IMAGE,
    })
    public @interface QuotaType {}

    private static native boolean setQuotaProjectId(String path, long projectId);

    /**
     * Let StorageManager know that the quota type for a file on external storage should
     * be updated. Android tracks quotas for various media types. Consequently, this should be
     * called on first creation of a new file on external storage, and whenever the
     * media type of the file is updated later.
     *
     * This API doesn't require any special permissions, though typical implementations
     * will require being called from an SELinux domain that allows setting file attributes
     * related to quota (eg the GID or project ID).
     *
     * The default platform user of this API is the MediaProvider process, which is
     * responsible for managing all of external storage.
     *
     * @param path the path to the file for which we should update the quota type
     * @param quotaType the quota type of the file; this is based on the
     *                  {@code QuotaType} constants, eg
     *                  {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO}
     *
     * @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid
     *                                  quota type.
     * @throws IOException              if the quota type could not be updated.
     *
     * @hide
     */
    @SystemApi
    public void updateExternalStorageFileQuotaType(@NonNull File path,
            @QuotaType int quotaType) throws IOException {
        long projectId;
        final String filePath = path.getCanonicalPath();
        switch (quotaType) {
            case QUOTA_TYPE_MEDIA_NONE:
                projectId = QUOTA_PROJECT_ID_MEDIA_NONE;
                break;
            case QUOTA_TYPE_MEDIA_AUDIO:
                projectId = QUOTA_PROJECT_ID_MEDIA_AUDIO;
                break;
            case QUOTA_TYPE_MEDIA_VIDEO:
                projectId = QUOTA_PROJECT_ID_MEDIA_VIDEO;
                break;
            case QUOTA_TYPE_MEDIA_IMAGE:
                projectId = QUOTA_PROJECT_ID_MEDIA_IMAGE;
                break;
            default:
                throw new IllegalArgumentException("Invalid quota type: " + quotaType);
        }
        if (!setQuotaProjectId(filePath, projectId)) {
            throw new IOException("Failed to update quota type for " + filePath);
        }
    }

    /** {@hide} */
    private static void setCacheBehavior(File path, String name, boolean enabled)
            throws IOException {
+1 −0
Original line number Diff line number Diff line
@@ -137,6 +137,7 @@ cc_library_shared {
                "android_os_Parcel.cpp",
                "android_os_SELinux.cpp",
                "android_os_SharedMemory.cpp",
                "android_os_storage_StorageManager.cpp",
                "android_os_Trace.cpp",
                "android_os_UEventObserver.cpp",
                "android_os_VintfObject.cpp",
+2 −0
Original line number Diff line number Diff line
@@ -143,6 +143,7 @@ extern int register_android_os_Parcel(JNIEnv* env);
extern int register_android_os_SELinux(JNIEnv* env);
extern int register_android_os_VintfObject(JNIEnv *env);
extern int register_android_os_VintfRuntimeInfo(JNIEnv *env);
extern int register_android_os_storage_StorageManager(JNIEnv* env);
extern int register_android_os_SystemProperties(JNIEnv *env);
extern int register_android_os_SystemClock(JNIEnv* env);
extern int register_android_os_Trace(JNIEnv* env);
@@ -1461,6 +1462,7 @@ static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_os_HwParcel),
    REG_JNI(register_android_os_HwRemoteBinder),
    REG_JNI(register_android_os_NativeHandle),
    REG_JNI(register_android_os_storage_StorageManager),
    REG_JNI(register_android_os_VintfObject),
    REG_JNI(register_android_os_VintfRuntimeInfo),
    REG_JNI(register_android_service_DataLoaderService),
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

#define LOG_TAG "StorageManager"
#include <android-base/logging.h>
#include <android-base/unique_fd.h>
#include <fcntl.h>
#include <linux/fs.h>

#include <nativehelper/JNIHelp.h>
#include "core_jni_helpers.h"

namespace android {

jboolean android_os_storage_StorageManager_setQuotaProjectId(JNIEnv* env, jobject self,
                                                             jstring path, jlong projectId) {
    struct fsxattr fsx;
    ScopedUtfChars utf_chars_path(env, path);

    if (projectId > UINT32_MAX) {
        LOG(ERROR) << "Invalid project id: " << projectId;
        return JNI_FALSE;
    }

    android::base::unique_fd fd(
            TEMP_FAILURE_RETRY(open(utf_chars_path.c_str(), O_RDONLY | O_CLOEXEC)));
    if (fd == -1) {
        PLOG(ERROR) << "Failed to open " << utf_chars_path.c_str() << " to set project id.";
        return JNI_FALSE;
    }

    int ret = ioctl(fd, FS_IOC_FSGETXATTR, &fsx);
    if (ret == -1) {
        PLOG(ERROR) << "Failed to get extended attributes for " << utf_chars_path.c_str()
                    << " to get project id.";
        return JNI_FALSE;
    }

    fsx.fsx_projid = projectId;
    ret = ioctl(fd, FS_IOC_FSSETXATTR, &fsx);
    if (ret == -1) {
        PLOG(ERROR) << "Failed to set extended attributes for " << utf_chars_path.c_str()
                    << " to set project id.";
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

// ----------------------------------------------------------------------------

static const JNINativeMethod gStorageManagerMethods[] = {
        {"setQuotaProjectId", "(Ljava/lang/String;J)Z",
         (void*)android_os_storage_StorageManager_setQuotaProjectId},
};

const char* const kStorageManagerPathName = "android/os/storage/StorageManager";

int register_android_os_storage_StorageManager(JNIEnv* env) {
    return RegisterMethodsOrDie(env, kStorageManagerPathName, gStorageManagerMethods,
                                NELEM(gStorageManagerMethods));
}

}; // namespace android