diff options
-rw-r--r-- | core/java/android/provider/Settings.java | 14 | ||||
-rw-r--r-- | core/java/com/android/internal/content/F2fsUtils.java | 296 | ||||
-rw-r--r-- | core/java/com/android/internal/content/OWNERS | 5 | ||||
-rw-r--r-- | core/jni/Android.bp | 1 | ||||
-rw-r--r-- | core/jni/AndroidRuntime.cpp | 2 | ||||
-rw-r--r-- | core/jni/com_android_internal_content_F2fsUtils.cpp | 84 | ||||
-rw-r--r-- | services/core/java/com/android/server/pm/PackageManagerService.java | 44 |
7 files changed, 444 insertions, 2 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 36bc3e779c12..cb87653718c2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6661,6 +6661,20 @@ public final class Settings { public static final String COMPLETED_CATEGORY_PREFIX = "suggested.completed_category."; /** + * Whether or not compress blocks should be released on install. + * <p>The setting only determines if the platform will attempt to release + * compress blocks; it does not guarantee that the files will have their + * compress blocks released. Compression is currently only supported on + * some f2fs filesystems. + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String RELEASE_COMPRESS_BLOCKS_ON_INSTALL = + "release_compress_blocks_on_install"; + + /** * List of input methods that are currently enabled. This is a string * containing the IDs of all enabled input methods, each ID separated * by ':'. diff --git a/core/java/com/android/internal/content/F2fsUtils.java b/core/java/com/android/internal/content/F2fsUtils.java new file mode 100644 index 000000000000..27f1b308ed9c --- /dev/null +++ b/core/java/com/android/internal/content/F2fsUtils.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2021 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 com.android.internal.content; + +import android.annotation.NonNull; +import android.content.ContentResolver; +import android.os.Environment; +import android.os.incremental.IncrementalManager; +import android.provider.Settings.Secure; +import android.text.TextUtils; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods to work with the f2fs file system. + */ +public final class F2fsUtils { + private static final String TAG = "F2fsUtils"; + private static final boolean DEBUG_F2FS = false; + + /** Directory containing kernel features */ + private static final File sKernelFeatures = + new File("/sys/fs/f2fs/features"); + /** File containing features enabled on "/data" */ + private static final File sUserDataFeatures = + new File("/dev/sys/fs/by-name/userdata/features"); + private static final File sDataDirectory = Environment.getDataDirectory(); + /** Name of the compression feature */ + private static final String COMPRESSION_FEATURE = "compression"; + + private static final boolean sKernelCompressionAvailable; + private static final boolean sUserDataCompressionAvailable; + + static { + sKernelCompressionAvailable = isCompressionEnabledInKernel(); + if (!sKernelCompressionAvailable) { + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression DISABLED; feature not part of the kernel"); + } + } + sUserDataCompressionAvailable = isCompressionEnabledOnUserData(); + if (!sUserDataCompressionAvailable) { + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression DISABLED; feature not enabled on filesystem"); + } + } + } + + /** + * Releases compressed blocks from eligible installation artifacts. + * <p> + * Modern f2fs implementations starting in {@code S} support compression + * natively within the file system. The data blocks of specific installation + * artifacts [eg. .apk, .so, ...] can be compressed at the file system level, + * making them look and act like any other uncompressed file, but consuming + * a fraction of the space. + * <p> + * However, the unused space is not free'd automatically. Instead, we must + * manually tell the file system to release the extra blocks [the delta between + * the compressed and uncompressed block counts] back to the free pool. + * <p> + * Because of how compression works within the file system, once the blocks + * have been released, the file becomes read-only and cannot be modified until + * the free'd blocks have again been reserved from the free pool. + */ + public static void releaseCompressedBlocks(ContentResolver resolver, File file) { + if (!sKernelCompressionAvailable || !sUserDataCompressionAvailable) { + return; + } + + // NOTE: Retrieving this setting means we need to delay releasing cblocks + // of any APKs installed during the PackageManagerService constructor. Instead + // of being able to release them in the constructor, they can only be released + // immediately prior to the system being available. When we no longer need to + // read this setting, move cblock release back to the package manager constructor. + final boolean releaseCompressBlocks = + Secure.getInt(resolver, Secure.RELEASE_COMPRESS_BLOCKS_ON_INSTALL, 1) != 0; + if (!releaseCompressBlocks) { + if (DEBUG_F2FS) { + Slog.d(TAG, "SKIP; release compress blocks not enabled"); + } + return; + } + if (!isCompressionAllowed(file)) { + if (DEBUG_F2FS) { + Slog.d(TAG, "SKIP; compression not allowed"); + } + return; + } + final File[] files = getFilesToRelease(file); + if (files == null || files.length == 0) { + if (DEBUG_F2FS) { + Slog.d(TAG, "SKIP; no files to compress"); + } + return; + } + for (int i = files.length - 1; i >= 0; --i) { + final long releasedBlocks = nativeReleaseCompressedBlocks(files[i].getAbsolutePath()); + if (DEBUG_F2FS) { + Slog.d(TAG, "RELEASED " + releasedBlocks + " blocks" + + " from \"" + files[i] + "\""); + } + } + } + + /** + * Returns {@code true} if compression is allowed on the file system containing + * the given file. + * <p> + * NOTE: The return value does not mean if the given file, or any other file + * on the same file system, is actually compressed. It merely determines whether + * not files <em>may</em> be compressed. + */ + private static boolean isCompressionAllowed(@NonNull File file) { + final String filePath; + try { + filePath = file.getCanonicalPath(); + } catch (IOException e) { + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression DISABLED; could not determine path"); + } + return false; + } + if (IncrementalManager.isIncrementalPath(filePath)) { + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression DISABLED; file on incremental fs"); + } + return false; + } + if (!isChild(sDataDirectory, filePath)) { + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression DISABLED; file not on /data"); + } + return false; + } + if (DEBUG_F2FS) { + Slog.d(TAG, "f2fs compression ENABLED"); + } + return true; + } + + /** + * Returns {@code true} if the given child is a descendant of the base. + */ + private static boolean isChild(@NonNull File base, @NonNull String childPath) { + try { + base = base.getCanonicalFile(); + + File parentFile = new File(childPath).getCanonicalFile(); + while (parentFile != null) { + if (base.equals(parentFile)) { + return true; + } + parentFile = parentFile.getParentFile(); + } + return false; + } catch (IOException ignore) { + return false; + } + } + + /** + * Returns whether or not the compression feature is enabled in the kernel. + * <p> + * NOTE: This doesn't mean compression is enabled on a particular file system + * or any files have been compressed. Only that the functionality is enabled + * on the device. + */ + private static boolean isCompressionEnabledInKernel() { + final File[] features = sKernelFeatures.listFiles(); + if (features == null || features.length == 0) { + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; no kernel features"); + } + return false; + } + for (int i = features.length - 1; i >= 0; --i) { + final File feature = features[i]; + if (COMPRESSION_FEATURE.equals(features[i].getName())) { + if (DEBUG_F2FS) { + Slog.d(TAG, "FOUND kernel compression feature"); + } + return true; + } + } + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; kernel compression feature not found"); + } + return false; + } + + /** + * Returns whether or not the compression feature is enabled on user data [ie. "/data"]. + * <p> + * NOTE: This doesn't mean any files have been compressed. Only that the functionality + * is enabled on the file system. + */ + private static boolean isCompressionEnabledOnUserData() { + if (!sUserDataFeatures.exists() + || !sUserDataFeatures.isFile() + || !sUserDataFeatures.canRead()) { + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; filesystem features not available"); + } + return false; + } + final List<String> configLines; + try { + configLines = Files.readAllLines(sUserDataFeatures.toPath()); + } catch (IOException ignore) { + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; couldn't read filesystem features"); + } + return false; + } + if (configLines == null + || configLines.size() > 1 + || TextUtils.isEmpty(configLines.get(0))) { + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; no filesystem features"); + } + return false; + } + final String[] features = configLines.get(0).split(","); + for (int i = features.length - 1; i >= 0; --i) { + if (COMPRESSION_FEATURE.equals(features[i].trim())) { + if (DEBUG_F2FS) { + Slog.d(TAG, "FOUND filesystem compression feature"); + } + return true; + } + } + if (DEBUG_F2FS) { + Slog.d(TAG, "ERROR; filesystem compression feature not found"); + } + return false; + } + + /** + * Returns all files contained within the directory at any depth from the given path. + */ + private static List<File> getFilesRecursive(@NonNull File path) { + final File[] allFiles = path.listFiles(); + if (allFiles == null) { + return null; + } + final ArrayList<File> files = new ArrayList<>(); + for (File f : allFiles) { + if (f.isDirectory()) { + files.addAll(getFilesRecursive(f)); + } else if (f.isFile()) { + files.add(f); + } + } + return files; + } + + /** + * Returns all files contained within the directory at any depth from the given path. + */ + private static File[] getFilesToRelease(@NonNull File codePath) { + final List<File> files = getFilesRecursive(codePath); + if (files == null) { + if (codePath.isFile()) { + return new File[] { codePath }; + } + return null; + } + if (files.size() == 0) { + return null; + } + return files.toArray(new File[files.size()]); + } + + private static native long nativeReleaseCompressedBlocks(String path); + +} diff --git a/core/java/com/android/internal/content/OWNERS b/core/java/com/android/internal/content/OWNERS new file mode 100644 index 000000000000..c42bee69410d --- /dev/null +++ b/core/java/com/android/internal/content/OWNERS @@ -0,0 +1,5 @@ +# Bug component: 36137 +include /core/java/android/content/pm/OWNERS + +per-file ReferrerIntent.aidl = file:/services/core/java/com/android/server/am/OWNERS +per-file ReferrerIntent.java = file:/services/core/java/com/android/server/am/OWNERS diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 68388d98dbb4..125182cab254 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -84,6 +84,7 @@ cc_library_shared { android: { srcs: [ "AndroidRuntime.cpp", + "com_android_internal_content_F2fsUtils.cpp", "com_android_internal_content_NativeLibraryHelper.cpp", "com_google_android_gles_jni_EGLImpl.cpp", "com_google_android_gles_jni_GLImpl.cpp", // TODO: .arm diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 3debb3e03483..443bfce7f050 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -189,6 +189,7 @@ extern int register_android_content_res_ObbScanner(JNIEnv* env); extern int register_android_content_res_Configuration(JNIEnv* env); extern int register_android_animation_PropertyValuesHolder(JNIEnv *env); extern int register_android_security_Scrypt(JNIEnv *env); +extern int register_com_android_internal_content_F2fsUtils(JNIEnv* env); extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env); extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env); extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env); @@ -1624,6 +1625,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_animation_PropertyValuesHolder), REG_JNI(register_android_security_Scrypt), + REG_JNI(register_com_android_internal_content_F2fsUtils), REG_JNI(register_com_android_internal_content_NativeLibraryHelper), REG_JNI(register_com_android_internal_os_DmabufInfoReader), REG_JNI(register_com_android_internal_os_FuseAppLoop), diff --git a/core/jni/com_android_internal_content_F2fsUtils.cpp b/core/jni/com_android_internal_content_F2fsUtils.cpp new file mode 100644 index 000000000000..8b9d59c416a0 --- /dev/null +++ b/core/jni/com_android_internal_content_F2fsUtils.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 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 "F2fsUtils" + +#include "core_jni_helpers.h" + +#include <nativehelper/ScopedUtfChars.h> +#include <nativehelper/jni_macros.h> + +#include <sys/ioctl.h> +#include <sys/types.h> + +#include <linux/f2fs.h> +#include <linux/fs.h> + +#include <android-base/unique_fd.h> + +#include <utils/Log.h> + +#include <errno.h> +#include <fcntl.h> + +#include <array> + +using namespace std::literals; + +namespace android { + +static jlong com_android_internal_content_F2fsUtils_nativeReleaseCompressedBlocks(JNIEnv *env, + jclass clazz, + jstring path) { + unsigned long long blkcnt; + int ret; + ScopedUtfChars filePath(env, path); + + android::base::unique_fd fd(open(filePath.c_str(), O_RDONLY | O_CLOEXEC, 0)); + if (fd < 0) { + ALOGW("Failed to open file: %s (%d)\n", filePath.c_str(), errno); + return 0; + } + + long flags = 0; + ret = ioctl(fd, FS_IOC_GETFLAGS, &flags); + if (ret < 0) { + ALOGW("Failed to get flags for file: %s (%d)\n", filePath.c_str(), errno); + return 0; + } + if ((flags & FS_COMPR_FL) == 0) { + return 0; + } + + ret = ioctl(fd, F2FS_IOC_RELEASE_COMPRESS_BLOCKS, &blkcnt); + if (ret < 0) { + return -errno; + } + return blkcnt; +} + +static const std::array gMethods = { + MAKE_JNI_NATIVE_METHOD( + "nativeReleaseCompressedBlocks", "(Ljava/lang/String;)J", + com_android_internal_content_F2fsUtils_nativeReleaseCompressedBlocks), +}; + +int register_com_android_internal_content_F2fsUtils(JNIEnv *env) { + return RegisterMethodsOrDie(env, "com/android/internal/content/F2fsUtils", gMethods.data(), + gMethods.size()); +} + +}; // namespace android diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 93d3d17918a1..5817a80ec6c1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -341,6 +341,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ResolverActivity; +import com.android.internal.content.F2fsUtils; import com.android.internal.content.NativeLibraryHelper; import com.android.internal.content.PackageHelper; import com.android.internal.content.om.OverlayConfig; @@ -896,6 +897,20 @@ public class PackageManagerService extends IPackageManager.Stub * Only non-null during an OTA, and even then it is nulled again once systemReady(). */ private @Nullable ArraySet<String> mExistingPackages = null; + + /** + * List of code paths that need to be released when the system becomes ready. + * <p> + * NOTE: We have to delay releasing cblocks for no other reason than we cannot + * retrieve the setting {@link Secure#RELEASE_COMPRESS_BLOCKS_ON_INSTALL}. When + * we no longer need to read that setting, cblock release can occur in the + * constructor. + * + * @see Secure#RELEASE_COMPRESS_BLOCKS_ON_INSTALL + * @see #systemReady() + */ + private @Nullable List<File> mReleaseOnSystemReady; + /** * Whether or not system app permissions should be promoted from install to runtime. */ @@ -7966,6 +7981,21 @@ public class PackageManagerService extends IPackageManager.Stub IoUtils.closeQuietly(handle); } } + if (ret == PackageManager.INSTALL_SUCCEEDED) { + // NOTE: During boot, we have to delay releasing cblocks for no other reason than + // we cannot retrieve the setting {@link Secure#RELEASE_COMPRESS_BLOCKS_ON_INSTALL}. + // When we no longer need to read that setting, cblock release can occur always + // occur here directly + if (!mSystemReady) { + if (mReleaseOnSystemReady == null) { + mReleaseOnSystemReady = new ArrayList<>(); + } + mReleaseOnSystemReady.add(dstCodePath); + } else { + final ContentResolver resolver = mContext.getContentResolver(); + F2fsUtils.releaseCompressedBlocks(resolver, dstCodePath); + } + } if (ret != PackageManager.INSTALL_SUCCEEDED) { if (!dstCodePath.exists()) { return null; @@ -17853,6 +17883,10 @@ public class PackageManagerService extends IPackageManager.Stub if (mRet == PackageManager.INSTALL_SUCCEEDED) { mRet = args.copyApk(); } + if (mRet == PackageManager.INSTALL_SUCCEEDED) { + F2fsUtils.releaseCompressedBlocks( + mContext.getContentResolver(), new File(args.getCodePath())); + } if (mParentInstallParams != null) { mParentInstallParams.tryProcessInstallRequest(args, mRet); } else { @@ -17860,7 +17894,6 @@ public class PackageManagerService extends IPackageManager.Stub processInstallRequestsAsync( res.returnCode == PackageManager.INSTALL_SUCCEEDED, Collections.singletonList(new InstallRequest(args, res))); - } } } @@ -24235,8 +24268,15 @@ public class PackageManagerService extends IPackageManager.Stub public void systemReady() { enforceSystemOrRoot("Only the system can claim the system is ready"); - mSystemReady = true; final ContentResolver resolver = mContext.getContentResolver(); + if (mReleaseOnSystemReady != null) { + for (int i = mReleaseOnSystemReady.size() - 1; i >= 0; --i) { + final File dstCodePath = mReleaseOnSystemReady.get(i); + F2fsUtils.releaseCompressedBlocks(resolver, dstCodePath); + } + mReleaseOnSystemReady = null; + } + mSystemReady = true; ContentObserver co = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { |