/* * 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. *
* 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. *
* 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. *
* 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. *
* 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 may 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. *
* 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"]. *
* 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