Add NtpTimeHelper
- Refactor GnssLocationProvider around handleInjectNtpTime
- Add robolectric test for NtpTimeHelper
Bug: 73319937
Bug: 38269641
Test: m -j ROBOTEST_FILTER=NtpTimeHelperTest RunFrameworksServicesRoboTests
Change-Id: Ie551a64641b03386c00cb2de5bbaee41150cc40c
diff --git a/services/core/java/com/android/server/location/ExponentialBackOff.java b/services/core/java/com/android/server/location/ExponentialBackOff.java
new file mode 100644
index 0000000..8c77b21
--- /dev/null
+++ b/services/core/java/com/android/server/location/ExponentialBackOff.java
@@ -0,0 +1,32 @@
+package com.android.server.location;
+
+/**
+ * A simple implementation of exponential backoff.
+ */
+class ExponentialBackOff {
+ private static final int MULTIPLIER = 2;
+ private final long mInitIntervalMillis;
+ private final long mMaxIntervalMillis;
+ private long mCurrentIntervalMillis;
+
+ ExponentialBackOff(long initIntervalMillis, long maxIntervalMillis) {
+ mInitIntervalMillis = initIntervalMillis;
+ mMaxIntervalMillis = maxIntervalMillis;
+
+ mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+ }
+
+ long nextBackoffMillis() {
+ if (mCurrentIntervalMillis > mMaxIntervalMillis) {
+ return mMaxIntervalMillis;
+ }
+
+ mCurrentIntervalMillis *= MULTIPLIER;
+ return mCurrentIntervalMillis;
+ }
+
+ void reset() {
+ mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+ }
+}
+
diff --git a/services/core/java/com/android/server/location/GnssLocationProvider.java b/services/core/java/com/android/server/location/GnssLocationProvider.java
index 5955c9c..5ba7380 100644
--- a/services/core/java/com/android/server/location/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/GnssLocationProvider.java
@@ -76,14 +76,14 @@
import android.telephony.gsm.GsmCellLocation;
import android.text.TextUtils;
import android.util.Log;
-import android.util.NtpTrustedTime;
-import com.android.internal.app.IAppOpsService;
+
import com.android.internal.app.IBatteryStats;
import com.android.internal.location.GpsNetInitiatedHandler;
import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
import com.android.internal.location.ProviderProperties;
import com.android.internal.location.ProviderRequest;
import com.android.internal.location.gnssmetrics.GnssMetrics;
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -93,7 +93,6 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -107,7 +106,7 @@
*
* {@hide}
*/
-public class GnssLocationProvider implements LocationProviderInterface {
+public class GnssLocationProvider implements LocationProviderInterface, InjectNtpTimeCallback {
private static final String TAG = "GnssLocationProvider";
@@ -208,7 +207,6 @@
private static final int UPDATE_LOCATION = 7; // Handle external location from network listener
private static final int ADD_LISTENER = 8;
private static final int REMOVE_LISTENER = 9;
- private static final int INJECT_NTP_TIME_FINISHED = 10;
private static final int DOWNLOAD_XTRA_DATA_FINISHED = 11;
private static final int SUBSCRIPTION_OR_SIM_CHANGED = 12;
private static final int INITIALIZE_HANDLER = 13;
@@ -329,9 +327,6 @@
// Typical hot TTTF is ~5 seconds, so 10 seconds seems sane.
private static final int GPS_POLLING_THRESHOLD_INTERVAL = 10 * 1000;
- // how often to request NTP time, in milliseconds
- // current setting 24 hours
- private static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
// how long to wait if we have a network error in NTP or XTRA downloading
// the initial value of the exponential backoff
// current setting - 5 minutes
@@ -344,8 +339,8 @@
// Timeout when holding wakelocks for downloading XTRA data.
private static final long DOWNLOAD_XTRA_DATA_TIMEOUT_MS = 60 * 1000;
- private BackOff mNtpBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
- private BackOff mXtraBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
+ private final ExponentialBackOff mXtraBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+ MAX_RETRY_INTERVAL);
// true if we are enabled, protected by this
private boolean mEnabled;
@@ -357,12 +352,8 @@
// flags to trigger NTP or XTRA data download when network becomes available
// initialized to true so we do NTP and XTRA when the network comes up after booting
- private int mInjectNtpTimePending = STATE_PENDING_NETWORK;
private int mDownloadXtraDataPending = STATE_PENDING_NETWORK;
- // set to true if the GPS engine requested on-demand NTP time requests
- private boolean mOnDemandTimeInjection;
-
// true if GPS is navigating
private boolean mNavigating;
@@ -417,7 +408,6 @@
private boolean mSuplEsEnabled = false;
private final Context mContext;
- private final NtpTrustedTime mNtpTime;
private final ILocationManager mILocationManager;
private final LocationExtras mLocationExtras = new LocationExtras();
private final GnssStatusListenerHelper mListenerHelper;
@@ -425,6 +415,7 @@
private final GnssNavigationMessageProvider mGnssNavigationMessageProvider;
private final LocationChangeListener mNetworkLocationListener = new NetworkLocationListener();
private final LocationChangeListener mFusedLocationListener = new FusedLocationListener();
+ private final NtpTimeHelper mNtpTimeHelper;
// Handler for processing events
private Handler mHandler;
@@ -517,9 +508,7 @@
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
- if (mInjectNtpTimePending == STATE_PENDING_NETWORK) {
- requestUtcTime();
- }
+ mNtpTimeHelper.onNetworkAvailable();
if (mDownloadXtraDataPending == STATE_PENDING_NETWORK) {
if (mSupportsXtra) {
// Download only if supported, (prevents an unneccesary on-boot
@@ -762,7 +751,6 @@
public GnssLocationProvider(Context context, ILocationManager ilocationManager,
Looper looper) {
mContext = context;
- mNtpTime = NtpTrustedTime.getInstance(context);
mILocationManager = ilocationManager;
// Create a wake lock
@@ -880,6 +868,8 @@
}
};
mGnssMetrics = new GnssMetrics(mBatteryStats);
+
+ mNtpTimeHelper = new NtpTimeHelper(mContext, Looper.myLooper(), this);
}
/**
@@ -895,6 +885,15 @@
return PROPERTIES;
}
+
+ /**
+ * Implements {@link InjectNtpTimeCallback#injectTime}
+ */
+ @Override
+ public void injectTime(long time, long timeReference, int uncertainty) {
+ native_inject_time(time, timeReference, uncertainty);
+ }
+
private void handleUpdateNetworkState(Network network) {
// retrieve NetworkInfo for this UID
NetworkInfo info = mConnMgr.getNetworkInfo(network);
@@ -1014,79 +1013,7 @@
Log.e(TAG, "Invalid status to release SUPL connection: " + agpsDataConnStatus);
}
}
-
- private void handleInjectNtpTime() {
- if (mInjectNtpTimePending == STATE_DOWNLOADING) {
- // already downloading data
- return;
- }
- if (!isDataNetworkConnected()) {
- // try again when network is up
- mInjectNtpTimePending = STATE_PENDING_NETWORK;
- return;
- }
- mInjectNtpTimePending = STATE_DOWNLOADING;
-
- // hold wake lock while task runs
- mWakeLock.acquire();
- Log.i(TAG, "WakeLock acquired by handleInjectNtpTime()");
- AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
- @Override
- public void run() {
- long delay;
-
- // force refresh NTP cache when outdated
- boolean refreshSuccess = true;
- if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
- refreshSuccess = mNtpTime.forceRefresh();
- }
-
- // only update when NTP time is fresh
- if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
- long time = mNtpTime.getCachedNtpTime();
- long timeReference = mNtpTime.getCachedNtpTimeReference();
- long certainty = mNtpTime.getCacheCertainty();
-
- if (DEBUG) {
- long now = System.currentTimeMillis();
- Log.d(TAG, "NTP server returned: "
- + time + " (" + new Date(time)
- + ") reference: " + timeReference
- + " certainty: " + certainty
- + " system time offset: " + (time - now));
- }
-
- native_inject_time(time, timeReference, (int) certainty);
- delay = NTP_INTERVAL;
- mNtpBackOff.reset();
- } else {
- Log.e(TAG, "requestTime failed");
- delay = mNtpBackOff.nextBackoffMillis();
- }
-
- sendMessage(INJECT_NTP_TIME_FINISHED, 0, null);
-
- if (DEBUG) {
- String message = String.format(
- "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
- mOnDemandTimeInjection,
- refreshSuccess,
- delay);
- Log.d(TAG, message);
- }
- if (mOnDemandTimeInjection || !refreshSuccess) {
- // send delayed message for next NTP injection
- // since this is delayed and not urgent we do not hold a wake lock here
- mHandler.sendEmptyMessageDelayed(INJECT_NTP_TIME, delay);
- }
-
- // release wake lock held by task
- mWakeLock.release();
- Log.i(TAG, "WakeLock released by handleInjectNtpTime()");
- }
- });
- }
-
+
private void handleRequestLocation(boolean independentFromGnss) {
if (isRequestLocationRateLimited()) {
if (DEBUG) {
@@ -2006,7 +1933,7 @@
mEngineCapabilities = capabilities;
if (hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME)) {
- mOnDemandTimeInjection = true;
+ mNtpTimeHelper.enablePeriodicTimeInjection();
requestUtcTime();
}
@@ -2467,7 +2394,7 @@
handleReleaseSuplConnection(msg.arg1);
break;
case INJECT_NTP_TIME:
- handleInjectNtpTime();
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
break;
case REQUEST_LOCATION:
handleRequestLocation((boolean) msg.obj);
@@ -2475,9 +2402,6 @@
case DOWNLOAD_XTRA_DATA:
handleDownloadXtraData();
break;
- case INJECT_NTP_TIME_FINISHED:
- mInjectNtpTimePending = STATE_IDLE;
- break;
case DOWNLOAD_XTRA_DATA_FINISHED:
mDownloadXtraDataPending = STATE_IDLE;
break;
@@ -2808,8 +2732,6 @@
return "REQUEST_LOCATION";
case DOWNLOAD_XTRA_DATA:
return "DOWNLOAD_XTRA_DATA";
- case INJECT_NTP_TIME_FINISHED:
- return "INJECT_NTP_TIME_FINISHED";
case DOWNLOAD_XTRA_DATA_FINISHED:
return "DOWNLOAD_XTRA_DATA_FINISHED";
case UPDATE_LOCATION:
@@ -2856,36 +2778,6 @@
pw.append(s);
}
- /**
- * A simple implementation of exponential backoff.
- */
- private static final class BackOff {
- private static final int MULTIPLIER = 2;
- private final long mInitIntervalMillis;
- private final long mMaxIntervalMillis;
- private long mCurrentIntervalMillis;
-
- public BackOff(long initIntervalMillis, long maxIntervalMillis) {
- mInitIntervalMillis = initIntervalMillis;
- mMaxIntervalMillis = maxIntervalMillis;
-
- mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
- }
-
- public long nextBackoffMillis() {
- if (mCurrentIntervalMillis > mMaxIntervalMillis) {
- return mMaxIntervalMillis;
- }
-
- mCurrentIntervalMillis *= MULTIPLIER;
- return mCurrentIntervalMillis;
- }
-
- public void reset() {
- mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
- }
- }
-
// preallocated to avoid memory allocation in reportNmea()
private byte[] mNmeaBuffer = new byte[120];
@@ -3022,3 +2914,4 @@
private static native void native_cleanup_batching();
}
+
diff --git a/services/core/java/com/android/server/location/NtpTimeHelper.java b/services/core/java/com/android/server/location/NtpTimeHelper.java
new file mode 100644
index 0000000..296b500
--- /dev/null
+++ b/services/core/java/com/android/server/location/NtpTimeHelper.java
@@ -0,0 +1,191 @@
+package com.android.server.location;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.NtpTrustedTime;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Date;
+
+/**
+ * Handles inject NTP time to GNSS.
+ *
+ * <p>The client is responsible to call {@link #onNetworkAvailable()} when network is available
+ * for retrieving NTP Time.
+ */
+class NtpTimeHelper {
+
+ private static final String TAG = "NtpTimeHelper";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // states for injecting ntp
+ private static final int STATE_PENDING_NETWORK = 0;
+ private static final int STATE_RETRIEVING_AND_INJECTING = 1;
+ private static final int STATE_IDLE = 2;
+
+ // how often to request NTP time, in milliseconds
+ // current setting 24 hours
+ @VisibleForTesting
+ static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
+
+ // how long to wait if we have a network error in NTP
+ // the initial value of the exponential backoff
+ // current setting - 5 minutes
+ @VisibleForTesting
+ static final long RETRY_INTERVAL = 5 * 60 * 1000;
+ // how long to wait if we have a network error in NTP
+ // the max value of the exponential backoff
+ // current setting - 4 hours
+ private static final long MAX_RETRY_INTERVAL = 4 * 60 * 60 * 1000;
+
+ private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
+ private static final String WAKELOCK_KEY = "NtpTimeHelper";
+
+ private final ExponentialBackOff mNtpBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+ MAX_RETRY_INTERVAL);
+
+ private final ConnectivityManager mConnMgr;
+ private final NtpTrustedTime mNtpTime;
+ private final WakeLock mWakeLock;
+ private final Handler mHandler;
+
+ @GuardedBy("this")
+ private final InjectNtpTimeCallback mCallback;
+
+ // flags to trigger NTP when network becomes available
+ // initialized to STATE_PENDING_NETWORK so we do NTP when the network comes up after booting
+ @GuardedBy("this")
+ private int mInjectNtpTimeState = STATE_PENDING_NETWORK;
+
+ // set to true if the GPS engine requested on-demand NTP time requests
+ @GuardedBy("this")
+ private boolean mOnDemandTimeInjection;
+
+ interface InjectNtpTimeCallback {
+ void injectTime(long time, long timeReference, int uncertainty);
+ }
+
+ @VisibleForTesting
+ NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback,
+ NtpTrustedTime ntpTime) {
+ mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCallback = callback;
+ mNtpTime = ntpTime;
+ mHandler = new Handler(looper);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
+ }
+
+ NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback) {
+ this(context, looper, callback, NtpTrustedTime.getInstance(context));
+ }
+
+ synchronized void enablePeriodicTimeInjection() {
+ mOnDemandTimeInjection = true;
+ }
+
+ synchronized void onNetworkAvailable() {
+ if (mInjectNtpTimeState == STATE_PENDING_NETWORK) {
+ retrieveAndInjectNtpTime();
+ }
+ }
+
+ /**
+ * @return {@code true} if there is a network available for outgoing connections,
+ * {@code false} otherwise.
+ */
+ private boolean isNetworkConnected() {
+ NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
+ return activeNetworkInfo != null && activeNetworkInfo.isConnected();
+ }
+
+ synchronized void retrieveAndInjectNtpTime() {
+ if (mInjectNtpTimeState == STATE_RETRIEVING_AND_INJECTING) {
+ // already downloading data
+ return;
+ }
+ if (!isNetworkConnected()) {
+ // try again when network is up
+ mInjectNtpTimeState = STATE_PENDING_NETWORK;
+ return;
+ }
+ mInjectNtpTimeState = STATE_RETRIEVING_AND_INJECTING;
+
+ // hold wake lock while task runs
+ mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
+ new Thread(this::blockingGetNtpTimeAndInject).start();
+ }
+
+ /** {@link NtpTrustedTime#forceRefresh} is a blocking network operation. */
+ private void blockingGetNtpTimeAndInject() {
+ long delay;
+
+ // force refresh NTP cache when outdated
+ boolean refreshSuccess = true;
+ if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
+ // Blocking network operation.
+ refreshSuccess = mNtpTime.forceRefresh();
+ }
+
+ synchronized (this) {
+ mInjectNtpTimeState = STATE_IDLE;
+
+ // only update when NTP time is fresh
+ // If refreshSuccess is false, cacheAge does not drop down.
+ if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
+ long time = mNtpTime.getCachedNtpTime();
+ long timeReference = mNtpTime.getCachedNtpTimeReference();
+ long certainty = mNtpTime.getCacheCertainty();
+
+ if (DEBUG) {
+ long now = System.currentTimeMillis();
+ Log.d(TAG, "NTP server returned: "
+ + time + " (" + new Date(time)
+ + ") reference: " + timeReference
+ + " certainty: " + certainty
+ + " system time offset: " + (time - now));
+ }
+
+ // Ok to cast to int, as can't rollover in practice
+ mHandler.post(() -> mCallback.injectTime(time, timeReference, (int) certainty));
+
+ delay = NTP_INTERVAL;
+ mNtpBackOff.reset();
+ } else {
+ Log.e(TAG, "requestTime failed");
+ delay = mNtpBackOff.nextBackoffMillis();
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
+ mOnDemandTimeInjection,
+ refreshSuccess,
+ delay));
+ }
+ // TODO(b/73893222): reconcile Capabilities bit 'on demand' name vs. de facto periodic
+ // injection.
+ if (mOnDemandTimeInjection || !refreshSuccess) {
+ /* Schedule next NTP injection.
+ * Since this is delayed, the wake lock is released right away, and will be held
+ * again when the delayed task runs.
+ */
+ mHandler.postDelayed(this::retrieveAndInjectNtpTime, delay);
+ }
+ }
+ try {
+ // release wake lock held by task
+ mWakeLock.release();
+ } catch (Exception e) {
+ // This happens when the WakeLock is already released.
+ }
+ }
+}
diff --git a/services/robotests/src/com/android/server/location/NtpTimeHelperTest.java b/services/robotests/src/com/android/server/location/NtpTimeHelperTest.java
new file mode 100644
index 0000000..a68b579
--- /dev/null
+++ b/services/robotests/src/com/android/server/location/NtpTimeHelperTest.java
@@ -0,0 +1,100 @@
+package com.android.server.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.util.NtpTrustedTime;
+
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link NtpTimeHelper}.
+ */
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+ manifest = Config.NONE,
+ sdk = 27
+)
+@SystemLoaderPackages({"com.android.server.location"})
+@Presubmit
+public class NtpTimeHelperTest {
+
+ private static final long MOCK_NTP_TIME = 1519930775453L;
+ @Mock
+ private NtpTrustedTime mMockNtpTrustedTime;
+ private NtpTimeHelper mNtpTimeHelper;
+ private CountDownLatch mCountDownLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mCountDownLatch = new CountDownLatch(1);
+ InjectNtpTimeCallback callback =
+ (time, timeReference, uncertainty) -> {
+ assertThat(time).isEqualTo(MOCK_NTP_TIME);
+ mCountDownLatch.countDown();
+ };
+ mNtpTimeHelper = new NtpTimeHelper(RuntimeEnvironment.application,
+ Looper.myLooper(),
+ callback, mMockNtpTrustedTime);
+ }
+
+ @Test
+ public void handleInjectNtpTime_cachedAgeLow_injectTime() throws InterruptedException {
+ doReturn(NtpTimeHelper.NTP_INTERVAL - 1).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
+
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ }
+
+ @Test
+ public void handleInjectNtpTime_injectTimeFailed_injectTimeDelayed()
+ throws InterruptedException {
+ doReturn(NtpTimeHelper.NTP_INTERVAL + 1).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(false).when(mMockNtpTrustedTime).forceRefresh();
+
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isFalse();
+
+ doReturn(true).when(mMockNtpTrustedTime).forceRefresh();
+ doReturn(1L).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+ ShadowSystemClock.sleep(NtpTimeHelper.RETRY_INTERVAL);
+
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ }
+
+ /**
+ * Since a thread is created in {@link NtpTimeHelper#retrieveAndInjectNtpTime} and the task to
+ * be verified is posted in the thread, we have to wait for the task to be posted and then it
+ * can be run.
+ */
+ private void waitForTasksToBePostedOnHandlerAndRunThem() throws InterruptedException {
+ mCountDownLatch.await(1, TimeUnit.SECONDS);
+ ShadowLooper.runUiThreadTasks();
+ }
+}
+