1 /*
2  * Copyright (C) 2016 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 package com.android.tradefed.device;
17 
18 import com.android.ddmlib.AdbCommandRejectedException;
19 import com.android.ddmlib.CollectingOutputReceiver;
20 import com.android.ddmlib.IDevice;
21 import com.android.ddmlib.ShellCommandUnresponsiveException;
22 import com.android.ddmlib.TimeoutException;
23 import com.android.tradefed.device.IDeviceManager.IFastbootListener;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.result.error.DeviceErrorIdentifier;
26 import com.android.tradefed.result.error.InfraErrorIdentifier;
27 import com.android.tradefed.util.IRunUtil;
28 import com.android.tradefed.util.RunInterruptedException;
29 import com.android.tradefed.util.RunUtil;
30 import com.android.tradefed.util.TimeUtil;
31 
32 import com.google.common.base.Strings;
33 import com.google.common.primitives.Longs;
34 
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.concurrent.Callable;
43 import java.util.concurrent.TimeUnit;
44 
45 /**
46  * Helper class for monitoring the state of a {@link IDevice} with no framework support.
47  */
48 public class NativeDeviceStateMonitor implements IDeviceStateMonitor {
49 
50     static final String BOOTCOMPLETE_PROP = "dev.bootcomplete";
51 
52     private IDevice mDevice;
53     private TestDeviceState mDeviceState;
54 
55     /** the time in ms to wait between 'poll for responsiveness' attempts */
56     private static final long CHECK_POLL_TIME = 1 * 1000;
57 
58     protected static final long MAX_CHECK_POLL_TIME = 10 * 1000;
59     /** the maximum operation time in ms for a 'poll for responsiveness' command */
60     protected static final int MAX_OP_TIME = 10 * 1000;
61     /** Reference for TMPFS from 'man statfs' */
62     private static final Set<String> TMPFS_MAGIC =
63             new HashSet<>(Arrays.asList("1021994", "01021994"));
64 
65     /** The  time in ms to wait for a device to be online. */
66     private long mDefaultOnlineTimeout = 1 * 60 * 1000;
67 
68     /** The  time in ms to wait for a device to available. */
69     private long mDefaultAvailableTimeout = 6 * 60 * 1000;
70 
71     /** The fastboot mode serial number */
72     private String mFastbootSerialNumber = null;
73 
74     private List<DeviceStateListener> mStateListeners;
75     private IDeviceManager mMgr;
76     private final boolean mFastbootEnabled;
77     private boolean mMountFileSystemCheckEnabled = false;
78 
79     protected static final String PERM_DENIED_ERROR_PATTERN = "Permission denied";
80 
NativeDeviceStateMonitor(IDeviceManager mgr, IDevice device, boolean fastbootEnabled)81     public NativeDeviceStateMonitor(IDeviceManager mgr, IDevice device,
82             boolean fastbootEnabled) {
83         mMgr = mgr;
84         mDevice = device;
85         mStateListeners = new ArrayList<DeviceStateListener>();
86         mDeviceState = TestDeviceState.getStateByDdms(device.getState());
87         mFastbootEnabled = fastbootEnabled;
88         mMountFileSystemCheckEnabled = mMgr.isFileSystemMountCheckEnabled();
89     }
90 
91     /**
92      * Get the {@link RunUtil} instance to use.
93      * <p/>
94      * Exposed for unit testing.
95      */
getRunUtil()96     IRunUtil getRunUtil() {
97         return RunUtil.getDefault();
98     }
99 
100     /**
101      * Set the time in ms to wait for a device to be online in {@link #waitForDeviceOnline()}.
102      */
103     @Override
setDefaultOnlineTimeout(long timeoutMs)104     public void setDefaultOnlineTimeout(long timeoutMs) {
105         mDefaultOnlineTimeout = timeoutMs;
106     }
107 
108     /**
109      * Set the time in ms to wait for a device to be available in {@link #waitForDeviceAvailable()}.
110      */
111     @Override
setDefaultAvailableTimeout(long timeoutMs)112     public void setDefaultAvailableTimeout(long timeoutMs) {
113         mDefaultAvailableTimeout = timeoutMs;
114     }
115 
116     /** Set the fastboot mode serial number. */
117     @Override
setFastbootSerialNumber(String serial)118     public void setFastbootSerialNumber(String serial) {
119         mFastbootSerialNumber = serial;
120 
121         if (mFastbootSerialNumber != null && !mFastbootSerialNumber.equals(getSerialNumber())) {
122             // Add to IDeviceManager to monitor it
123             mMgr.addMonitoringTcpFastbootDevice(getSerialNumber(), mFastbootSerialNumber);
124         }
125     }
126 
127     /**
128      * {@inheritDoc}
129      */
130     @Override
waitForDeviceOnline(long waitTime)131     public IDevice waitForDeviceOnline(long waitTime) {
132         if (waitForDeviceState(TestDeviceState.ONLINE, waitTime)) {
133             return getIDevice();
134         }
135         return null;
136     }
137 
138     /**
139      * {@inheritDoc}
140      */
141     @Override
waitForDeviceOnline()142     public IDevice waitForDeviceOnline() {
143         return waitForDeviceOnline(mDefaultOnlineTimeout);
144     }
145 
146     @Override
waitForDeviceInRecovery()147     public IDevice waitForDeviceInRecovery() {
148         if (waitForDeviceState(TestDeviceState.RECOVERY, mDefaultOnlineTimeout)) {
149             return getIDevice();
150         }
151         return null;
152     }
153 
154     /**
155      * {@inheritDoc}
156      */
157     @Override
waitForDeviceInRecovery(long waitTime)158     public boolean waitForDeviceInRecovery(long waitTime) {
159         return waitForDeviceState(TestDeviceState.RECOVERY, waitTime);
160     }
161 
162     /**
163      * @return {@link IDevice} associate with the state monitor
164      */
getIDevice()165     protected IDevice getIDevice() {
166         synchronized (mDevice) {
167             return mDevice;
168         }
169     }
170 
171     /**
172      * {@inheritDoc}
173      */
174     @Override
getSerialNumber()175     public String getSerialNumber() {
176         return getIDevice().getSerialNumber();
177     }
178 
179     /** {@inheritDoc} */
180     @Override
getFastbootSerialNumber()181     public String getFastbootSerialNumber() {
182         if (mFastbootSerialNumber == null) {
183             mFastbootSerialNumber = getSerialNumber();
184         }
185         return mFastbootSerialNumber;
186     }
187 
188     /**
189      * {@inheritDoc}
190      */
191     @Override
waitForDeviceNotAvailable(long waitTime)192     public boolean waitForDeviceNotAvailable(long waitTime) {
193         IFastbootListener listener = new StubFastbootListener();
194         if (mFastbootEnabled) {
195             mMgr.addFastbootListener(listener);
196         }
197         boolean result = waitForDeviceState(TestDeviceState.NOT_AVAILABLE, waitTime);
198         if (mFastbootEnabled) {
199             mMgr.removeFastbootListener(listener);
200         }
201         return result;
202     }
203 
204     /** {@inheritDoc} */
205     @Override
waitForDeviceInSideload(long waitTime)206     public boolean waitForDeviceInSideload(long waitTime) {
207         return waitForDeviceState(TestDeviceState.SIDELOAD, waitTime);
208     }
209 
210     /**
211      * {@inheritDoc}
212      */
213     @Override
waitForDeviceShell(final long waitTime)214     public boolean waitForDeviceShell(final long waitTime) {
215         CLog.i("Waiting %d ms for device %s shell to be responsive", waitTime,
216                 getSerialNumber());
217         Callable<BUSY_WAIT_STATUS> bootComplete =
218                 () -> {
219                     final CollectingOutputReceiver receiver = createOutputReceiver();
220                     final String cmd = "id";
221                     try {
222                         getIDevice()
223                                 .executeShellCommand(
224                                         cmd, receiver, MAX_OP_TIME, TimeUnit.MILLISECONDS);
225                         String output = receiver.getOutput();
226                         if (output.contains("uid=")) {
227                             CLog.i("shell ready. id output: %s", output);
228                             return BUSY_WAIT_STATUS.SUCCESS;
229                         }
230                     } catch (IOException
231                             | AdbCommandRejectedException
232                             | ShellCommandUnresponsiveException e) {
233                         CLog.e("%s failed on: %s", cmd, getSerialNumber());
234                         CLog.e(e);
235                     } catch (TimeoutException e) {
236                         CLog.e("%s failed on %s: timeout", cmd, getSerialNumber());
237                         CLog.e(e);
238                     }
239                     return BUSY_WAIT_STATUS.CONTINUE_WAITING;
240                 };
241         boolean result = busyWaitFunction(bootComplete, waitTime);
242         if (!result) {
243             CLog.w("Device %s shell is unresponsive", getSerialNumber());
244         }
245         return result;
246     }
247 
248     /**
249      * {@inheritDoc}
250      */
251     @Override
waitForDeviceAvailable(final long waitTime)252     public IDevice waitForDeviceAvailable(final long waitTime) {
253         try {
254             return internalWaitForDeviceAvailable(waitTime);
255         } catch (DeviceNotAvailableException e) {
256             return null;
257         }
258     }
259 
260     /** {@inheritDoc} */
261     @Override
waitForDeviceAvailable()262     public IDevice waitForDeviceAvailable() {
263         return waitForDeviceAvailable(mDefaultAvailableTimeout);
264     }
265 
266     /** {@inheritDoc} */
267     @Override
waitForDeviceAvailableInRecoverPath(final long waitTime)268     public IDevice waitForDeviceAvailableInRecoverPath(final long waitTime)
269             throws DeviceNotAvailableException {
270         return internalWaitForDeviceAvailable(waitTime);
271     }
272 
internalWaitForDeviceAvailable(final long waitTime)273     private IDevice internalWaitForDeviceAvailable(final long waitTime)
274             throws DeviceNotAvailableException {
275         // A device is currently considered "available" if and only if four events are true:
276         // 1. Device is online aka visible via DDMS/adb
277         // 2. Device has dev.bootcomplete flag set
278         // 3. Device's package manager is responsive (may be inop)
279         // 4. Device's external storage is mounted
280         //
281         // The current implementation waits for each event to occur in sequence.
282         //
283         // it will track the currently elapsed time and fail if it is
284         // greater than waitTime
285 
286         long startTime = System.currentTimeMillis();
287         IDevice device = waitForDeviceOnline(waitTime);
288         if (device == null) {
289             return null;
290         }
291         long elapsedTime = System.currentTimeMillis() - startTime;
292         if (!waitForBootComplete(waitTime - elapsedTime)) {
293             return null;
294         }
295         elapsedTime = System.currentTimeMillis() - startTime;
296         if (!postOnlineCheck(waitTime - elapsedTime)) {
297             return null;
298         }
299         return device;
300     }
301 
302     /**
303      * {@inheritDoc}
304      */
305     @Override
waitForBootComplete(final long waitTime)306     public boolean waitForBootComplete(final long waitTime) {
307         CLog.i("Waiting %d ms for device %s boot complete", waitTime, getSerialNumber());
308         long start = System.currentTimeMillis();
309         // For the first boot (first adb command after ONLINE state), we allow a few miscall for
310         // stability.
311         int[] offlineCount = new int[1];
312         offlineCount[0] = 5;
313         Callable<BUSY_WAIT_STATUS> bootComplete =
314                 () -> {
315                     final String cmd = "getprop " + BOOTCOMPLETE_PROP;
316                     try {
317                         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
318                         getIDevice()
319                                 .executeShellCommand(
320                                         "getprop " + BOOTCOMPLETE_PROP,
321                                         receiver,
322                                         60000L,
323                                         TimeUnit.MILLISECONDS);
324                         String bootFlag = receiver.getOutput();
325                         if (bootFlag != null) {
326                             // Workaround for microdroid: `adb shell` prints permission warnings
327                             bootFlag = bootFlag.lines().reduce((a, b) -> b).orElse(null);
328                         }
329                         if (bootFlag != null && "1".equals(bootFlag.trim())) {
330                             return BUSY_WAIT_STATUS.SUCCESS;
331                         }
332                     } catch (IOException | ShellCommandUnresponsiveException e) {
333                         CLog.e("%s failed on: %s", cmd, getSerialNumber());
334                         CLog.e(e);
335                     } catch (TimeoutException e) {
336                         CLog.e("%s failed on %s: timeout", cmd, getSerialNumber());
337                         CLog.e(e);
338                     } catch (AdbCommandRejectedException e) {
339                         CLog.e("%s failed on: %s", cmd, getSerialNumber());
340                         CLog.e(e);
341                         if (e.isDeviceOffline() || e.wasErrorDuringDeviceSelection()) {
342                             offlineCount[0]--;
343                             if (offlineCount[0] <= 0) {
344                                 return BUSY_WAIT_STATUS.ABORT;
345                             }
346                         }
347                     }
348                     return BUSY_WAIT_STATUS.CONTINUE_WAITING;
349                 };
350         boolean result = busyWaitFunction(bootComplete, waitTime);
351         if (!result) {
352             CLog.w(
353                     "Device %s did not boot after %s ms",
354                     getSerialNumber(),
355                     TimeUtil.formatElapsedTime(System.currentTimeMillis() - start));
356         }
357         return result;
358     }
359 
360     /**
361      * Additional checks to be done on an Online device
362      *
363      * @param waitTime time in ms to wait before giving up
364      * @return <code>true</code> if checks are successful before waitTime expires. <code>false
365      *     </code> otherwise
366      * @throws DeviceNotAvailableException
367      */
postOnlineCheck(final long waitTime)368     protected boolean postOnlineCheck(final long waitTime) throws DeviceNotAvailableException {
369         // Until we have clarity on storage requirements, move the check to
370         // full device only.
371         // return waitForStoreMount(waitTime);
372         return true;
373     }
374 
375     /**
376      * Waits for the device's external store to be mounted.
377      *
378      * @param waitTime time in ms to wait before giving up
379      * @return <code>true</code> if external store is mounted before waitTime expires. <code>false
380      *     </code> otherwise
381      */
waitForStoreMount(final long waitTime)382     protected boolean waitForStoreMount(final long waitTime) throws DeviceNotAvailableException {
383         CLog.i("Waiting %d ms for device %s external store", waitTime, getSerialNumber());
384         long startTime = System.currentTimeMillis();
385         int counter = 0;
386         // TODO(b/151119210): Remove this 'retryOnPermissionDenied' workaround when we figure out
387         // what causes "Permission denied" to be returned incorrectly.
388         int retryOnPermissionDenied = 1;
389         while (System.currentTimeMillis() - startTime < waitTime) {
390             if (counter > 0) {
391                 getRunUtil().sleep(Math.min(getCheckPollTime() * counter, MAX_CHECK_POLL_TIME));
392             }
393             counter++;
394             final CollectingOutputReceiver receiver = createOutputReceiver();
395             final CollectingOutputReceiver bitBucket = new CollectingOutputReceiver();
396             final long number = getCurrentTime();
397             final String externalStore = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
398             if (externalStore == null) {
399                 CLog.w("Failed to get external store mount point for %s", getSerialNumber());
400                 continue;
401             }
402 
403             if (mMountFileSystemCheckEnabled) {
404                 String fileSystem = getFileSystem(externalStore);
405                 if (Strings.isNullOrEmpty(fileSystem)) {
406                     CLog.w("Failed to get the fileSystem of '%s'", externalStore);
407                     continue;
408                 }
409                 if (TMPFS_MAGIC.contains(fileSystem)) {
410                     CLog.w(
411                             "External storage fileSystem is '%s', waiting for it to be mounted.",
412                             fileSystem);
413                     continue;
414                 }
415             }
416             final String testFile = String.format("'%s/%d'", externalStore, number);
417             final String testString = String.format("number %d one", number);
418             final String writeCmd = String.format("echo '%s' > %s", testString, testFile);
419             final String checkCmd = String.format("cat %s", testFile);
420             final String cleanupCmd = String.format("rm %s", testFile);
421             String cmd = null;
422 
423             try {
424                 cmd = writeCmd;
425                 getIDevice()
426                         .executeShellCommand(
427                                 writeCmd, bitBucket, MAX_OP_TIME, TimeUnit.MILLISECONDS);
428                 cmd = checkCmd;
429                 getIDevice()
430                         .executeShellCommand(
431                                 checkCmd, receiver, MAX_OP_TIME, TimeUnit.MILLISECONDS);
432                 cmd = cleanupCmd;
433                 getIDevice()
434                         .executeShellCommand(
435                                 cleanupCmd, bitBucket, MAX_OP_TIME, TimeUnit.MILLISECONDS);
436 
437                 String output = receiver.getOutput();
438                 CLog.v("%s returned %s", checkCmd, output);
439                 if (output.contains(testString)) {
440                     return true;
441                 } else if (output.contains(PERM_DENIED_ERROR_PATTERN)
442                         && --retryOnPermissionDenied < 0) {
443                     CLog.w(
444                             "Device %s mount check returned Permission Denied, "
445                                     + "issue with mounting.",
446                             getSerialNumber());
447                     return false;
448                 }
449             } catch (IOException
450                     | ShellCommandUnresponsiveException e) {
451                 CLog.i("%s on device %s failed:", cmd, getSerialNumber());
452                 CLog.e(e);
453             } catch (TimeoutException e) {
454                 CLog.i("%s on device %s failed: timeout", cmd, getSerialNumber());
455                 CLog.e(e);
456             } catch (AdbCommandRejectedException e) {
457                 String message =
458                         String.format("%s on device %s was rejected:", cmd, getSerialNumber());
459                 CLog.i(message);
460                 CLog.e(e);
461                 rejectToUnavailable(message, e);
462             }
463         }
464         CLog.w("Device %s external storage is not mounted after %d ms",
465                 getSerialNumber(), waitTime);
466         return false;
467     }
468 
469     /** {@inheritDoc} */
470     @Override
getMountPoint(String mountName)471     public String getMountPoint(String mountName) throws DeviceNotAvailableException {
472         String mountPoint = getIDevice().getMountPoint(mountName);
473         if (mountPoint != null) {
474             return mountPoint;
475         }
476         // cached mount point is null - try querying directly
477         CollectingOutputReceiver receiver = createOutputReceiver();
478         String command = "echo $" + mountName;
479         try {
480             getIDevice().executeShellCommand(command, receiver);
481             return receiver.getOutput().trim();
482         } catch (IOException e) {
483             return null;
484         } catch (TimeoutException e) {
485             return null;
486         } catch (AdbCommandRejectedException e) {
487             rejectToUnavailable(command, e);
488             return null;
489         } catch (ShellCommandUnresponsiveException e) {
490             return null;
491         }
492     }
493 
494     /**
495      * {@inheritDoc}
496      */
497     @Override
getDeviceState()498     public TestDeviceState getDeviceState() {
499         return mDeviceState;
500     }
501 
502     /**
503      * {@inheritDoc}
504      */
505     @Override
waitForDeviceBootloader(long time)506     public boolean waitForDeviceBootloader(long time) {
507         return waitForDeviceBootloaderOrFastbootd(time, false);
508     }
509 
510     /** {@inheritDoc} */
511     @Override
waitForDeviceFastbootd(String fastbootPath, long time)512     public boolean waitForDeviceFastbootd(String fastbootPath, long time) {
513         return waitForDeviceBootloaderOrFastbootd(time, true);
514     }
515 
waitForDeviceBootloaderOrFastbootd(long time, boolean fastbootd)516     private boolean waitForDeviceBootloaderOrFastbootd(long time, boolean fastbootd) {
517         if (!mFastbootEnabled) {
518             return false;
519         }
520         long startTime = System.currentTimeMillis();
521         // ensure fastboot state is updated at least once
522         waitForDeviceBootloaderStateUpdate();
523         long elapsedTime = System.currentTimeMillis() - startTime;
524         IFastbootListener listener = new StubFastbootListener();
525         mMgr.addFastbootListener(listener);
526         long waitTime = time - elapsedTime;
527         if (waitTime < 0) {
528             // wait at least 200ms
529             waitTime = 200;
530         }
531         TestDeviceState mode = TestDeviceState.FASTBOOT;
532         if (fastbootd) {
533             mode = TestDeviceState.FASTBOOTD;
534         }
535         boolean result = waitForDeviceState(mode, waitTime);
536         mMgr.removeFastbootListener(listener);
537         return result;
538     }
539 
540     @Override
waitForDeviceBootloaderStateUpdate()541     public void waitForDeviceBootloaderStateUpdate() {
542         if (!mFastbootEnabled) {
543             return;
544         }
545         IFastbootListener listener = new NotifyFastbootListener();
546         synchronized (listener) {
547             mMgr.addFastbootListener(listener);
548             try {
549                 listener.wait();
550             } catch (InterruptedException e) {
551                 CLog.w("wait for device bootloader state update interrupted");
552                 CLog.w(e);
553                 throw new RunInterruptedException(
554                         e.getMessage(), e, InfraErrorIdentifier.UNDETERMINED);
555             } finally {
556                 mMgr.removeFastbootListener(listener);
557             }
558         }
559     }
560 
waitForDeviceState(TestDeviceState state, long time)561     private boolean waitForDeviceState(TestDeviceState state, long time) {
562         String deviceSerial = getSerialNumber();
563         TestDeviceState currentStatus = getDeviceState();
564         if (currentStatus.equals(state)) {
565             CLog.i("Device %s is already %s", deviceSerial, state);
566             return true;
567         }
568         CLog.i(
569                 "Waiting for device %s to be in %s mode for '%s'; it is currently in %s mode...",
570                 deviceSerial, state, TimeUtil.formatElapsedTime(time), currentStatus);
571         DeviceStateListener listener = new DeviceStateListener(state);
572         addDeviceStateListener(listener);
573         synchronized (listener) {
574             try {
575                 listener.wait(time);
576             } catch (InterruptedException e) {
577                 CLog.w("wait for device state interrupted");
578                 CLog.w(e);
579                 throw new RunInterruptedException(
580                         e.getMessage(), e, InfraErrorIdentifier.UNDETERMINED);
581             } finally {
582                 removeDeviceStateListener(listener);
583             }
584         }
585         return getDeviceState().equals(state);
586     }
587 
588     /**
589      * @param listener
590      */
removeDeviceStateListener(DeviceStateListener listener)591     private void removeDeviceStateListener(DeviceStateListener listener) {
592         synchronized (mStateListeners) {
593             mStateListeners.remove(listener);
594         }
595     }
596 
597     /**
598      * @param listener
599      */
addDeviceStateListener(DeviceStateListener listener)600     private void addDeviceStateListener(DeviceStateListener listener) {
601         synchronized (mStateListeners) {
602             mStateListeners.add(listener);
603         }
604     }
605 
606     /**
607      * {@inheritDoc}
608      */
609     @Override
setState(TestDeviceState deviceState)610     public void setState(TestDeviceState deviceState) {
611         mDeviceState = deviceState;
612         // create a copy of listeners to prevent holding mStateListeners lock when notifying
613         // and to protect from list modification when iterating
614         Collection<DeviceStateListener> listenerCopy = new ArrayList<DeviceStateListener>(
615                 mStateListeners.size());
616         synchronized (mStateListeners) {
617             listenerCopy.addAll(mStateListeners);
618         }
619         for (DeviceStateListener listener: listenerCopy) {
620             listener.stateChanged(deviceState);
621         }
622     }
623 
624     @Override
setIDevice(IDevice newDevice)625     public void setIDevice(IDevice newDevice) {
626         IDevice currentDevice = mDevice;
627         if (!getIDevice().equals(newDevice)) {
628             synchronized (currentDevice) {
629                 mDevice = newDevice;
630             }
631         }
632     }
633 
634     private static class DeviceStateListener {
635         private final TestDeviceState mExpectedState;
636 
DeviceStateListener(TestDeviceState expectedState)637         public DeviceStateListener(TestDeviceState expectedState) {
638             mExpectedState = expectedState;
639         }
640 
stateChanged(TestDeviceState newState)641         public void stateChanged(TestDeviceState newState) {
642             if (mExpectedState.equals(newState)) {
643                 synchronized (this) {
644                     notify();
645                 }
646             }
647         }
648     }
649 
650     /**
651      * An empty implementation of {@link IFastbootListener}
652      */
653     private static class StubFastbootListener implements IFastbootListener {
654         @Override
stateUpdated()655         public void stateUpdated() {
656             // ignore
657         }
658     }
659 
660     /**
661      * A {@link IFastbootListener} that notifies when a status update has been received.
662      */
663     private static class NotifyFastbootListener implements IFastbootListener {
664         @Override
stateUpdated()665         public void stateUpdated() {
666             synchronized (this) {
667                 notify();
668             }
669         }
670     }
671 
672     /**
673      * {@inheritDoc}
674      */
675     @Override
isAdbTcp()676     public boolean isAdbTcp() {
677         return mDevice.getSerialNumber().contains(":");
678     }
679 
680     /**
681      * Exposed for testing
682      * @return {@link CollectingOutputReceiver}
683      */
createOutputReceiver()684     protected CollectingOutputReceiver createOutputReceiver() {
685         return new CollectingOutputReceiver();
686     }
687 
688     /**
689      * Exposed for testing
690      */
getCheckPollTime()691     protected long getCheckPollTime() {
692         return CHECK_POLL_TIME;
693     }
694 
695     /**
696      * Exposed for testing
697      */
getCurrentTime()698     protected long getCurrentTime() {
699         return System.currentTimeMillis();
700     }
701 
getFileSystem(String externalStorePath)702     private String getFileSystem(String externalStorePath) throws DeviceNotAvailableException {
703         final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
704         String statCommand = "stat -f -c \"%t\" " + externalStorePath;
705         try {
706             getIDevice().executeShellCommand(statCommand, receiver, 10000, TimeUnit.MILLISECONDS);
707         } catch (TimeoutException
708                 | ShellCommandUnresponsiveException
709                 | IOException e) {
710             CLog.e("Exception while attempting to read filesystem of '%s'", externalStorePath);
711             CLog.e(e);
712             return null;
713         } catch (AdbCommandRejectedException e) {
714             rejectToUnavailable(
715                     String.format(
716                             "Exception while attempting to read filesystem of '%s'",
717                             externalStorePath),
718                     e);
719             return null;
720         }
721         String output = receiver.getOutput().trim();
722         CLog.v("'%s' returned %s", statCommand, output);
723         if (Longs.tryParse(output) == null && Longs.tryParse(output, 16) == null) {
724             CLog.w("stat command return value should be a number. output: %s", output);
725             return null;
726         }
727         return output;
728     }
729 
busyWaitFunction(Callable<BUSY_WAIT_STATUS> callable, long maxWaitTime)730     private boolean busyWaitFunction(Callable<BUSY_WAIT_STATUS> callable, long maxWaitTime) {
731         int counter = 0;
732         long startTime = System.currentTimeMillis();
733         long currentTotalWaitTime = 0L;
734         while ((System.currentTimeMillis() - startTime) < maxWaitTime) {
735             if (counter > 0) {
736                 long nextWaitTime = Math.min(getCheckPollTime() * counter, MAX_CHECK_POLL_TIME);
737                 if (currentTotalWaitTime + nextWaitTime > maxWaitTime) {
738                     nextWaitTime = maxWaitTime - currentTotalWaitTime;
739                 }
740                 getRunUtil().sleep(nextWaitTime);
741                 currentTotalWaitTime += nextWaitTime;
742             }
743             counter++;
744             try {
745                 BUSY_WAIT_STATUS res = callable.call();
746                 if (BUSY_WAIT_STATUS.SUCCESS.equals(res)) {
747                     return true;
748                 }
749                 if (BUSY_WAIT_STATUS.ABORT.equals(res)) {
750                     return false;
751                 }
752             } catch (Exception e) {
753                 CLog.e(e);
754             }
755         }
756         return false;
757     }
758 
759     private enum BUSY_WAIT_STATUS {
760         CONTINUE_WAITING,
761         ABORT,
762         SUCCESS,
763     }
764 
765     /** Translate a reject adb command exception into DeviceNotAvailableException */
rejectToUnavailable(String command, AdbCommandRejectedException e)766     private void rejectToUnavailable(String command, AdbCommandRejectedException e)
767             throws DeviceNotAvailableException {
768         if (e.isDeviceOffline() || e.wasErrorDuringDeviceSelection()) {
769             throw new DeviceNotAvailableException(
770                     String.format("%s: %s", command, e.getMessage()),
771                     e,
772                     getSerialNumber(),
773                     DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
774         }
775     }
776 }
777