summaryrefslogtreecommitdiff
path: root/core/java/com/android/internal/content/F2fsUtils.java
blob: 27f1b308ed9c380b2cbc6cd4d50060f850c6ff5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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);

}