Revert "Revert "Release cblocks back to the free pool""
This reverts commit 516cb2d025a8c5dc67f4a926bdbb79c0e3725b4c.
Reason for revert: Add back after merging f2fs/f2fs-tools changes removing immutable bit stuffs
Bug: 188928405
Change-Id: I6a6fcde1190bff14abe1a67a75b8e7be95d89072
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 5cfb665..3116b26 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6624,6 +6624,20 @@
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 0000000..27f1b30
--- /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 0000000..c42bee6
--- /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 1468633..4502a33 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -84,6 +84,7 @@
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 7e8fc7e..dca5d96 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -190,6 +190,7 @@
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);
@@ -1621,6 +1622,7 @@
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 0000000..8b9d59c
--- /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 14861c2d..7b1b2e5 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.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 @@
* 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.
*/
@@ -7301,6 +7316,21 @@
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;
@@ -17172,6 +17202,10 @@
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 {
@@ -17179,7 +17213,6 @@
processInstallRequestsAsync(
res.returnCode == PackageManager.INSTALL_SUCCEEDED,
Collections.singletonList(new InstallRequest(args, res)));
-
}
}
}
@@ -23514,8 +23547,15 @@
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) {