1 /* 2 * Copyright (C) 2021 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.system.virtualmachine; 18 19 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 20 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; 21 22 import android.annotation.CallbackExecutor; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.os.Binder; 27 import android.os.IBinder; 28 import android.os.ParcelFileDescriptor; 29 import android.os.RemoteException; 30 import android.os.ServiceManager; 31 import android.system.virtualizationservice.IVirtualMachine; 32 import android.system.virtualizationservice.IVirtualMachineCallback; 33 import android.system.virtualizationservice.IVirtualizationService; 34 import android.system.virtualizationservice.PartitionType; 35 import android.system.virtualizationservice.VirtualMachineAppConfig; 36 import android.system.virtualizationservice.VirtualMachineState; 37 import android.util.JsonReader; 38 39 import com.android.internal.annotations.GuardedBy; 40 41 import java.io.File; 42 import java.io.FileInputStream; 43 import java.io.FileNotFoundException; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.InputStreamReader; 48 import java.nio.file.FileAlreadyExistsException; 49 import java.nio.file.Files; 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Optional; 53 import java.util.concurrent.Executor; 54 import java.util.concurrent.ExecutorService; 55 import java.util.concurrent.Executors; 56 import java.util.concurrent.Future; 57 import java.util.concurrent.atomic.AtomicBoolean; 58 import java.util.function.Consumer; 59 import java.util.zip.ZipFile; 60 61 /** 62 * A handle to the virtual machine. The virtual machine is local to the app which created the 63 * virtual machine. 64 * 65 * @hide 66 */ 67 public class VirtualMachine { 68 /** Name of the directory under the files directory where all VMs created for the app exist. */ 69 private static final String VM_DIR = "vm"; 70 71 /** Name of the persisted config file for a VM. */ 72 private static final String CONFIG_FILE = "config.xml"; 73 74 /** Name of the instance image file for a VM. (Not implemented) */ 75 private static final String INSTANCE_IMAGE_FILE = "instance.img"; 76 77 /** Name of the idsig file for a VM */ 78 private static final String IDSIG_FILE = "idsig"; 79 80 /** Name of the idsig files for extra APKs. */ 81 private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_"; 82 83 /** Name of the virtualization service. */ 84 private static final String SERVICE_NAME = "android.system.virtualizationservice"; 85 86 /** Status of a virtual machine */ 87 public enum Status { 88 /** The virtual machine has just been created, or {@link #stop()} was called on it. */ 89 STOPPED, 90 /** The virtual machine is running. */ 91 RUNNING, 92 /** 93 * The virtual machine is deleted. This is a irreversable state. Once a virtual machine is 94 * deleted, it can never be undone, which means all its secrets are permanently lost. 95 */ 96 DELETED, 97 } 98 99 /** Lock for internal synchronization. */ 100 private final Object mLock = new Object(); 101 102 /** The package which owns this VM. */ 103 private final @NonNull String mPackageName; 104 105 /** Name of this VM within the package. The name should be unique in the package. */ 106 private final @NonNull String mName; 107 108 /** 109 * Path to the config file for this VM. The config file is where the configuration is persisted. 110 */ 111 private final @NonNull File mConfigFilePath; 112 113 /** Path to the instance image file for this VM. */ 114 private final @NonNull File mInstanceFilePath; 115 116 /** Path to the idsig file for this VM. */ 117 private final @NonNull File mIdsigFilePath; 118 119 private static class ExtraApkSpec { 120 public final File apk; 121 public final File idsig; 122 ExtraApkSpec(File apk, File idsig)123 ExtraApkSpec(File apk, File idsig) { 124 this.apk = apk; 125 this.idsig = idsig; 126 } 127 } 128 129 /** 130 * List of extra apks. Apks are specified by the vm config, and corresponding idsigs are to be 131 * generated. 132 */ 133 private final @NonNull List<ExtraApkSpec> mExtraApks; 134 135 /** Size of the instance image. 10 MB. */ 136 private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024; 137 138 /** The configuration that is currently associated with this VM. */ 139 private @NonNull VirtualMachineConfig mConfig; 140 141 /** Handle to the "running" VM. */ 142 private @Nullable IVirtualMachine mVirtualMachine; 143 144 /** The registered callback */ 145 @GuardedBy("mLock") 146 private @Nullable VirtualMachineCallback mCallback; 147 148 /** The executor on which the callback will be executed */ 149 @GuardedBy("mLock") 150 private @Nullable Executor mCallbackExecutor; 151 152 private @Nullable ParcelFileDescriptor mConsoleReader; 153 private @Nullable ParcelFileDescriptor mConsoleWriter; 154 155 private @Nullable ParcelFileDescriptor mLogReader; 156 private @Nullable ParcelFileDescriptor mLogWriter; 157 158 private final ExecutorService mExecutorService = Executors.newCachedThreadPool(); 159 160 static { 161 System.loadLibrary("virtualmachine_jni"); 162 } 163 VirtualMachine( @onNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)164 private VirtualMachine( 165 @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config) 166 throws VirtualMachineException { 167 mPackageName = context.getPackageName(); 168 mName = name; 169 mConfig = config; 170 mConfigFilePath = getConfigFilePath(context, name); 171 172 final File vmRoot = new File(context.getFilesDir(), VM_DIR); 173 final File thisVmDir = new File(vmRoot, mName); 174 mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE); 175 mIdsigFilePath = new File(thisVmDir, IDSIG_FILE); 176 mExtraApks = setupExtraApks(context, config, thisVmDir); 177 } 178 179 /** 180 * Creates a virtual machine with the given name and config. Once a virtual machine is created 181 * it is persisted until it is deleted by calling {@link #delete()}. The created virtual machine 182 * is in {@link #STOPPED} state. To run the VM, call {@link #run()}. 183 */ create( @onNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)184 /* package */ static @NonNull VirtualMachine create( 185 @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config) 186 throws VirtualMachineException { 187 if (config == null) { 188 throw new VirtualMachineException("null config"); 189 } 190 VirtualMachine vm = new VirtualMachine(context, name, config); 191 192 try { 193 final File thisVmDir = vm.mConfigFilePath.getParentFile(); 194 Files.createDirectories(thisVmDir.getParentFile().toPath()); 195 196 // The checking of the existence of this directory and the creation of it is done 197 // atomically. If the directory already exists (i.e. the VM with the same name was 198 // already created), FileAlreadyExistsException is thrown 199 Files.createDirectory(thisVmDir.toPath()); 200 201 try (FileOutputStream output = new FileOutputStream(vm.mConfigFilePath)) { 202 vm.mConfig.serialize(output); 203 } 204 } catch (FileAlreadyExistsException e) { 205 throw new VirtualMachineException("virtual machine already exists", e); 206 } catch (IOException e) { 207 throw new VirtualMachineException(e); 208 } 209 210 try { 211 vm.mInstanceFilePath.createNewFile(); 212 } catch (IOException e) { 213 throw new VirtualMachineException("failed to create instance image", e); 214 } 215 216 IVirtualizationService service = 217 IVirtualizationService.Stub.asInterface( 218 ServiceManager.waitForService(SERVICE_NAME)); 219 220 try { 221 service.initializeWritablePartition( 222 ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE), 223 INSTANCE_FILE_SIZE, 224 PartitionType.ANDROID_VM_INSTANCE); 225 } catch (FileNotFoundException e) { 226 throw new VirtualMachineException("instance image missing", e); 227 } catch (RemoteException e) { 228 throw new VirtualMachineException("failed to create instance partition", e); 229 } 230 231 return vm; 232 } 233 234 /** Loads a virtual machine that is already created before. */ load( @onNull Context context, @NonNull String name)235 /* package */ static @Nullable VirtualMachine load( 236 @NonNull Context context, @NonNull String name) throws VirtualMachineException { 237 File configFilePath = getConfigFilePath(context, name); 238 VirtualMachineConfig config; 239 try (FileInputStream input = new FileInputStream(configFilePath)) { 240 config = VirtualMachineConfig.from(input); 241 } catch (FileNotFoundException e) { 242 // The VM doesn't exist. 243 return null; 244 } catch (IOException e) { 245 throw new VirtualMachineException(e); 246 } 247 248 VirtualMachine vm = new VirtualMachine(context, name, config); 249 250 // If config file exists, but the instance image file doesn't, it means that the VM is 251 // corrupted. That's different from the case that the VM doesn't exist. Throw an exception 252 // instead of returning null. 253 if (!vm.mInstanceFilePath.exists()) { 254 throw new VirtualMachineException("instance image missing"); 255 } 256 257 return vm; 258 } 259 260 /** 261 * Returns the name of this virtual machine. The name is unique in the package and can't be 262 * changed. 263 */ getName()264 public @NonNull String getName() { 265 return mName; 266 } 267 268 /** 269 * Returns the currently selected config of this virtual machine. There can be multiple virtual 270 * machines sharing the same config. Even in that case, the virtual machines are completely 271 * isolated from each other; one cannot share its secret to another virtual machine even if they 272 * share the same config. It is also possible that a virtual machine can switch its config, 273 * which can be done by calling {@link #setConfig(VirtualMachineCOnfig)}. 274 */ getConfig()275 public @NonNull VirtualMachineConfig getConfig() { 276 return mConfig; 277 } 278 279 /** Returns the current status of this virtual machine. */ getStatus()280 public @NonNull Status getStatus() throws VirtualMachineException { 281 try { 282 if (mVirtualMachine != null) { 283 switch (mVirtualMachine.getState()) { 284 case VirtualMachineState.NOT_STARTED: 285 return Status.STOPPED; 286 case VirtualMachineState.STARTING: 287 case VirtualMachineState.STARTED: 288 case VirtualMachineState.READY: 289 case VirtualMachineState.FINISHED: 290 return Status.RUNNING; 291 case VirtualMachineState.DEAD: 292 return Status.STOPPED; 293 } 294 } 295 } catch (RemoteException e) { 296 throw new VirtualMachineException(e); 297 } 298 if (!mConfigFilePath.exists()) { 299 return Status.DELETED; 300 } 301 return Status.STOPPED; 302 } 303 304 /** 305 * Registers the callback object to get events from the virtual machine. If a callback was 306 * already registered, it is replaced with the new one. 307 */ setCallback( @onNull @allbackExecutor Executor executor, @NonNull VirtualMachineCallback callback)308 public void setCallback( 309 @NonNull @CallbackExecutor Executor executor, 310 @NonNull VirtualMachineCallback callback) { 311 synchronized (mLock) { 312 mCallback = callback; 313 mCallbackExecutor = executor; 314 } 315 } 316 317 /** Clears the currently registered callback. */ clearCallback()318 public void clearCallback() { 319 synchronized (mLock) { 320 mCallback = null; 321 mCallbackExecutor = null; 322 } 323 } 324 325 /** Executes a callback on the callback executor. */ executeCallback(Consumer<VirtualMachineCallback> fn)326 private void executeCallback(Consumer<VirtualMachineCallback> fn) { 327 final VirtualMachineCallback callback; 328 final Executor executor; 329 synchronized (mLock) { 330 callback = mCallback; 331 executor = mCallbackExecutor; 332 } 333 if (callback == null || executor == null) { 334 return; 335 } 336 final long restoreToken = Binder.clearCallingIdentity(); 337 try { 338 executor.execute(() -> fn.accept(callback)); 339 } finally { 340 Binder.restoreCallingIdentity(restoreToken); 341 } 342 } 343 344 /** 345 * Runs this virtual machine. The returning of this method however doesn't mean that the VM has 346 * actually started running or the OS has booted there. Such events can be notified by 347 * registering a callback object (not implemented currently). 348 */ run()349 public void run() throws VirtualMachineException { 350 if (getStatus() != Status.STOPPED) { 351 throw new VirtualMachineException(this + " is not in stopped state"); 352 } 353 354 try { 355 mIdsigFilePath.createNewFile(); 356 for (ExtraApkSpec extraApk : mExtraApks) { 357 extraApk.idsig.createNewFile(); 358 } 359 } catch (IOException e) { 360 // If the file already exists, exception is not thrown. 361 throw new VirtualMachineException("failed to create idsig file", e); 362 } 363 364 IVirtualizationService service = 365 IVirtualizationService.Stub.asInterface( 366 ServiceManager.waitForService(SERVICE_NAME)); 367 368 try { 369 if (mConsoleReader == null && mConsoleWriter == null) { 370 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 371 mConsoleReader = pipe[0]; 372 mConsoleWriter = pipe[1]; 373 } 374 375 if (mLogReader == null && mLogWriter == null) { 376 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 377 mLogReader = pipe[0]; 378 mLogWriter = pipe[1]; 379 } 380 381 VirtualMachineAppConfig appConfig = getConfig().toParcel(); 382 383 // Fill the idsig file by hashing the apk 384 service.createOrUpdateIdsigFile( 385 appConfig.apk, ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_WRITE)); 386 387 for (ExtraApkSpec extraApk : mExtraApks) { 388 service.createOrUpdateIdsigFile( 389 ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY), 390 ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_WRITE)); 391 } 392 393 // Re-open idsig file in read-only mode 394 appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY); 395 appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE); 396 List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>(); 397 for (ExtraApkSpec extraApk : mExtraApks) { 398 extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY)); 399 } 400 appConfig.extraIdsigs = extraIdsigs; 401 402 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel = 403 android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig); 404 405 // The VM should only be observed to die once 406 AtomicBoolean onDiedCalled = new AtomicBoolean(false); 407 408 IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { 409 @Override 410 public void binderDied() { 411 if (onDiedCalled.compareAndSet(false, true)) { 412 executeCallback((cb) -> cb.onDied(VirtualMachine.this, 413 VirtualMachineCallback.DEATH_REASON_VIRTUALIZATIONSERVICE_DIED)); 414 } 415 } 416 }; 417 418 mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, mLogWriter); 419 mVirtualMachine.registerCallback( 420 new IVirtualMachineCallback.Stub() { 421 @Override 422 public void onPayloadStarted(int cid, ParcelFileDescriptor stream) { 423 executeCallback( 424 (cb) -> cb.onPayloadStarted(VirtualMachine.this, stream)); 425 } 426 @Override 427 public void onPayloadReady(int cid) { 428 executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this)); 429 } 430 @Override 431 public void onPayloadFinished(int cid, int exitCode) { 432 executeCallback( 433 (cb) -> cb.onPayloadFinished(VirtualMachine.this, exitCode)); 434 } 435 @Override 436 public void onError(int cid, int errorCode, String message) { 437 executeCallback( 438 (cb) -> cb.onError(VirtualMachine.this, errorCode, message)); 439 } 440 @Override 441 public void onDied(int cid, int reason) { 442 service.asBinder().unlinkToDeath(deathRecipient, 0); 443 if (onDiedCalled.compareAndSet(false, true)) { 444 executeCallback((cb) -> cb.onDied(VirtualMachine.this, reason)); 445 } 446 } 447 } 448 ); 449 service.asBinder().linkToDeath(deathRecipient, 0); 450 mVirtualMachine.start(); 451 } catch (IOException e) { 452 throw new VirtualMachineException(e); 453 } catch (RemoteException e) { 454 throw new VirtualMachineException(e); 455 } 456 } 457 458 /** Returns the stream object representing the console output from the virtual machine. */ getConsoleOutputStream()459 public @NonNull InputStream getConsoleOutputStream() throws VirtualMachineException { 460 if (mConsoleReader == null) { 461 throw new VirtualMachineException("Console output not available"); 462 } 463 return new FileInputStream(mConsoleReader.getFileDescriptor()); 464 } 465 466 /** Returns the stream object representing the log output from the virtual machine. */ getLogOutputStream()467 public @NonNull InputStream getLogOutputStream() throws VirtualMachineException { 468 if (mLogReader == null) { 469 throw new VirtualMachineException("Log output not available"); 470 } 471 return new FileInputStream(mLogReader.getFileDescriptor()); 472 } 473 474 /** 475 * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real 476 * computer; the machine halts immediately. Software running on the virtual machine is not 477 * notified with the event. A stopped virtual machine can be re-started by calling {@link 478 * #run()}. 479 */ stop()480 public void stop() throws VirtualMachineException { 481 // Dropping the IVirtualMachine handle stops the VM 482 mVirtualMachine = null; 483 } 484 485 /** 486 * Deletes this virtual machine. Deleting a virtual machine means deleting any persisted data 487 * associated with it including the per-VM secret. This is an irreversable action. A virtual 488 * machine once deleted can never be restored. A new virtual machine created with the same name 489 * and the same config is different from an already deleted virtual machine. 490 */ delete()491 public void delete() throws VirtualMachineException { 492 if (getStatus() != Status.STOPPED) { 493 throw new VirtualMachineException("Virtual machine is not stopped"); 494 } 495 final File vmRootDir = mConfigFilePath.getParentFile(); 496 for (ExtraApkSpec extraApks : mExtraApks) { 497 extraApks.idsig.delete(); 498 } 499 mConfigFilePath.delete(); 500 mInstanceFilePath.delete(); 501 mIdsigFilePath.delete(); 502 vmRootDir.delete(); 503 } 504 505 /** Returns the CID of this virtual machine, if it is running. */ getCid()506 public @NonNull Optional<Integer> getCid() throws VirtualMachineException { 507 if (getStatus() != Status.RUNNING) { 508 return Optional.empty(); 509 } 510 try { 511 return Optional.of(mVirtualMachine.getCid()); 512 } catch (RemoteException e) { 513 throw new VirtualMachineException(e); 514 } 515 } 516 517 /** 518 * Changes the config of this virtual machine to a new one. This can be used to adjust things 519 * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the 520 * application to run on the virtual machine, etc.) However, changing a config might make the 521 * virtual machine un-bootable if the new config is not compatible with the existing one. For 522 * example, if the signer of the app payload in the new config is different from that of the old 523 * config, the virtual machine won't boot. To prevent such cases, this method returns exception 524 * when an incompatible config is attempted. 525 * 526 * @return the old config 527 */ setConfig(@onNull VirtualMachineConfig newConfig)528 public @NonNull VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig) 529 throws VirtualMachineException { 530 final VirtualMachineConfig oldConfig = getConfig(); 531 if (!oldConfig.isCompatibleWith(newConfig)) { 532 throw new VirtualMachineException("incompatible config"); 533 } 534 if (getStatus() != Status.STOPPED) { 535 throw new VirtualMachineException( 536 "can't change config while virtual machine is not stopped"); 537 } 538 539 try { 540 FileOutputStream output = new FileOutputStream(mConfigFilePath); 541 newConfig.serialize(output); 542 output.close(); 543 } catch (IOException e) { 544 throw new VirtualMachineException(e); 545 } 546 mConfig = newConfig; 547 548 return oldConfig; 549 } 550 nativeConnectToVsockServer(IBinder vmBinder, int port)551 private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port); 552 553 /** 554 * Connects to a VM's RPC server via vsock, and returns a root IBinder object. Guest VMs are 555 * expected to set up vsock servers in their payload. After the host app receives onPayloadReady 556 * callback, the host app can use this method to establish an RPC session to the guest VMs. 557 * 558 * <p>If the connection succeeds, the root IBinder object will be returned via {@link 559 * VirtualMachineCallback.onVsockServerReady()}. If the connection fails, {@link 560 * VirtualMachineCallback.onVsockServerConnectionFailed()} will be called. 561 */ connectToVsockServer(int port)562 public Future<IBinder> connectToVsockServer(int port) throws VirtualMachineException { 563 if (getStatus() != Status.RUNNING) { 564 throw new VirtualMachineException("VM is not running"); 565 } 566 return mExecutorService.submit( 567 () -> nativeConnectToVsockServer(mVirtualMachine.asBinder(), port)); 568 } 569 570 @Override toString()571 public String toString() { 572 StringBuilder sb = new StringBuilder(); 573 sb.append("VirtualMachine("); 574 sb.append("name:" + getName() + ", "); 575 sb.append("config:" + getConfig().getPayloadConfigPath() + ", "); 576 sb.append("package: " + mPackageName); 577 sb.append(")"); 578 return sb.toString(); 579 } 580 parseExtraApkListFromPayloadConfig(JsonReader reader)581 private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader) 582 throws VirtualMachineException { 583 /** 584 * JSON schema from packages/modules/Virtualization/microdroid/payload/config/src/lib.rs: 585 * 586 * <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... } 587 */ 588 try { 589 List<String> apks = new ArrayList<>(); 590 591 reader.beginObject(); 592 while (reader.hasNext()) { 593 if (reader.nextName().equals("extra_apks")) { 594 reader.beginArray(); 595 while (reader.hasNext()) { 596 reader.beginObject(); 597 String name = reader.nextName(); 598 if (name.equals("path")) { 599 apks.add(reader.nextString()); 600 } else { 601 reader.skipValue(); 602 } 603 reader.endObject(); 604 } 605 reader.endArray(); 606 } else { 607 reader.skipValue(); 608 } 609 } 610 reader.endObject(); 611 return apks; 612 } catch (IOException e) { 613 throw new VirtualMachineException(e); 614 } 615 } 616 617 /** 618 * Reads the payload config inside the application, parses extra APK information, and then 619 * creates corresponding idsig file paths. 620 */ setupExtraApks( @onNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)621 private static List<ExtraApkSpec> setupExtraApks( 622 @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir) 623 throws VirtualMachineException { 624 try { 625 ZipFile zipFile = new ZipFile(context.getPackageCodePath()); 626 String payloadPath = config.getPayloadConfigPath(); 627 InputStream inputStream = 628 zipFile.getInputStream(zipFile.getEntry(config.getPayloadConfigPath())); 629 List<String> apkList = 630 parseExtraApkListFromPayloadConfig( 631 new JsonReader(new InputStreamReader(inputStream))); 632 633 List<ExtraApkSpec> extraApks = new ArrayList<>(); 634 for (int i = 0; i < apkList.size(); ++i) { 635 extraApks.add( 636 new ExtraApkSpec( 637 new File(apkList.get(i)), 638 new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i))); 639 } 640 641 return extraApks; 642 } catch (IOException e) { 643 throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e); 644 } 645 } 646 getConfigFilePath(@onNull Context context, @NonNull String name)647 private static File getConfigFilePath(@NonNull Context context, @NonNull String name) { 648 final File vmRoot = new File(context.getFilesDir(), VM_DIR); 649 final File thisVmDir = new File(vmRoot, name); 650 return new File(thisVmDir, CONFIG_FILE); 651 } 652 } 653