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) {