Home | History | Annotate | Download | only in tests
      1 /*
      2  * Copyright (C) 2012 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.ota.tests;
     18 
     19 import static org.hamcrest.CoreMatchers.equalTo;
     20 import static org.hamcrest.CoreMatchers.not;
     21 import static org.junit.Assert.assertThat;
     22 
     23 import com.android.tradefed.build.IBuildInfo;
     24 import com.android.tradefed.build.IDeviceBuildInfo;
     25 import com.android.tradefed.build.OtaDeviceBuildInfo;
     26 import com.android.tradefed.config.IConfiguration;
     27 import com.android.tradefed.config.IConfigurationReceiver;
     28 import com.android.tradefed.config.Option;
     29 import com.android.tradefed.config.OptionClass;
     30 import com.android.tradefed.device.DeviceNotAvailableException;
     31 import com.android.tradefed.device.IManagedTestDevice;
     32 import com.android.tradefed.device.ITestDevice;
     33 import com.android.tradefed.device.TestDeviceState;
     34 import com.android.tradefed.log.LogReceiver;
     35 import com.android.tradefed.log.LogUtil.CLog;
     36 import com.android.tradefed.result.FileInputStreamSource;
     37 import com.android.tradefed.result.ITestInvocationListener;
     38 import com.android.tradefed.result.InputStreamSource;
     39 import com.android.tradefed.result.LogDataType;
     40 import com.android.tradefed.result.TestDescription;
     41 import com.android.tradefed.targetprep.BuildError;
     42 import com.android.tradefed.targetprep.ITargetPreparer;
     43 import com.android.tradefed.targetprep.TargetSetupError;
     44 import com.android.tradefed.testtype.IBuildReceiver;
     45 import com.android.tradefed.testtype.IDeviceTest;
     46 import com.android.tradefed.testtype.IResumableTest;
     47 import com.android.tradefed.util.DeviceRecoveryModeUtil;
     48 import com.android.tradefed.util.FileUtil;
     49 import com.android.tradefed.util.StreamUtil;
     50 import com.android.tradefed.util.proto.TfMetricProtoUtil;
     51 
     52 import org.junit.Assert;
     53 
     54 import java.io.DataInputStream;
     55 import java.io.DataOutputStream;
     56 import java.io.File;
     57 import java.io.IOException;
     58 import java.net.InetSocketAddress;
     59 import java.net.ServerSocket;
     60 import java.net.Socket;
     61 import java.net.SocketAddress;
     62 import java.util.HashMap;
     63 import java.util.Map;
     64 
     65 /**
     66  * A test that will perform repeated flash + install OTA actions on a device.
     67  * <p/>
     68  * adb must have root.
     69  * <p/>
     70  * Expects a {@link OtaDeviceBuildInfo}.
     71  * <p/>
     72  * Note: this test assumes that the {@link ITargetPreparer}s included in this test's
     73  * {@link IConfiguration} will flash the device back to a baseline build, and prepare the device to
     74  * receive the OTA to a new build.
     75  */
     76 @OptionClass(alias = "ota-stability")
     77 public class SideloadOtaStabilityTest implements IDeviceTest, IBuildReceiver,
     78         IConfigurationReceiver, IResumableTest {
     79 
     80     private static final String UNCRYPT_FILE_PATH = "/cache/recovery/uncrypt_file";
     81     private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
     82     private static final String BLOCK_MAP_PATH = "@/cache/recovery/block.map";
     83     private static final String RECOVERY_COMMAND_PATH = "/cache/recovery/command";
     84     private static final String LOG_RECOV = "/cache/recovery/last_log";
     85     private static final String LOG_KMSG = "/cache/recovery/last_kmsg";
     86     private static final String KMSG_CMD = "cat /proc/kmsg";
     87     private static final long SOCKET_RETRY_CT = 30;
     88     private static final long UNCRYPT_TIMEOUT = 15 * 60 * 1000;
     89 
     90     private static final long SHORT_WAIT_UNCRYPT = 3 * 1000;
     91 
     92     private OtaDeviceBuildInfo mOtaDeviceBuild;
     93     private IConfiguration mConfiguration;
     94     private ITestDevice mDevice;
     95 
     96     @Option(name = "run-name", description =
     97             "The name of the ota stability test run. Used to report metrics.")
     98     private String mRunName = "ota-stability";
     99 
    100     @Option(name = "iterations", description =
    101             "Number of ota stability 'flash + wait for ota' iterations to run.")
    102     private int mIterations = 20;
    103 
    104     @Option(name = "resume", description = "Resume the ota test run if an device setup error "
    105             + "stopped the previous test run.")
    106     private boolean mResumeMode = false;
    107 
    108     @Option(name = "max-install-time", description =
    109             "The maximum time to wait for an ota to install in seconds.")
    110     private int mMaxInstallOnlineTimeSec = 5 * 60;
    111 
    112     @Option(name = "package-data-path", description =
    113             "path on /data for the package to be saved to")
    114     /* This is currently the only path readable by uncrypt on the userdata partition */
    115     private String mPackageDataPath = "/data/data/com.google.android.gsf/app_download/update.zip";
    116 
    117     @Option(name = "max-reboot-time", description =
    118             "The maximum time to wait for a device to reboot out of recovery if it fails")
    119     private long mMaxRebootTimeSec = 5 * 60;
    120 
    121     @Option(name = "uncrypt-only", description = "if true, only uncrypt then exit")
    122     private boolean mUncryptOnly = false;
    123 
    124     /** controls if this test should be resumed. Only used if mResumeMode is enabled */
    125     private boolean mResumable = true;
    126 
    127     private long mUncryptDuration;
    128     private LogReceiver mKmsgReceiver;
    129 
    130     /**
    131      * {@inheritDoc}
    132      */
    133     @Override
    134     public void setConfiguration(IConfiguration configuration) {
    135         mConfiguration = configuration;
    136     }
    137 
    138     /**
    139      * {@inheritDoc}
    140      */
    141     @Override
    142     public void setBuild(IBuildInfo buildInfo) {
    143         mOtaDeviceBuild = (OtaDeviceBuildInfo) buildInfo;
    144     }
    145 
    146     /**
    147      * {@inheritDoc}
    148      */
    149     @Override
    150     public void setDevice(ITestDevice device) {
    151         mDevice = device;
    152     }
    153 
    154     /**
    155      * {@inheritDoc}
    156      */
    157     @Override
    158     public ITestDevice getDevice() {
    159         return mDevice;
    160     }
    161 
    162     /**
    163      * Set the run name
    164      */
    165     void setRunName(String runName) {
    166         mRunName = runName;
    167     }
    168 
    169     /**
    170      * Return the number of iterations.
    171      * <p/>
    172      * Exposed for unit testing
    173      */
    174     public int getIterations() {
    175         return mIterations;
    176     }
    177 
    178     /**
    179      * Set the iterations
    180      */
    181     void setIterations(int iterations) {
    182         mIterations = iterations;
    183     }
    184 
    185     /**
    186      * {@inheritDoc}
    187      */
    188     @Override
    189     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    190         // started run, turn to off
    191         mResumable = false;
    192         mKmsgReceiver = new LogReceiver(getDevice(), KMSG_CMD, "kmsg");
    193         checkFields();
    194 
    195         CLog.i("Starting OTA sideload test from %s to %s, for %d iterations",
    196                 mOtaDeviceBuild.getDeviceImageVersion(),
    197                 mOtaDeviceBuild.getOtaBuild().getOtaPackageVersion(), mIterations);
    198 
    199         long startTime = System.currentTimeMillis();
    200         listener.testRunStarted(mRunName, 0);
    201         int actualIterations = 0;
    202         BootTimeInfo lastBootTime = null;
    203         try {
    204             while (actualIterations < mIterations) {
    205                 if (actualIterations != 0) {
    206                     // don't need to flash device on first iteration
    207                     flashDevice();
    208                 }
    209                 lastBootTime = installOta(listener, mOtaDeviceBuild.getOtaBuild());
    210                 actualIterations++;
    211                 CLog.i("Device %s successfully OTA-ed to build %s. Iteration: %d of %d",
    212                         mDevice.getSerialNumber(),
    213                         mOtaDeviceBuild.getOtaBuild().getOtaPackageVersion(),
    214                         actualIterations, mIterations);
    215             }
    216         } catch (AssertionError | BuildError e) {
    217             CLog.e(e);
    218         } catch (TargetSetupError e) {
    219             CLog.i("Encountered TargetSetupError, marking this test as resumable");
    220             mResumable = true;
    221             CLog.e(e);
    222             // throw up an exception so this test can be resumed
    223             Assert.fail(e.toString());
    224         } finally {
    225             // if the device is down, we need to recover it so we can safely pull logs
    226             IManagedTestDevice managedDevice = (IManagedTestDevice) mDevice;
    227             if (!managedDevice.getDeviceState().equals(TestDeviceState.ONLINE)) {
    228                 // not all IDeviceRecovery implementations can handle getting out of recovery mode,
    229                 // so we should just reboot in that case since we no longer need to be in
    230                 // recovery
    231                 CLog.i("Device is not online, attempting to recover before capturing logs");
    232                 DeviceRecoveryModeUtil.bootOutOfRecovery((IManagedTestDevice) mDevice,
    233                         mMaxInstallOnlineTimeSec * 1000);
    234             }
    235             double updateTime = sendRecoveryLog(listener);
    236             Map<String, String> metrics = new HashMap<>(1);
    237             metrics.put("iterations", Integer.toString(actualIterations));
    238             metrics.put("failed_iterations", Integer.toString(mIterations - actualIterations));
    239             metrics.put("update_time", Double.toString(updateTime));
    240             metrics.put("uncrypt_time", Long.toString(mUncryptDuration));
    241             if (lastBootTime != null) {
    242                 metrics.put("boot_time_online", Double.toString(lastBootTime.mOnlineTime));
    243                 metrics.put("boot_time_available", Double.toString(lastBootTime.mAvailTime));
    244             }
    245             long endTime = System.currentTimeMillis() - startTime;
    246             listener.testRunEnded(endTime, TfMetricProtoUtil.upgradeConvert(metrics));
    247         }
    248     }
    249 
    250     /**
    251      * Flash the device back to baseline build.
    252      * <p/>
    253      * Currently does this by re-running {@link ITargetPreparer#setUp(ITestDevice, IBuildInfo)}
    254      */
    255     private void flashDevice() throws TargetSetupError, BuildError, DeviceNotAvailableException {
    256         // assume the target preparers will flash the device back to device build
    257         for (ITargetPreparer preparer : mConfiguration.getTargetPreparers()) {
    258             preparer.setUp(mDevice, mOtaDeviceBuild);
    259         }
    260     }
    261 
    262     private void checkFields() {
    263         if (mDevice == null) {
    264             throw new IllegalArgumentException("missing device");
    265         }
    266         if (mConfiguration == null) {
    267             throw new IllegalArgumentException("missing configuration");
    268         }
    269         if (mOtaDeviceBuild == null) {
    270             throw new IllegalArgumentException("missing build info");
    271         }
    272     }
    273 
    274     /**
    275      * {@inheritDoc}
    276      */
    277     @Override
    278     public boolean isResumable() {
    279         return mResumeMode && mResumable;
    280     }
    281 
    282     /**
    283      * Actually install the OTA.
    284      * @param listener
    285      * @param otaBuild
    286      * @return the amount of time in ms that the device took to boot after installing.
    287      * @throws DeviceNotAvailableException
    288      */
    289     private BootTimeInfo installOta(ITestInvocationListener listener, IDeviceBuildInfo otaBuild)
    290             throws DeviceNotAvailableException {
    291         TestDescription test =
    292                 new TestDescription(getClass().getName(), String.format("apply_ota[%s]", mRunName));
    293         Map<String, String> metrics = new HashMap<String, String>();
    294         listener.testStarted(test);
    295         try {
    296             mKmsgReceiver.start();
    297             try {
    298                 CLog.i("Pushing OTA package %s", otaBuild.getOtaPackageFile().getAbsolutePath());
    299                 Assert.assertTrue(mDevice.pushFile(otaBuild.getOtaPackageFile(), mPackageDataPath));
    300                 // this file needs to be uncrypted, since /data isn't mounted in recovery
    301                 // block.map should be empty since cache should be cleared
    302                 mDevice.pushString(mPackageDataPath + "\n", UNCRYPT_FILE_PATH);
    303                 // Flushing the file to flash.
    304                 mDevice.executeShellCommand("sync");
    305 
    306                 mUncryptDuration = doUncrypt(SocketFactory.getInstance(), listener);
    307                 metrics.put("uncrypt_duration", Long.toString(mUncryptDuration));
    308                 String installOtaCmd = String.format("--update_package=%s\n", BLOCK_MAP_PATH);
    309                 mDevice.pushString(installOtaCmd, RECOVERY_COMMAND_PATH);
    310                 CLog.i("Rebooting to install OTA");
    311             } finally {
    312                 // Kmsg contents during the OTA will be capture in last_kmsg, so we can turn off the
    313                 // kmsg receiver now
    314                 mKmsgReceiver.postLog(listener);
    315                 mKmsgReceiver.stop();
    316             }
    317             // uncrypt is complete
    318             if (mUncryptOnly) {
    319                 return new BootTimeInfo(-1, -1);
    320             }
    321             try {
    322                 mDevice.rebootIntoRecovery();
    323             } catch (DeviceNotAvailableException e) {
    324                 // The device will only enter the RECOVERY state if it hits the recovery menu.
    325                 // Since we added a command to /cache/recovery/command, recovery mode executes the
    326                 // command rather than booting into the menu. While applying the update as a result
    327                 // of the installed command, the device reports its state as NOT_AVAILABLE. If the
    328                 // device *actually* becomes unavailable, we will catch the resulting DNAE in the
    329                 // next call to waitForDeviceOnline.
    330                 CLog.i("Didn't go to recovery, went straight to update");
    331             }
    332 
    333             mDevice.waitForDeviceNotAvailable(mMaxInstallOnlineTimeSec * 1000);
    334             long start = System.currentTimeMillis();
    335 
    336             try {
    337                 mDevice.waitForDeviceOnline(mMaxInstallOnlineTimeSec * 1000);
    338             } catch (DeviceNotAvailableException e) {
    339                 CLog.e("Device %s did not come back online after recovery", mDevice.getSerialNumber());
    340                 listener.testFailed(test, e.getLocalizedMessage());
    341                 listener.testRunFailed("Device did not come back online after recovery");
    342                 sendUpdatePackage(listener, otaBuild);
    343                 throw new AssertionError("Device did not come back online after recovery");
    344             }
    345             double onlineTime = (System.currentTimeMillis() - start) / 1000.0;
    346             try {
    347                 mDevice.waitForDeviceAvailable();
    348             } catch (DeviceNotAvailableException e) {
    349                 CLog.e("Device %s did not boot up successfully after installing OTA",
    350                         mDevice.getSerialNumber());
    351                 listener.testFailed(test, e.getLocalizedMessage());
    352                 listener.testRunFailed("Device failed to boot after OTA");
    353                 sendUpdatePackage(listener, otaBuild);
    354                 throw new AssertionError("Device failed to boot after OTA");
    355             }
    356             double availTime = (System.currentTimeMillis() - start) / 1000.0;
    357             return new BootTimeInfo(availTime, onlineTime);
    358         } finally {
    359             listener.testEnded(test, TfMetricProtoUtil.upgradeConvert(metrics));
    360         }
    361     }
    362 
    363     private InputStreamSource pullLogFile(String location)
    364             throws DeviceNotAvailableException {
    365         try {
    366             // get recovery log
    367             File destFile = FileUtil.createTempFile("recovery", "log");
    368             boolean gotFile = mDevice.pullFile(location, destFile);
    369             if (gotFile) {
    370                 return new FileInputStreamSource(destFile, true /* delete */);
    371             }
    372         } catch (IOException e) {
    373             CLog.e("Failed to get recovery log from device %s", mDevice.getSerialNumber());
    374             CLog.e(e);
    375         }
    376         return null;
    377     }
    378 
    379     protected void sendUpdatePackage(ITestInvocationListener listener, IDeviceBuildInfo otaBuild) {
    380         InputStreamSource pkgSource = null;
    381         try {
    382             pkgSource = new FileInputStreamSource(otaBuild.getOtaPackageFile());
    383             listener.testLog(mRunName + "_package", LogDataType.ZIP, pkgSource);
    384         } catch (NullPointerException e) {
    385             CLog.w("Couldn't save update package due to exception");
    386             CLog.e(e);
    387             return;
    388         } finally {
    389             StreamUtil.cancel(pkgSource);
    390         }
    391     }
    392 
    393     protected double sendRecoveryLog(ITestInvocationListener listener)
    394             throws DeviceNotAvailableException {
    395         InputStreamSource lastLog = pullLogFile(LOG_RECOV);
    396         InputStreamSource lastKmsg = pullLogFile(LOG_KMSG);
    397         InputStreamSource blockMap = pullLogFile(BLOCK_MAP_PATH.substring(1));
    398         double elapsedTime = 0;
    399         // last_log contains a timing metric in its last line, capture it here and return it
    400         // for the metrics map to report
    401         try {
    402             if (lastLog == null || lastKmsg == null) {
    403                 CLog.w(
    404                         "Could not find last_log at directory %s, or last_kmsg at directory %s",
    405                         LOG_RECOV, LOG_KMSG);
    406                 return elapsedTime;
    407             }
    408 
    409             try {
    410                 String[] lastLogLines = StreamUtil.getStringFromSource(lastLog).split("\n");
    411                 String endLine = lastLogLines[lastLogLines.length - 1];
    412                 elapsedTime = Double.parseDouble(
    413                         endLine.substring(endLine.indexOf('[') + 1, endLine.indexOf(']')).trim());
    414             } catch (IOException | NumberFormatException | NullPointerException e) {
    415                 CLog.w("Couldn't get elapsed time from last_log due to exception");
    416                 CLog.e(e);
    417             }
    418             listener.testLog(this.mRunName + "_recovery_log", LogDataType.TEXT,
    419                     lastLog);
    420             listener.testLog(this.mRunName + "_recovery_kmsg", LogDataType.TEXT,
    421                     lastKmsg);
    422             if (blockMap == null) {
    423                 CLog.w("Could not find block.map");
    424             } else {
    425                 listener.testLog(this.mRunName + "_block_map", LogDataType.TEXT,
    426                     blockMap);
    427             }
    428             return elapsedTime;
    429         } finally {
    430             StreamUtil.cancel(lastLog);
    431             StreamUtil.cancel(lastKmsg);
    432             StreamUtil.cancel(blockMap);
    433         }
    434     }
    435 
    436     /**
    437      * Uncrypt needs to attach to a socket before it will actually begin work, so we need to attach
    438      * a socket to it.
    439      *
    440      * @return Elapse time of uncrypt
    441      */
    442     public long doUncrypt(
    443             ISocketFactory sockets, @SuppressWarnings("unused") ITestInvocationListener listener)
    444             throws DeviceNotAvailableException {
    445         // init has to start uncrypt or the socket will not be allocated
    446         CLog.i("Starting uncrypt service");
    447         mDevice.executeShellCommand("setprop ctl.start uncrypt");
    448         try {
    449             // This is a workaround for known issue with f2fs system.
    450             CLog.i("Sleeping %d for uncrypt to be ready for socket connection.",
    451                     SHORT_WAIT_UNCRYPT);
    452             Thread.sleep(SHORT_WAIT_UNCRYPT);
    453         } catch (InterruptedException e) {
    454             CLog.i("Got interrupted when waiting to uncrypt file.");
    455             Thread.currentThread().interrupt();
    456         }
    457         // MNC version of uncrypt does not require socket connection
    458         if (mDevice.getApiLevel() < 24) {
    459             CLog.i("Waiting for MNC uncrypt service finish");
    460             return waitForUncrypt();
    461         }
    462         int port;
    463         try {
    464             port = getFreePort();
    465         } catch (IOException e) {
    466             throw new RuntimeException(e);
    467         }
    468         // The socket uncrypt wants to run on is a local unix socket, so we can forward a tcp
    469         // port to connect to it.
    470         CLog.i("Connecting to uncrypt on port %d", port);
    471         mDevice.executeAdbCommand("forward", "tcp:" + port, "localreserved:uncrypt");
    472         // connect to uncrypt!
    473         String hostname = "localhost";
    474         long start = System.currentTimeMillis();
    475         try (Socket uncrypt = sockets.createClientSocket(hostname, port)) {
    476             connectSocket(uncrypt, hostname, port);
    477             try (DataInputStream dis = new DataInputStream(uncrypt.getInputStream());
    478                  DataOutputStream dos = new DataOutputStream(uncrypt.getOutputStream())) {
    479                 int status = Integer.MIN_VALUE;
    480                 while (true) {
    481                     status = dis.readInt();
    482                     if (isUncryptSuccess(status)) {
    483                         dos.writeInt(0);
    484                         break;
    485                     }
    486                 }
    487                 CLog.i("Final uncrypt status: %d", status);
    488                 return System.currentTimeMillis() - start;
    489             }
    490         } catch (IOException e) {
    491             CLog.e("Lost connection with uncrypt due to IOException:");
    492             CLog.e(e);
    493             return waitForUncrypt();
    494         }
    495     }
    496 
    497     private long waitForUncrypt() throws DeviceNotAvailableException {
    498         CLog.i("Continuing to watch uncrypt progress for %d ms", UNCRYPT_TIMEOUT);
    499         long time = 0;
    500         int lastStatus = -1;
    501         long start = System.currentTimeMillis();
    502         while ((time = System.currentTimeMillis() - start) < UNCRYPT_TIMEOUT) {
    503             int status = readUncryptStatusFromFile();
    504             if (isUncryptSuccess(status)) {
    505                 return time;
    506             }
    507             if (status != lastStatus) {
    508                 lastStatus = status;
    509                 CLog.d("uncrypt status: %d", status);
    510             }
    511             try {
    512                 Thread.sleep(1000);
    513             } catch (InterruptedException unused) {
    514                 Thread.currentThread().interrupt();
    515             }
    516         }
    517         CLog.e("Uncrypt didn't finish, last status was %d", lastStatus);
    518         throw new RuntimeException("Uncrypt didn't succeed after timeout");
    519     }
    520 
    521     private boolean isUncryptSuccess(int status) {
    522         if (status == 100) {
    523             CLog.i("Uncrypt finished successfully");
    524             return true;
    525         } else if (status > 100 || status < 0) {
    526             CLog.e("Uncrypt returned error status %d", status);
    527             assertThat(status, not(equalTo(100)));
    528         }
    529         return false;
    530     }
    531 
    532     protected int getFreePort() throws IOException {
    533         try (ServerSocket sock = new ServerSocket(0)) {
    534             return sock.getLocalPort();
    535         }
    536     }
    537 
    538     /**
    539      * Read the error status from uncrypt_status on device.
    540      *
    541      * @return 0 if uncrypt succeed, -1 if file not exists, specific error code otherwise.
    542      */
    543     protected int readUncryptStatusFromFile()
    544             throws DeviceNotAvailableException {
    545         try {
    546             InputStreamSource status = pullLogFile(UNCRYPT_STATUS_FILE);
    547             if (status == null) {
    548                 return -1;
    549             }
    550             String[] lastLogLines = StreamUtil.getStringFromSource(status).split("\n");
    551             String endLine = lastLogLines[lastLogLines.length - 1];
    552             if (endLine.toLowerCase().startsWith("uncrypt_error:")) {
    553                 String[] elements = endLine.split(":");
    554                 return Integer.parseInt(elements[1].trim());
    555             } else {
    556                 // MNC device case
    557                 return Integer.parseInt(endLine.trim());
    558             }
    559         } catch (IOException e) {
    560             throw new RuntimeException(e);
    561         }
    562     }
    563 
    564     protected void connectSocket(Socket s, String host, int port) {
    565         SocketAddress address = new InetSocketAddress(host, port);
    566         boolean connected = false;
    567         for (int i = 0; i < SOCKET_RETRY_CT; i++) {
    568             try {
    569                 if (!s.isConnected()) {
    570                     s.connect(address);
    571                 }
    572                 connected = true;
    573                 break;
    574             } catch (IOException unused) {
    575                 try {
    576                     Thread.sleep(1000);
    577                     CLog.d("Uncrypt socket was not ready on iteration %d of %d, retrying", i,
    578                             SOCKET_RETRY_CT);
    579                 } catch (InterruptedException e) {
    580                     CLog.w("Interrupted while connecting uncrypt socket on iteration %d", i);
    581                     Thread.currentThread().interrupt();
    582                 }
    583             }
    584         }
    585         if (!connected) {
    586             throw new RuntimeException("failed to connect uncrypt socket");
    587         }
    588     }
    589 
    590     /**
    591      * Provides a client socket. Allows for providing mock sockets to doUncrypt in unit testing.
    592      */
    593     public interface ISocketFactory {
    594         public Socket createClientSocket(String host, int port) throws IOException;
    595     }
    596 
    597     /**
    598      * Default implementation of {@link ISocketFactory}, which provides a {@link Socket}.
    599      */
    600     protected static class SocketFactory implements ISocketFactory {
    601         private static SocketFactory sInstance;
    602 
    603         private SocketFactory() {
    604         }
    605 
    606         public static SocketFactory getInstance() {
    607             if (sInstance == null) {
    608                 sInstance = new SocketFactory();
    609             }
    610             return sInstance;
    611         }
    612 
    613         @Override
    614         public Socket createClientSocket(String host, int port) throws IOException {
    615             return new Socket(host, port);
    616         }
    617     }
    618 
    619     private static class BootTimeInfo {
    620         /**
    621          * Time (s) until device is completely available
    622          */
    623         public double mAvailTime;
    624         /**
    625          * Time (s) until device is in "ONLINE" state
    626          */
    627         public double mOnlineTime;
    628 
    629         public BootTimeInfo(double avail, double online) {
    630             mAvailTime = avail;
    631             mOnlineTime = online;
    632         }
    633     }
    634 }
    635