• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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