Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package android.os.incremental; |
| 18 | |
| 19 | /** |
| 20 | * Set up files and directories used in an installation session. |
| 21 | * Currently only used by Incremental Installation. |
| 22 | * For Incremental installation, the expected outcome of this function is: |
| 23 | * 0) All the files are in defaultStorage |
| 24 | * 1) All APK files are in the same directory, bound to mApkStorage, and bound to the |
| 25 | * InstallerSession's stage dir. The files are linked from mApkStorage to defaultStorage. |
| 26 | * 2) All lib files are in the sub directories as their names suggest, and in the same parent |
| 27 | * directory as the APK files. The files are linked from mApkStorage to defaultStorage. |
| 28 | * 3) OBB files are in another directory that is different from APK files and lib files, bound |
| 29 | * to mObbStorage. The files are linked from mObbStorage to defaultStorage. |
| 30 | * |
| 31 | * @throws IllegalStateException the session is not an Incremental installation session. |
| 32 | */ |
| 33 | |
| 34 | import static dalvik.system.VMRuntime.getInstructionSet; |
| 35 | |
| 36 | import android.annotation.NonNull; |
| 37 | import android.annotation.Nullable; |
Alex Buynytskyy | ea14d19 | 2019-12-13 15:42:18 -0800 | [diff] [blame] | 38 | import android.content.pm.DataLoaderParams; |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 39 | import android.content.pm.InstallationFile; |
| 40 | import android.os.IVold; |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 41 | import android.os.Process; |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 42 | import android.os.RemoteException; |
| 43 | import android.os.ServiceManager; |
| 44 | import android.util.ArraySet; |
| 45 | import android.util.Slog; |
| 46 | |
| 47 | import java.io.File; |
| 48 | import java.io.IOException; |
| 49 | import java.nio.file.Files; |
| 50 | import java.nio.file.Path; |
| 51 | import java.nio.file.Paths; |
| 52 | import java.util.Random; |
| 53 | |
| 54 | /** |
| 55 | * This class manages storage instances used during a package installation session. |
| 56 | * @hide |
| 57 | */ |
| 58 | public final class IncrementalFileStorages { |
| 59 | private static final String TAG = "IncrementalFileStorages"; |
| 60 | private @Nullable IncrementalStorage mDefaultStorage; |
| 61 | private @Nullable IncrementalStorage mApkStorage; |
| 62 | private @Nullable IncrementalStorage mObbStorage; |
| 63 | private @Nullable String mDefaultDir; |
| 64 | private @Nullable String mObbDir; |
| 65 | private @NonNull IncrementalManager mIncrementalManager; |
| 66 | private @Nullable ArraySet<String> mLibDirs; |
| 67 | private @NonNull String mPackageName; |
| 68 | private @NonNull File mStageDir; |
| 69 | |
| 70 | /** |
| 71 | * Set up files and directories used in an installation session. |
| 72 | * Currently only used by Incremental Installation. |
| 73 | * For Incremental installation, the expected outcome of this function is: |
| 74 | * 0) All the files are in defaultStorage |
| 75 | * 1) All APK files are in the same directory, bound to mApkStorage, and bound to the |
| 76 | * InstallerSession's stage dir. The files are linked from mApkStorage to defaultStorage. |
| 77 | * 2) All lib files are in the sub directories as their names suggest, and in the same parent |
| 78 | * directory as the APK files. The files are linked from mApkStorage to defaultStorage. |
| 79 | * 3) OBB files are in another directory that is different from APK files and lib files, bound |
| 80 | * to mObbStorage. The files are linked from mObbStorage to defaultStorage. |
| 81 | * |
| 82 | * @throws IllegalStateException the session is not an Incremental installation session. |
| 83 | */ |
| 84 | public IncrementalFileStorages(@NonNull String packageName, |
| 85 | @NonNull File stageDir, |
| 86 | @NonNull IncrementalManager incrementalManager, |
Alex Buynytskyy | ea14d19 | 2019-12-13 15:42:18 -0800 | [diff] [blame] | 87 | @NonNull DataLoaderParams dataLoaderParams) { |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 88 | mPackageName = packageName; |
| 89 | mStageDir = stageDir; |
| 90 | mIncrementalManager = incrementalManager; |
Alex Buynytskyy | 1ecfcec | 2019-12-17 12:10:41 -0800 | [diff] [blame] | 91 | if (dataLoaderParams.getComponentName().getPackageName().equals("local")) { |
| 92 | final String incrementalPath = dataLoaderParams.getArguments(); |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 93 | mDefaultStorage = mIncrementalManager.openStorage(incrementalPath); |
| 94 | mDefaultDir = incrementalPath; |
| 95 | return; |
| 96 | } |
| 97 | mDefaultDir = getTempDir(); |
| 98 | if (mDefaultDir == null) { |
| 99 | return; |
| 100 | } |
| 101 | mDefaultStorage = mIncrementalManager.createStorage(mDefaultDir, |
Alex Buynytskyy | ea14d19 | 2019-12-13 15:42:18 -0800 | [diff] [blame] | 102 | dataLoaderParams, |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 103 | IncrementalManager.CREATE_MODE_CREATE |
| 104 | | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Adds a file into the installation session. Makes sure it will be placed inside |
| 109 | * a proper storage instance, based on its file type. |
| 110 | */ |
| 111 | public void addFile(@NonNull InstallationFile file) throws IOException { |
| 112 | if (mDefaultStorage == null) { |
| 113 | throw new IOException("Cannot add file because default storage does not exist"); |
| 114 | } |
| 115 | if (file.getFileType() == InstallationFile.FILE_TYPE_APK) { |
| 116 | addApkFile(file); |
| 117 | } else if (file.getFileType() == InstallationFile.FILE_TYPE_OBB) { |
| 118 | addObbFile(file); |
| 119 | } else if (file.getFileType() == InstallationFile.FILE_TYPE_LIB) { |
| 120 | addLibFile(file); |
| 121 | } else { |
| 122 | throw new IOException("Unknown file type: " + file.getFileType()); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | private void addApkFile(@NonNull InstallationFile apk) throws IOException { |
| 127 | // Create a storage for APK files and lib files |
| 128 | final String stageDirPath = mStageDir.getAbsolutePath(); |
| 129 | if (mApkStorage == null) { |
| 130 | mApkStorage = mIncrementalManager.createStorage(stageDirPath, mDefaultStorage, |
| 131 | IncrementalManager.CREATE_MODE_CREATE |
| 132 | | IncrementalManager.CREATE_MODE_TEMPORARY_BIND); |
| 133 | mApkStorage.bind(stageDirPath); |
| 134 | } |
| 135 | |
| 136 | if (!new File(mDefaultDir, apk.getName()).exists()) { |
| 137 | mDefaultStorage.makeFile(apk.getName(), apk.getSize(), |
| 138 | apk.getMetadata()); |
| 139 | } |
| 140 | // Assuming APK files are already named properly, e.g., "base.apk" |
| 141 | mDefaultStorage.makeLink(apk.getName(), mApkStorage, apk.getName()); |
| 142 | } |
| 143 | |
| 144 | private void addLibFile(@NonNull InstallationFile lib) throws IOException { |
| 145 | // TODO(b/136132412): remove this after we have incfs support for lib file mapping |
| 146 | if (mApkStorage == null) { |
| 147 | throw new IOException("Cannot add lib file without adding an apk file first"); |
| 148 | } |
| 149 | if (mLibDirs == null) { |
| 150 | mLibDirs = new ArraySet<>(); |
| 151 | } |
| 152 | String current = ""; |
| 153 | final Path libDirPath = Paths.get(lib.getName()).getParent(); |
| 154 | final int numDirComponents = libDirPath.getNameCount(); |
| 155 | for (int i = 0; i < numDirComponents; i++) { |
| 156 | String dirName = libDirPath.getName(i).toString(); |
| 157 | try { |
| 158 | dirName = getInstructionSet(dirName); |
| 159 | } catch (IllegalArgumentException ignored) { |
| 160 | } |
| 161 | current += dirName; |
| 162 | if (!mLibDirs.contains(current)) { |
| 163 | mDefaultStorage.makeDirectory(current); |
| 164 | mApkStorage.makeDirectory(current); |
| 165 | mLibDirs.add(current); |
| 166 | } |
| 167 | current += '/'; |
| 168 | } |
| 169 | String libFilePath = current + Paths.get(lib.getName()).getFileName(); |
| 170 | mDefaultStorage.makeFile(libFilePath, lib.getSize(), lib.getMetadata()); |
| 171 | mDefaultStorage.makeLink(libFilePath, mApkStorage, libFilePath); |
| 172 | } |
| 173 | |
| 174 | private void addObbFile(@NonNull InstallationFile obb) throws IOException { |
| 175 | if (mObbStorage == null) { |
| 176 | // Create a storage for OBB files |
| 177 | mObbDir = getTempDir(); |
| 178 | if (mObbDir == null) { |
| 179 | throw new IOException("Failed to create obb storage directory."); |
| 180 | } |
| 181 | mObbStorage = mIncrementalManager.createStorage( |
| 182 | mObbDir, mDefaultStorage, |
| 183 | IncrementalManager.CREATE_MODE_CREATE |
| 184 | | IncrementalManager.CREATE_MODE_TEMPORARY_BIND); |
| 185 | } |
| 186 | mDefaultStorage.makeFile(obb.getName(), obb.getSize(), obb.getMetadata()); |
| 187 | mDefaultStorage.makeLink(obb.getName(), mObbStorage, obb.getName()); |
| 188 | } |
| 189 | |
| 190 | private boolean hasObb() { |
| 191 | return (mObbStorage != null && mObbDir != null); |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Starts loading data for default storage. |
| 196 | * TODO(b/136132412): update the implementation with latest API design. |
| 197 | */ |
| 198 | public boolean startLoading() { |
| 199 | if (mDefaultStorage == null) { |
| 200 | return false; |
| 201 | } |
| 202 | return mDefaultStorage.startLoading(); |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Sets up obb storage directory and create bindings. |
| 207 | */ |
| 208 | public void finishSetUp() { |
| 209 | if (!hasObb()) { |
| 210 | return; |
| 211 | } |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 212 | final String obbDir = "/storage/emulated/0/Android/obb"; |
| 213 | final String packageObbDir = String.format("%s/%s", obbDir, mPackageName); |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 214 | final String packageObbDirRoot = |
| 215 | String.format("/mnt/runtime/%s/emulated/0/Android/obb/", mPackageName); |
| 216 | final String[] obbDirs = { |
| 217 | packageObbDirRoot + "read", |
| 218 | packageObbDirRoot + "write", |
| 219 | packageObbDirRoot + "full", |
| 220 | packageObbDirRoot + "default", |
| 221 | String.format("/data/media/0/Android/obb/%s", mPackageName), |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 222 | packageObbDir, |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 223 | }; |
| 224 | try { |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 225 | Slog.i(TAG, "Creating obb directory '" + packageObbDir + "'"); |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 226 | final IVold vold = IVold.Stub.asInterface(ServiceManager.getServiceOrThrow("vold")); |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 227 | vold.setupAppDir(packageObbDir, obbDir, Process.ROOT_UID); |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 228 | for (String d : obbDirs) { |
| 229 | mObbStorage.bindPermanent(d); |
| 230 | } |
| 231 | } catch (ServiceManager.ServiceNotFoundException ex) { |
| 232 | Slog.e(TAG, "vold service is not found."); |
| 233 | cleanUp(); |
| 234 | } catch (IOException | RemoteException ex) { |
Martijn Coenen | 9fd2b64 | 2019-12-24 13:04:36 +0100 | [diff] [blame] | 235 | Slog.e(TAG, "Failed to create obb dir at: " + packageObbDir, ex); |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 236 | cleanUp(); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Resets the states and unbinds storage instances for an installation session. |
| 242 | * TODO(b/136132412): make sure unnecessary binds are removed but useful storages are kept |
| 243 | */ |
| 244 | public void cleanUp() { |
| 245 | if (mDefaultStorage != null && mDefaultDir != null) { |
| 246 | try { |
| 247 | mDefaultStorage.unBind(mDefaultDir); |
| 248 | } catch (IOException ignored) { |
| 249 | } |
| 250 | mDefaultDir = null; |
| 251 | mDefaultStorage = null; |
| 252 | } |
| 253 | if (mApkStorage != null && mStageDir != null) { |
| 254 | try { |
| 255 | mApkStorage.unBind(mStageDir.getAbsolutePath()); |
| 256 | } catch (IOException ignored) { |
| 257 | } |
| 258 | mApkStorage = null; |
| 259 | } |
| 260 | if (mObbStorage != null && mObbDir != null) { |
| 261 | try { |
| 262 | mObbStorage.unBind(mObbDir); |
| 263 | } catch (IOException ignored) { |
| 264 | } |
| 265 | mObbDir = null; |
| 266 | mObbStorage = null; |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | private String getTempDir() { |
Songchun Fan | 720a781 | 2019-12-17 19:31:18 -0800 | [diff] [blame] | 271 | final String tmpDirRoot = "/data/incremental/tmp"; |
Songchun Fan | 4e75869 | 2019-11-29 15:43:27 -0800 | [diff] [blame] | 272 | final Random random = new Random(); |
| 273 | final Path tmpDir = |
| 274 | Paths.get(tmpDirRoot, String.valueOf(random.nextInt(Integer.MAX_VALUE - 1))); |
| 275 | try { |
| 276 | Files.createDirectories(tmpDir); |
| 277 | } catch (Exception ex) { |
| 278 | Slog.e(TAG, "Failed to create dir", ex); |
| 279 | return null; |
| 280 | } |
| 281 | return tmpDir.toAbsolutePath().toString(); |
| 282 | } |
| 283 | } |