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 com.android.dynsystem;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.gsi.AvbPublicKey;
22 import android.gsi.IGsiService;
23 import android.net.Uri;
24 import android.os.AsyncTask;
25 import android.os.Build;
26 import android.os.SharedMemory;
27 import android.os.SystemProperties;
28 import android.os.image.DynamicSystemManager;
29 import android.service.persistentdata.PersistentDataBlockManager;
30 import android.system.ErrnoException;
31 import android.util.Log;
32 import android.util.Pair;
33 import android.util.Range;
34 import android.webkit.URLUtil;
35 
36 import org.json.JSONException;
37 
38 import java.io.BufferedInputStream;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.net.URL;
43 import java.nio.ByteBuffer;
44 import java.util.Arrays;
45 import java.util.Enumeration;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.concurrent.ExecutionException;
49 import java.util.concurrent.ExecutorService;
50 import java.util.concurrent.Executors;
51 import java.util.concurrent.Future;
52 import java.util.zip.GZIPInputStream;
53 import java.util.zip.ZipEntry;
54 import java.util.zip.ZipFile;
55 import java.util.zip.ZipInputStream;
56 
57 class InstallationAsyncTask extends AsyncTask<String, Long, Throwable> {
58 
59     private static final String TAG = "InstallationAsyncTask";
60 
61     private static final int MIN_SHARED_MEMORY_SIZE = 8 << 10; // 8KiB
62     private static final int MAX_SHARED_MEMORY_SIZE = 8 << 20; // 8MiB
63     private static final int DEFAULT_SHARED_MEMORY_SIZE = 512 << 10; // 512KiB
64     private static final String SHARED_MEMORY_SIZE_PROP =
65             "dynamic_system.data_transfer.shared_memory.size";
66 
67     private static final long MIN_PROGRESS_TO_PUBLISH = 1 << 27;
68 
69     private static final List<String> UNSUPPORTED_PARTITIONS =
70             Arrays.asList(
71                     "vbmeta", "boot", "userdata", "dtbo", "super_empty", "system_other", "scratch");
72 
73     private class UnsupportedUrlException extends Exception {
UnsupportedUrlException(String message)74         private UnsupportedUrlException(String message) {
75             super(message);
76         }
77     }
78 
79     private class UnsupportedFormatException extends Exception {
UnsupportedFormatException(String message)80         private UnsupportedFormatException(String message) {
81             super(message);
82         }
83     }
84 
85     static class ImageValidationException extends Exception {
ImageValidationException(String message)86         ImageValidationException(String message) {
87             super(message);
88         }
89 
ImageValidationException(Throwable cause)90         ImageValidationException(Throwable cause) {
91             super(cause);
92         }
93     }
94 
95     static class RevocationListFetchException extends ImageValidationException {
RevocationListFetchException(Throwable cause)96         RevocationListFetchException(Throwable cause) {
97             super(cause);
98         }
99     }
100 
101     static class KeyRevokedException extends ImageValidationException {
KeyRevokedException(String message)102         KeyRevokedException(String message) {
103             super(message);
104         }
105     }
106 
107     static class PublicKeyException extends ImageValidationException {
PublicKeyException(String message)108         PublicKeyException(String message) {
109             super(message);
110         }
111     }
112 
113     static class InsufficientSpaceException extends IOException {
InsufficientSpaceException(String message)114         InsufficientSpaceException(String message) {
115             super(message);
116         }
117     }
118 
119     /** UNSET means the installation is not completed */
120     static final int RESULT_UNSET = 0;
121 
122     static final int RESULT_OK = 1;
123     static final int RESULT_CANCELLED = 2;
124     static final int RESULT_ERROR_IO = 3;
125     static final int RESULT_ERROR_UNSUPPORTED_URL = 4;
126     static final int RESULT_ERROR_UNSUPPORTED_FORMAT = 5;
127     static final int RESULT_ERROR_EXCEPTION = 6;
128 
129     static class Progress {
130         public final String partitionName;
131         public final long installedBytes;
132         public final long totalBytes;
133         public final int partitionNumber;
134         public final int totalPartitionNumber;
135         public final int totalProgressPercentage;
136 
Progress( String partitionName, long installedBytes, long totalBytes, int partitionNumber, int totalPartitionNumber, int totalProgressPercentage)137         Progress(
138                 String partitionName,
139                 long installedBytes,
140                 long totalBytes,
141                 int partitionNumber,
142                 int totalPartitionNumber,
143                 int totalProgressPercentage) {
144             this.partitionName = partitionName;
145             this.installedBytes = installedBytes;
146             this.totalBytes = totalBytes;
147             this.partitionNumber = partitionNumber;
148             this.totalPartitionNumber = totalPartitionNumber;
149             this.totalProgressPercentage = totalProgressPercentage;
150         }
151     }
152 
153     interface ProgressListener {
onProgressUpdate(Progress progress)154         void onProgressUpdate(Progress progress);
155 
onResult(int resultCode, Throwable detail)156         void onResult(int resultCode, Throwable detail);
157     }
158 
159     private static class MappedMemoryBuffer implements AutoCloseable {
160         public ByteBuffer mBuffer;
161 
MappedMemoryBuffer(@onNull ByteBuffer buffer)162         MappedMemoryBuffer(@NonNull ByteBuffer buffer) {
163             mBuffer = buffer;
164         }
165 
166         @Override
close()167         public void close() {
168             if (mBuffer != null) {
169                 SharedMemory.unmap(mBuffer);
170                 mBuffer = null;
171             }
172         }
173     }
174 
175     private final int mSharedMemorySize;
176     private final String mUrl;
177     private final String mDsuSlot;
178     private final String mPublicKey;
179     private final long mSystemSize;
180     private final long mUserdataSize;
181     private final Context mContext;
182     private final DynamicSystemManager mDynSystem;
183     private final ProgressListener mListener;
184     private final boolean mIsNetworkUrl;
185     private final boolean mIsDeviceBootloaderUnlocked;
186     private final boolean mWantScratchPartition;
187     private int mCreatePartitionStatus;
188     private DynamicSystemManager.Session mInstallationSession;
189     private KeyRevocationList mKeyRevocationList;
190 
191     private boolean mIsZip;
192     private boolean mIsCompleted;
193     private InputStream mStream;
194     private ZipFile mZipFile;
195 
196     private static final double PROGRESS_READONLY_PARTITION_WEIGHT = 0.8;
197     private static final double PROGRESS_WRITABLE_PARTITION_WEIGHT = 0.2;
198 
199     private String mProgressPartitionName;
200     private long mProgressTotalBytes;
201     private int mProgressPartitionNumber;
202     private boolean mProgressPartitionIsReadonly;
203     private int mProgressCompletedReadonlyPartitions;
204     private int mProgressCompletedWritablePartitions;
205     private int mTotalReadonlyPartitions;
206     private int mTotalWritablePartitions;
207     private int mTotalPartitionNumber;
208 
InstallationAsyncTask( String url, String dsuSlot, String publicKey, long systemSize, long userdataSize, Context context, DynamicSystemManager dynSystem, ProgressListener listener)209     InstallationAsyncTask(
210             String url,
211             String dsuSlot,
212             String publicKey,
213             long systemSize,
214             long userdataSize,
215             Context context,
216             DynamicSystemManager dynSystem,
217             ProgressListener listener) {
218         mSharedMemorySize =
219                 Range.create(MIN_SHARED_MEMORY_SIZE, MAX_SHARED_MEMORY_SIZE)
220                         .clamp(
221                                 SystemProperties.getInt(
222                                         SHARED_MEMORY_SIZE_PROP, DEFAULT_SHARED_MEMORY_SIZE));
223         mUrl = url;
224         mDsuSlot = dsuSlot;
225         mPublicKey = publicKey;
226         mSystemSize = systemSize;
227         mUserdataSize = userdataSize;
228         mContext = context;
229         mDynSystem = dynSystem;
230         mListener = listener;
231         mIsNetworkUrl = URLUtil.isNetworkUrl(mUrl);
232         PersistentDataBlockManager pdbManager =
233                 (PersistentDataBlockManager)
234                         mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
235         mIsDeviceBootloaderUnlocked =
236                 (pdbManager != null)
237                         && (pdbManager.getFlashLockState()
238                                 == PersistentDataBlockManager.FLASH_LOCK_UNLOCKED);
239         mWantScratchPartition = Build.IS_DEBUGGABLE;
240     }
241 
242     @Override
doInBackground(String... voids)243     protected Throwable doInBackground(String... voids) {
244         Log.d(TAG, "Start doInBackground(), URL: " + mUrl);
245 
246         try {
247             // call DynamicSystemManager to cleanup stuff
248             mDynSystem.remove();
249 
250             verifyAndPrepare();
251 
252             mDynSystem.startInstallation(mDsuSlot);
253 
254             installUserdata();
255             if (isCancelled()) {
256                 mDynSystem.remove();
257                 return null;
258             }
259             if (mUrl == null) {
260                 mDynSystem.finishInstallation();
261                 return null;
262             }
263             installImages();
264             if (isCancelled()) {
265                 mDynSystem.remove();
266                 return null;
267             }
268 
269             if (mWantScratchPartition) {
270                 // If host is debuggable, then install a scratch partition so that we can do
271                 // adb remount in the guest system.
272                 try {
273                     installScratch();
274                 } catch (IOException e) {
275                     // Failing to install overlayFS scratch shouldn't be fatal.
276                     // Just ignore the error and skip installing the scratch partition.
277                     Log.w(TAG, e.toString(), e);
278                 }
279                 if (isCancelled()) {
280                     mDynSystem.remove();
281                     return null;
282                 }
283             }
284 
285             mDynSystem.finishInstallation();
286         } catch (Exception e) {
287             Log.e(TAG, e.toString(), e);
288             mDynSystem.remove();
289             return e;
290         } finally {
291             close();
292         }
293 
294         return null;
295     }
296 
297     @Override
onPostExecute(Throwable detail)298     protected void onPostExecute(Throwable detail) {
299         int result = RESULT_UNSET;
300 
301         if (detail == null) {
302             result = RESULT_OK;
303             mIsCompleted = true;
304         } else if (detail instanceof IOException) {
305             result = RESULT_ERROR_IO;
306         } else if (detail instanceof UnsupportedUrlException) {
307             result = RESULT_ERROR_UNSUPPORTED_URL;
308         } else if (detail instanceof UnsupportedFormatException) {
309             result = RESULT_ERROR_UNSUPPORTED_FORMAT;
310         } else {
311             result = RESULT_ERROR_EXCEPTION;
312         }
313 
314         Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + result);
315 
316         mListener.onResult(result, detail);
317     }
318 
319     @Override
onCancelled()320     protected void onCancelled() {
321         Log.d(TAG, "onCancelled(), URL: " + mUrl);
322 
323         if (mDynSystem.abort()) {
324             Log.d(TAG, "Installation aborted");
325         } else {
326             Log.w(TAG, "DynamicSystemManager.abort() returned false");
327         }
328 
329         mListener.onResult(RESULT_CANCELLED, null);
330     }
331 
332     @Override
onProgressUpdate(Long... progress)333     protected void onProgressUpdate(Long... progress) {
334         final long installedBytes = progress[0];
335         int totalProgressPercentage = 0;
336         if (mTotalPartitionNumber > 0) {
337             final double readonlyPartitionWeight =
338                     mTotalReadonlyPartitions > 0
339                             ? PROGRESS_READONLY_PARTITION_WEIGHT / mTotalReadonlyPartitions
340                             : 0;
341             final double writablePartitionWeight =
342                     mTotalWritablePartitions > 0
343                             ? PROGRESS_WRITABLE_PARTITION_WEIGHT / mTotalWritablePartitions
344                             : 0;
345             double totalProgress = 0.0;
346             if (mProgressTotalBytes > 0) {
347                 totalProgress +=
348                         (mProgressPartitionIsReadonly
349                                         ? readonlyPartitionWeight
350                                         : writablePartitionWeight)
351                                 * installedBytes
352                                 / mProgressTotalBytes;
353             }
354             totalProgress += readonlyPartitionWeight * mProgressCompletedReadonlyPartitions;
355             totalProgress += writablePartitionWeight * mProgressCompletedWritablePartitions;
356             totalProgressPercentage = (int) (totalProgress * 100);
357         }
358         mListener.onProgressUpdate(
359                 new Progress(
360                         mProgressPartitionName,
361                         installedBytes,
362                         mProgressTotalBytes,
363                         mProgressPartitionNumber,
364                         mTotalPartitionNumber,
365                         totalProgressPercentage));
366     }
367 
initPartitionProgress(String partitionName, long totalBytes, boolean readonly)368     private void initPartitionProgress(String partitionName, long totalBytes, boolean readonly) {
369         if (mProgressPartitionNumber > 0) {
370             // Assume previous partition completed successfully.
371             if (mProgressPartitionIsReadonly) {
372                 ++mProgressCompletedReadonlyPartitions;
373             } else {
374                 ++mProgressCompletedWritablePartitions;
375             }
376         }
377         mProgressPartitionName = partitionName;
378         mProgressTotalBytes = totalBytes;
379         mProgressPartitionIsReadonly = readonly;
380         ++mProgressPartitionNumber;
381     }
382 
verifyAndPrepare()383     private void verifyAndPrepare() throws Exception {
384         if (mUrl == null) {
385             return;
386         }
387         String extension = mUrl.substring(mUrl.lastIndexOf('.') + 1);
388 
389         if ("gz".equals(extension) || "gzip".equals(extension)) {
390             mIsZip = false;
391         } else if ("zip".equals(extension)) {
392             mIsZip = true;
393         } else {
394             throw new UnsupportedFormatException(
395                     String.format(Locale.US, "Unsupported file format: %s", mUrl));
396         }
397 
398         if (mIsNetworkUrl) {
399             mStream = new URL(mUrl).openStream();
400         } else if (URLUtil.isFileUrl(mUrl)) {
401             if (mIsZip) {
402                 mZipFile = new ZipFile(new File(new URL(mUrl).toURI()));
403             } else {
404                 mStream = new URL(mUrl).openStream();
405             }
406         } else if (URLUtil.isContentUrl(mUrl)) {
407             mStream = mContext.getContentResolver().openInputStream(Uri.parse(mUrl));
408         } else {
409             throw new UnsupportedUrlException(
410                     String.format(Locale.US, "Unsupported URL: %s", mUrl));
411         }
412 
413         boolean hasTotalPartitionNumber = false;
414         if (mIsZip) {
415             if (mZipFile != null) {
416                 // {*.img in zip} + {userdata}
417                 hasTotalPartitionNumber = true;
418                 mTotalReadonlyPartitions = calculateNumberOfImagesInLocalZip(mZipFile);
419                 mTotalWritablePartitions = 1;
420             } else {
421                 // TODO: Come up with a way to retrieve the number of total partitions from
422                 // network URL.
423             }
424         } else {
425             // gzip has exactly two partitions, {system, userdata}
426             hasTotalPartitionNumber = true;
427             mTotalReadonlyPartitions = 1;
428             mTotalWritablePartitions = 1;
429         }
430 
431         if (hasTotalPartitionNumber) {
432             if (mWantScratchPartition) {
433                 // {scratch}
434                 ++mTotalWritablePartitions;
435             }
436             mTotalPartitionNumber = mTotalReadonlyPartitions + mTotalWritablePartitions;
437         }
438 
439         try {
440             String listUrl = mContext.getString(R.string.key_revocation_list_url);
441             mKeyRevocationList = KeyRevocationList.fromUrl(new URL(listUrl));
442         } catch (IOException | JSONException e) {
443             mKeyRevocationList = new KeyRevocationList();
444             imageValidationThrowOrWarning(new RevocationListFetchException(e));
445         }
446         if (mKeyRevocationList.isRevoked(mPublicKey)) {
447             imageValidationThrowOrWarning(new KeyRevokedException(mPublicKey));
448         }
449     }
450 
imageValidationThrowOrWarning(ImageValidationException e)451     private void imageValidationThrowOrWarning(ImageValidationException e)
452             throws ImageValidationException {
453         if (mIsDeviceBootloaderUnlocked || !mIsNetworkUrl) {
454             // If device is OEM unlocked or DSU is being installed from a local file URI,
455             // then be permissive.
456             Log.w(TAG, e.toString());
457         } else {
458             throw e;
459         }
460     }
461 
installWritablePartition(final String partitionName, final long partitionSize)462     private void installWritablePartition(final String partitionName, final long partitionSize)
463             throws IOException {
464         Log.d(TAG, "Creating writable partition: " + partitionName + ", size: " + partitionSize);
465 
466         mCreatePartitionStatus = 0;
467         mInstallationSession = null;
468         Thread thread =
469                 new Thread() {
470                     @Override
471                     public void run() {
472                         Pair<Integer, DynamicSystemManager.Session> result =
473                                 mDynSystem.createPartition(
474                                         partitionName, partitionSize, /* readOnly = */ false);
475                         mCreatePartitionStatus = result.first;
476                         mInstallationSession = result.second;
477                     }
478                 };
479 
480         initPartitionProgress(partitionName, partitionSize, /* readonly = */ false);
481         publishProgress(/* installedSize = */ 0L);
482 
483         long prevInstalledSize = 0;
484         thread.start();
485         while (thread.isAlive()) {
486             if (isCancelled()) {
487                 return;
488             }
489 
490             final long installedSize = mDynSystem.getInstallationProgress().bytes_processed;
491 
492             if (installedSize > prevInstalledSize + MIN_PROGRESS_TO_PUBLISH) {
493                 publishProgress(installedSize);
494                 prevInstalledSize = installedSize;
495             }
496 
497             try {
498                 Thread.sleep(100);
499             } catch (InterruptedException e) {
500                 // Ignore the error.
501             }
502         }
503 
504         if (mInstallationSession == null) {
505             if (mCreatePartitionStatus == IGsiService.INSTALL_ERROR_NO_SPACE
506                     || mCreatePartitionStatus == IGsiService.INSTALL_ERROR_FILE_SYSTEM_CLUTTERED) {
507                 throw new InsufficientSpaceException(
508                         "Failed to create "
509                                 + partitionName
510                                 + " partition: storage media has insufficient free space");
511             } else {
512                 throw new IOException(
513                         "Failed to start installation with requested size: " + partitionSize);
514             }
515         }
516 
517         // Reset installation session and verify that installation completes successfully.
518         mInstallationSession = null;
519         if (!mDynSystem.closePartition()) {
520             throw new IOException("Failed to complete partition installation: " + partitionName);
521         }
522 
523         // Ensure a 100% mark is published.
524         if (prevInstalledSize != partitionSize) {
525             publishProgress(partitionSize);
526         }
527     }
528 
installScratch()529     private void installScratch() throws IOException {
530         installWritablePartition("scratch", mDynSystem.suggestScratchSize());
531     }
532 
installUserdata()533     private void installUserdata() throws IOException {
534         installWritablePartition("userdata", mUserdataSize);
535     }
536 
installImages()537     private void installImages() throws ExecutionException, IOException, ImageValidationException {
538         if (mStream != null) {
539             if (mIsZip) {
540                 installStreamingZipUpdate();
541             } else {
542                 installStreamingGzUpdate();
543             }
544         } else {
545             installLocalZipUpdate();
546         }
547     }
548 
installStreamingGzUpdate()549     private void installStreamingGzUpdate()
550             throws ExecutionException, IOException, ImageValidationException {
551         Log.d(TAG, "To install a streaming GZ update");
552         installImage("system", mSystemSize, new GZIPInputStream(mStream));
553     }
554 
shouldInstallEntry(String name)555     private boolean shouldInstallEntry(String name) {
556         if (!name.endsWith(".img")) {
557             return false;
558         }
559         String partitionName = name.substring(0, name.length() - 4);
560         if (UNSUPPORTED_PARTITIONS.contains(partitionName)) {
561             return false;
562         }
563         return true;
564     }
565 
calculateNumberOfImagesInLocalZip(ZipFile zipFile)566     private int calculateNumberOfImagesInLocalZip(ZipFile zipFile) {
567         int total = 0;
568         Enumeration<? extends ZipEntry> entries = zipFile.entries();
569         while (entries.hasMoreElements()) {
570             ZipEntry entry = entries.nextElement();
571             if (shouldInstallEntry(entry.getName())) {
572                 ++total;
573             }
574         }
575         return total;
576     }
577 
installStreamingZipUpdate()578     private void installStreamingZipUpdate()
579             throws ExecutionException, IOException, ImageValidationException {
580         Log.d(TAG, "To install a streaming ZIP update");
581 
582         ZipInputStream zis = new ZipInputStream(mStream);
583         ZipEntry entry = null;
584 
585         while ((entry = zis.getNextEntry()) != null) {
586             String name = entry.getName();
587             if (shouldInstallEntry(name)) {
588                 installImageFromAnEntry(entry, zis);
589             } else {
590                 Log.d(TAG, name + " installation is not supported, skip it.");
591             }
592 
593             if (isCancelled()) {
594                 break;
595             }
596         }
597     }
598 
installLocalZipUpdate()599     private void installLocalZipUpdate()
600             throws ExecutionException, IOException, ImageValidationException {
601         Log.d(TAG, "To install a local ZIP update");
602 
603         Enumeration<? extends ZipEntry> entries = mZipFile.entries();
604 
605         while (entries.hasMoreElements()) {
606             ZipEntry entry = entries.nextElement();
607             String name = entry.getName();
608             if (shouldInstallEntry(name)) {
609                 installImageFromAnEntry(entry, mZipFile.getInputStream(entry));
610             } else {
611                 Log.d(TAG, name + " installation is not supported, skip it.");
612             }
613 
614             if (isCancelled()) {
615                 break;
616             }
617         }
618     }
619 
installImageFromAnEntry(ZipEntry entry, InputStream is)620     private void installImageFromAnEntry(ZipEntry entry, InputStream is)
621             throws ExecutionException, IOException, ImageValidationException {
622         String name = entry.getName();
623 
624         Log.d(TAG, "ZipEntry: " + name);
625 
626         String partitionName = name.substring(0, name.length() - 4);
627         long uncompressedSize = entry.getSize();
628 
629         installImage(partitionName, uncompressedSize, is);
630     }
631 
installImage(String partitionName, long uncompressedSize, InputStream is)632     private void installImage(String partitionName, long uncompressedSize, InputStream is)
633             throws ExecutionException, IOException, ImageValidationException {
634 
635         SparseInputStream sis = new SparseInputStream(new BufferedInputStream(is));
636 
637         long unsparseSize = sis.getUnsparseSize();
638 
639         final long partitionSize;
640 
641         if (unsparseSize != -1) {
642             partitionSize = unsparseSize;
643             Log.d(TAG, partitionName + " is sparse, raw size = " + unsparseSize);
644         } else if (uncompressedSize != -1) {
645             partitionSize = uncompressedSize;
646             Log.d(TAG, partitionName + " is already unsparse, raw size = " + uncompressedSize);
647         } else {
648             throw new IOException("Cannot get raw size for " + partitionName);
649         }
650 
651         mCreatePartitionStatus = 0;
652         mInstallationSession = null;
653         Thread thread =
654                 new Thread() {
655                     @Override
656                     public void run() {
657                         Pair<Integer, DynamicSystemManager.Session> result =
658                                 mDynSystem.createPartition(
659                                         partitionName, partitionSize, /* readOnly = */ true);
660                         mCreatePartitionStatus = result.first;
661                         mInstallationSession = result.second;
662                     }
663                 };
664 
665         Log.d(TAG, "Start creating partition: " + partitionName);
666         thread.start();
667 
668         while (thread.isAlive()) {
669             if (isCancelled()) {
670                 return;
671             }
672 
673             try {
674                 Thread.sleep(100);
675             } catch (InterruptedException e) {
676                 // Ignore the error.
677             }
678         }
679 
680         if (mInstallationSession == null) {
681             if (mCreatePartitionStatus == IGsiService.INSTALL_ERROR_NO_SPACE
682                     || mCreatePartitionStatus == IGsiService.INSTALL_ERROR_FILE_SYSTEM_CLUTTERED) {
683                 throw new InsufficientSpaceException(
684                         "Failed to create "
685                                 + partitionName
686                                 + " partition: storage media has insufficient free space");
687             } else {
688                 throw new IOException(
689                         "Failed to start installation with requested size: " + partitionSize);
690             }
691         }
692 
693         Log.d(TAG, "Start installing: " + partitionName);
694 
695         long prevInstalledSize = 0;
696         try (SharedMemory sharedMemory =
697                         SharedMemory.create("dsu_buffer_" + partitionName, mSharedMemorySize);
698                 MappedMemoryBuffer mappedBuffer =
699                         new MappedMemoryBuffer(sharedMemory.mapReadWrite())) {
700             mInstallationSession.setAshmem(sharedMemory.getFdDup(), sharedMemory.getSize());
701 
702             initPartitionProgress(partitionName, partitionSize, /* readonly = */ true);
703             publishProgress(/* installedSize = */ 0L);
704 
705             long installedSize = 0;
706             byte[] readBuffer = new byte[sharedMemory.getSize()];
707             ByteBuffer buffer = mappedBuffer.mBuffer;
708             ExecutorService executor = Executors.newSingleThreadExecutor();
709             Future<Boolean> submitPromise = null;
710 
711             while (true) {
712                 final int numBytesRead = sis.read(readBuffer, 0, readBuffer.length);
713 
714                 if (submitPromise != null) {
715                     // Wait until the previous submit task is complete.
716                     while (true) {
717                         try {
718                             if (!submitPromise.get()) {
719                                 throw new IOException("Failed submitFromAshmem() to DynamicSystem");
720                             }
721                             break;
722                         } catch (InterruptedException e) {
723                             // Ignore.
724                         }
725                     }
726 
727                     // Publish the progress of the previous submit task.
728                     if (installedSize > prevInstalledSize + MIN_PROGRESS_TO_PUBLISH) {
729                         publishProgress(installedSize);
730                         prevInstalledSize = installedSize;
731                     }
732                 }
733 
734                 // Ensure the previous submit task (submitPromise) is complete before exiting the
735                 // loop.
736                 if (numBytesRead < 0) {
737                     break;
738                 }
739 
740                 if (isCancelled()) {
741                     return;
742                 }
743 
744                 buffer.position(0);
745                 buffer.put(readBuffer, 0, numBytesRead);
746                 submitPromise =
747                         executor.submit(() -> mInstallationSession.submitFromAshmem(numBytesRead));
748 
749                 // Even though we update the bytes counter here, the actual progress is updated only
750                 // after the submit task (submitPromise) is complete.
751                 installedSize += numBytesRead;
752             }
753         } catch (ErrnoException e) {
754             e.rethrowAsIOException();
755         }
756 
757         AvbPublicKey avbPublicKey = new AvbPublicKey();
758         if (!mInstallationSession.getAvbPublicKey(avbPublicKey)) {
759             imageValidationThrowOrWarning(new PublicKeyException("getAvbPublicKey() failed"));
760         } else {
761             String publicKey = toHexString(avbPublicKey.sha1);
762             if (mKeyRevocationList.isRevoked(publicKey)) {
763                 imageValidationThrowOrWarning(new KeyRevokedException(publicKey));
764             }
765         }
766 
767         // Reset installation session and verify that installation completes successfully.
768         mInstallationSession = null;
769         if (!mDynSystem.closePartition()) {
770             throw new IOException("Failed to complete partition installation: " + partitionName);
771         }
772 
773         // Ensure a 100% mark is published.
774         if (prevInstalledSize != partitionSize) {
775             publishProgress(partitionSize);
776         }
777     }
778 
toHexString(byte[] bytes)779     private static String toHexString(byte[] bytes) {
780         StringBuilder sb = new StringBuilder();
781         for (byte b : bytes) {
782             sb.append(String.format("%02x", b));
783         }
784         return sb.toString();
785     }
786 
close()787     private void close() {
788         try {
789             if (mStream != null) {
790                 mStream.close();
791                 mStream = null;
792             }
793             if (mZipFile != null) {
794                 mZipFile.close();
795                 mZipFile = null;
796             }
797         } catch (IOException e) {
798             // ignore
799         }
800     }
801 
isCompleted()802     boolean isCompleted() {
803         return mIsCompleted;
804     }
805 
commit(boolean oneShot)806     boolean commit(boolean oneShot) {
807         return mDynSystem.setEnable(true, oneShot);
808     }
809 }
810