summaryrefslogtreecommitdiff
path: root/core/java/android/os/incremental/IncrementalManager.java
blob: 9c8ee562849cd5ceec3baa37f38ddf2190076fb7 (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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/*
 * Copyright (C) 2019 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 android.os.incremental;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.DataLoaderParams;
import android.content.pm.IPackageLoadingProgressCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;

/**
 * Provides operations to open or create an IncrementalStorage, using IIncrementalService
 * service. Example Usage:
 *
 * <blockquote><pre>
 * IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);
 * IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");
 * </pre></blockquote>
 *
 * @hide
 */
@SystemService(Context.INCREMENTAL_SERVICE)
public final class IncrementalManager {
    private static final String TAG = "IncrementalManager";

    private static final String ALLOWED_PROPERTY = "incremental.allowed";

    public static final int MIN_VERSION_TO_SUPPORT_FSVERITY = 2;

    public static final int CREATE_MODE_TEMPORARY_BIND =
            IIncrementalService.CREATE_MODE_TEMPORARY_BIND;
    public static final int CREATE_MODE_PERMANENT_BIND =
            IIncrementalService.CREATE_MODE_PERMANENT_BIND;
    public static final int CREATE_MODE_CREATE =
            IIncrementalService.CREATE_MODE_CREATE;
    public static final int CREATE_MODE_OPEN_EXISTING =
            IIncrementalService.CREATE_MODE_OPEN_EXISTING;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"CREATE_MODE_"}, value = {
            CREATE_MODE_TEMPORARY_BIND,
            CREATE_MODE_PERMANENT_BIND,
            CREATE_MODE_CREATE,
            CREATE_MODE_OPEN_EXISTING,
    })
    public @interface CreateMode {
    }

    private final @Nullable IIncrementalService mService;

    private final LoadingProgressCallbacks mLoadingProgressCallbacks =
            new LoadingProgressCallbacks();

    public IncrementalManager(IIncrementalService service) {
        mService = service;
    }

    /**
     * Opens or create an Incremental File System mounted directory and returns an
     * IncrementalStorage object.
     *
     * @param path                Absolute path to mount Incremental File System on.
     * @param params              IncrementalDataLoaderParams object to configure data loading.
     * @param createMode          Mode for opening an old Incremental File System mount or creating
     *                            a new mount.
     * @return IncrementalStorage object corresponding to the mounted directory.
     */
    @Nullable
    public IncrementalStorage createStorage(@NonNull String path,
            @NonNull DataLoaderParams params,
            @CreateMode int createMode) {
        Objects.requireNonNull(path);
        Objects.requireNonNull(params);
        try {
            final int id = mService.createStorage(path, params.getData(), createMode);
            if (id < 0) {
                return null;
            }
            return new IncrementalStorage(mService, id);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Opens an existing Incremental File System mounted directory and returns an IncrementalStorage
     * object.
     *
     * @param path Absolute target path that Incremental File System has been mounted on.
     * @return IncrementalStorage object corresponding to the mounted directory.
     */
    @Nullable
    public IncrementalStorage openStorage(@NonNull String path) {
        try {
            final int id = mService.openStorage(path);
            if (id < 0) {
                return null;
            }
            final IncrementalStorage storage = new IncrementalStorage(mService, id);
            return storage;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Opens or creates an IncrementalStorage that is linked to another IncrementalStorage.
     *
     * @return IncrementalStorage object corresponding to the linked storage.
     */
    @Nullable
    public IncrementalStorage createStorage(@NonNull String path,
            @NonNull IncrementalStorage linkedStorage, @CreateMode int createMode) {
        try {
            final int id = mService.createLinkedStorage(
                    path, linkedStorage.getId(), createMode);
            if (id < 0) {
                return null;
            }
            return new IncrementalStorage(mService, id);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Link an app's files from the stage dir to the final installation location.
     * The expected outcome of this method is:
     * 1) The actual apk directory under /data/incremental is bind-mounted to the parent directory
     * of {@code afterCodeFile}.
     * 2) All the files under {@code beforeCodeFile} will show up under {@code afterCodeFile}.
     *
     * @param beforeCodeFile Path that is currently bind-mounted and have APKs under it.
     *                       Example: /data/app/vmdl*tmp
     * @param afterCodeFile Path that should will have APKs after this method is called. Its parent
     *                      directory should be bind-mounted to a directory under /data/incremental.
     *                      Example: /data/app/~~[randomStringA]/[packageName]-[randomStringB]
     * @throws IllegalArgumentException
     * @throws IOException
     */
    public void linkCodePath(File beforeCodeFile, File afterCodeFile)
            throws IllegalArgumentException, IOException {
        final File beforeCodeAbsolute = beforeCodeFile.getAbsoluteFile();
        final IncrementalStorage apkStorage = openStorage(beforeCodeAbsolute.toString());
        if (apkStorage == null) {
            throw new IllegalArgumentException("Not an Incremental path: " + beforeCodeAbsolute);
        }
        final String targetStorageDir = afterCodeFile.getAbsoluteFile().getParent();
        final IncrementalStorage linkedApkStorage =
                createStorage(targetStorageDir, apkStorage,
                        IncrementalManager.CREATE_MODE_CREATE
                                | IncrementalManager.CREATE_MODE_PERMANENT_BIND);
        if (linkedApkStorage == null) {
            throw new IOException("Failed to create linked storage at dir: " + targetStorageDir);
        }
        try {
            final String afterCodePathName = afterCodeFile.getName();
            linkFiles(apkStorage, beforeCodeAbsolute, "", linkedApkStorage, afterCodePathName);
        } catch (Exception e) {
            linkedApkStorage.unBind(targetStorageDir);
            throw e;
        }
    }

    /**
     * Recursively set up directories and link all the files from source storage to target storage.
     *
     * @param sourceStorage The storage that has all the files and directories underneath.
     * @param sourceAbsolutePath The absolute path of the directory that holds all files and dirs.
     * @param sourceRelativePath The relative path on the source directory, e.g., "" or "lib".
     * @param targetStorage The target storage that will have the same files and directories.
     * @param targetRelativePath The relative path to the directory on the target storage that
     *                           should have all the files and dirs underneath,
     *                           e.g., "packageName-random".
     * @throws IOException When makeDirectory or makeLink fails on the Incremental File System.
     */
    private void linkFiles(IncrementalStorage sourceStorage, File sourceAbsolutePath,
            String sourceRelativePath, IncrementalStorage targetStorage,
            String targetRelativePath) throws IOException {
        final Path sourceBase = sourceAbsolutePath.toPath().resolve(sourceRelativePath);
        final Path targetRelative = Paths.get(targetRelativePath);
        Files.walkFileTree(sourceAbsolutePath.toPath(), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                final Path relativeDir = sourceBase.relativize(dir);
                targetStorage.makeDirectory(targetRelative.resolve(relativeDir).toString());
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                final Path relativeFile = sourceBase.relativize(file);
                sourceStorage.makeLink(
                        file.toAbsolutePath().toString(), targetStorage,
                        targetRelative.resolve(relativeFile).toString());
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Checks if Incremental feature is enabled on this device.
     */
    public static boolean isFeatureEnabled() {
        return nativeIsEnabled();
    }

    /**
     * 0 - IncFs is disabled.
     * 1 - IncFs v1, core features, no PerUid support. Optional in R.
     * 2 - IncFs v2, PerUid support, fs-verity support. Required in S.
     */
    public static int getVersion() {
        return nativeIsEnabled() ? nativeIsV2Available() ? 2 : 1 : 0;
    }

    /**
     * Checks if Incremental installations are allowed.
     * A developer can disable Incremental installations by setting the property.
     */
    public static boolean isAllowed() {
        return isFeatureEnabled() && android.os.SystemProperties.getBoolean(ALLOWED_PROPERTY, true);
    }

    /**
     * Checks if path is mounted on Incremental File System.
     */
    public static boolean isIncrementalPath(@NonNull String path) {
        return nativeIsIncrementalPath(path);
    }

    /**
     * Checks if an fd corresponds to a file on a mounted Incremental File System.
     */
    public static boolean isIncrementalFileFd(@NonNull FileDescriptor fd) {
        return nativeIsIncrementalFd(fd.getInt$());
    }

    /**
     * Returns raw signature for file if it's on Incremental File System.
     * Unsafe, use only if you are sure what you are doing.
     */
    public static @Nullable byte[] unsafeGetFileSignature(@NonNull String path) {
        return nativeUnsafeGetFileSignature(path);
    }

    /**
     * Closes a storage specified by the absolute path. If the path is not Incremental, do nothing.
     * Unbinds the target dir and deletes the corresponding storage instance.
     * Deletes the package name and associated storage id from maps.
     */
    public void rmPackageDir(@NonNull File codeFile) {
        try {
            final String codePath = codeFile.getAbsolutePath();
            final IncrementalStorage storage = openStorage(codePath);
            if (storage == null) {
                return;
            }
            mLoadingProgressCallbacks.cleanUpCallbacks(storage);
            storage.unBind(codePath);
        } catch (IOException e) {
            Slog.w(TAG, "Failed to remove code path", e);
        }
    }

    /**
     * Called when a new callback wants to listen to the loading progress of an installed package.
     * Increment the count of callbacks associated to the corresponding storage.
     * Only register storage listener if there hasn't been any existing callback on the storage yet.
     * @param codePath Path of the installed package. This path is on an Incremental Storage.
     * @param callback To report loading progress to.
     * @return True if the package name and associated storage id are valid. False otherwise.
     */
    public boolean registerLoadingProgressCallback(@NonNull String codePath,
            @NonNull IPackageLoadingProgressCallback callback) {
        final IncrementalStorage storage = openStorage(codePath);
        if (storage == null) {
            // storage does not exist, package not installed
            return false;
        }
        return mLoadingProgressCallbacks.registerCallback(storage, callback);
    }

    /**
     * Called to stop all listeners from listening to loading progress of an installed package.
     * @param codePath Path of the installed package
     */
    public void unregisterLoadingProgressCallbacks(@NonNull String codePath) {
        final IncrementalStorage storage = openStorage(codePath);
        if (storage == null) {
            // storage does not exist, package not installed
            return;
        }
        mLoadingProgressCallbacks.cleanUpCallbacks(storage);
    }

    private static class LoadingProgressCallbacks extends IStorageLoadingProgressListener.Stub {
        @GuardedBy("mCallbacks")
        private final SparseArray<RemoteCallbackList<IPackageLoadingProgressCallback>> mCallbacks =
                new SparseArray<>();

        public void cleanUpCallbacks(@NonNull IncrementalStorage storage) {
            final int storageId = storage.getId();
            final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
            synchronized (mCallbacks) {
                callbacksForStorage = mCallbacks.removeReturnOld(storageId);
            }
            if (callbacksForStorage == null) {
                return;
            }
            // Unregister all existing callbacks on this storage
            callbacksForStorage.kill();
            storage.unregisterLoadingProgressListener();
        }

        public boolean registerCallback(@NonNull IncrementalStorage storage,
                @NonNull IPackageLoadingProgressCallback callback) {
            final int storageId = storage.getId();
            synchronized (mCallbacks) {
                RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage =
                        mCallbacks.get(storageId);
                if (callbacksForStorage == null) {
                    callbacksForStorage = new RemoteCallbackList<>();
                    mCallbacks.put(storageId, callbacksForStorage);
                }
                // Registration in RemoteCallbackList needs to be done first, such that when events
                // come from Incremental Service, the callback is already registered
                callbacksForStorage.register(callback);
                if (callbacksForStorage.getRegisteredCallbackCount() > 1) {
                    // already listening for progress for this storage
                    return true;
                }
            }
            return storage.registerLoadingProgressListener(this);
        }

        @Override
        public void onStorageLoadingProgressChanged(int storageId, float progress) {
            final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
            synchronized (mCallbacks) {
                callbacksForStorage = mCallbacks.get(storageId);
            }
            if (callbacksForStorage == null) {
                // no callback has ever been registered on this storage
                return;
            }
            final int n = callbacksForStorage.beginBroadcast();
            // RemoteCallbackList use ArrayMap internally and it's safe to iterate this way
            for (int i = 0; i < n; i++) {
                final IPackageLoadingProgressCallback callback =
                        callbacksForStorage.getBroadcastItem(i);
                try {
                    callback.onPackageLoadingProgressChanged(progress);
                } catch (RemoteException ignored) {
                }
            }
            callbacksForStorage.finishBroadcast();
        }
    }

    /**
     * Returns the metrics of an Incremental Storage.
     */
    public IncrementalMetrics getMetrics(@NonNull String codePath) {
        final IncrementalStorage storage = openStorage(codePath);
        if (storage == null) {
            // storage does not exist, package not installed
            return null;
        }
        return new IncrementalMetrics(storage.getMetrics());
    }

    /* Native methods */
    private static native boolean nativeIsEnabled();
    private static native boolean nativeIsV2Available();
    private static native boolean nativeIsIncrementalPath(@NonNull String path);
    private static native boolean nativeIsIncrementalFd(@NonNull int fd);
    private static native byte[] nativeUnsafeGetFileSignature(@NonNull String path);
}