Update ThreadUtils APIs

Expose ExecutorServices to facilitate better async handling

Bug: 306256803
Test: Unit tests. General pass over bluetooth and profiles components
Change-Id: Ia1489b18cd75445bf784768f186517c8652109f0
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 8964ada..b9dc618 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -14,6 +14,7 @@
         "androidx.localbroadcastmanager_localbroadcastmanager",
         "androidx.room_room-runtime",
         "zxing-core",
+        "guava",
 
         "WifiTrackerLibRes",
         "iconloader",
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index cac3103..07de7fd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -281,7 +281,7 @@
 
         for (int i = 0; i < Math.min(appEntries.size(), number); i++) {
             final ApplicationsState.AppEntry entry = appEntries.get(i);
-            ThreadUtils.postOnBackgroundThread(() -> {
+            var unused = ThreadUtils.getBackgroundExecutor().submit(() -> {
                 getIcon(context, entry);
             });
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 96bb4b5..079cde0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1679,7 +1679,7 @@
             ensureLabel(context);
             // Speed up the cache of the label description if they haven't been created.
             if (this.labelDescription == null) {
-                ThreadUtils.postOnBackgroundThread(
+                var unused = ThreadUtils.getBackgroundExecutor().submit(
                         () -> this.ensureLabelDescriptionLocked(context));
             }
             UserManager um = UserManager.get(context);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index b0832e3..66efb1c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -49,6 +49,10 @@
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -708,7 +712,7 @@
 
 
     void refresh() {
-        ThreadUtils.postOnBackgroundThread(() -> {
+        ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
             if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) {
                 Uri uri = BluetoothUtils.getUriMetaData(getDevice(),
                         BluetoothDevice.METADATA_MAIN_ICON);
@@ -718,11 +722,17 @@
                                     mContext, this).first);
                 }
             }
-
-            ThreadUtils.postOnMainThread(() -> {
-                dispatchAttributesChanged();
-            });
+            return null;
         });
+        Futures.addCallback(future, new FutureCallback<>() {
+            @Override
+            public void onSuccess(Void result) {
+                dispatchAttributesChanged();
+            }
+
+            @Override
+            public void onFailure(Throwable t) {}
+        }, mContext.getMainExecutor());
     }
 
     public void setJustDiscovered(boolean justDiscovered) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java
index 4fcdc8b..0b2b354 100644
--- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.text.TextUtils;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
@@ -29,6 +30,10 @@
 import com.android.settingslib.core.lifecycle.Lifecycle;
 import com.android.settingslib.utils.ThreadUtils;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 /**
  * Preference controller for bluetooth address
  */
@@ -75,9 +80,11 @@
     protected void updateConnectivity() {
         BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
         if (bluetooth != null && mBtAddress != null) {
-            ThreadUtils.postOnBackgroundThread(() -> {
-                String address = bluetooth.isEnabled() ? bluetooth.getAddress() : null;
-                ThreadUtils.postOnMainThread(() -> {
+            ListenableFuture<String> future = ThreadUtils.getBackgroundExecutor()
+                    .submit(() -> bluetooth.isEnabled() ? bluetooth.getAddress() : null);
+            Futures.addCallback(future, new FutureCallback<>() {
+                @Override
+                public void onSuccess(@Nullable String address) {
                     if (!TextUtils.isEmpty(address)) {
                         // Convert the address to lowercase for consistency with the wifi MAC
                         // address.
@@ -85,8 +92,11 @@
                     } else {
                         mBtAddress.setSummary(R.string.status_unavailable);
                     }
-                });
-            });
+                }
+
+                @Override
+                public void onFailure(Throwable t) {}
+            }, mContext.getMainExecutor());
         }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java
index 4ce88ee..f165c9f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java
@@ -36,20 +36,23 @@
 import android.util.EventLog;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.core.content.FileProvider;
 
 import com.android.settingslib.utils.ThreadUtils;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import libcore.io.Streams;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 
 class AvatarPhotoController {
 
@@ -71,6 +74,8 @@
         Uri createTempImageUri(File parentDir, String fileName, boolean purge);
 
         ContentResolver getContentResolver();
+
+        Context getContext();
     }
 
     private static final String TAG = "AvatarPhotoController";
@@ -163,14 +168,21 @@
     }
 
     private void copyAndCropPhoto(final Uri pictureUri, boolean delayBeforeCrop) {
-        try {
-            ThreadUtils.postOnBackgroundThread(() -> {
-                final ContentResolver cr = mContextInjector.getContentResolver();
-                try (InputStream in = cr.openInputStream(pictureUri);
-                        OutputStream out = cr.openOutputStream(mPreCropPictureUri)) {
-                    Streams.copy(in, out);
-                } catch (IOException e) {
-                    Log.w(TAG, "Failed to copy photo", e);
+        ListenableFuture<Uri> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
+            final ContentResolver cr = mContextInjector.getContentResolver();
+            try (InputStream in = cr.openInputStream(pictureUri);
+                    OutputStream out = cr.openOutputStream(mPreCropPictureUri)) {
+                Streams.copy(in, out);
+                return mPreCropPictureUri;
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to copy photo", e);
+                return null;
+            }
+        });
+        Futures.addCallback(future, new FutureCallback<>() {
+            @Override
+            public void onSuccess(@Nullable Uri result) {
+                if (result == null) {
                     return;
                 }
                 Runnable cropRunnable = () -> {
@@ -179,15 +191,18 @@
                     }
                 };
                 if (delayBeforeCrop) {
-                    ThreadUtils.postOnMainThreadDelayed(cropRunnable, DELAY_BEFORE_CROP_MILLIS);
+                    mContextInjector.getContext().getMainThreadHandler()
+                            .postDelayed(cropRunnable, DELAY_BEFORE_CROP_MILLIS);
                 } else {
-                    ThreadUtils.postOnMainThread(cropRunnable);
+                    cropRunnable.run();
                 }
+            }
 
-            }).get();
-        } catch (InterruptedException | ExecutionException e) {
-            Log.e(TAG, "Error performing copy-and-crop", e);
-        }
+            @Override
+            public void onFailure(Throwable t) {
+                Log.e(TAG, "Error performing copy-and-crop", t);
+            }
+        }, mContextInjector.getContext().getMainExecutor());
     }
 
     private void cropPhoto(final Uri pictureUri) {
@@ -225,44 +240,49 @@
     }
 
     private void onPhotoNotCropped(final Uri data) {
-        try {
-            ThreadUtils.postOnBackgroundThread(() -> {
-                // Scale and crop to a square aspect ratio
-                Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
-                        Bitmap.Config.ARGB_8888);
-                Canvas canvas = new Canvas(croppedImage);
-                Bitmap fullImage;
-                try {
-                    InputStream imageStream = mContextInjector.getContentResolver()
-                            .openInputStream(data);
-                    fullImage = BitmapFactory.decodeStream(imageStream);
-                } catch (FileNotFoundException fe) {
-                    return;
-                }
-                if (fullImage != null) {
-                    int rotation = getRotation(data);
-                    final int squareSize = Math.min(fullImage.getWidth(),
-                            fullImage.getHeight());
-                    final int left = (fullImage.getWidth() - squareSize) / 2;
-                    final int top = (fullImage.getHeight() - squareSize) / 2;
+        ListenableFuture<Bitmap> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
+            // Scale and crop to a square aspect ratio
+            Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
+                    Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(croppedImage);
+            Bitmap fullImage;
+            try (InputStream imageStream = mContextInjector.getContentResolver()
+                    .openInputStream(data)) {
+                fullImage = BitmapFactory.decodeStream(imageStream);
+            }
+            if (fullImage == null) {
+                Log.e(TAG, "Image data could not be decoded");
+                return null;
+            }
+            int rotation = getRotation(data);
+            final int squareSize = Math.min(fullImage.getWidth(),
+                    fullImage.getHeight());
+            final int left = (fullImage.getWidth() - squareSize) / 2;
+            final int top = (fullImage.getHeight() - squareSize) / 2;
 
-                    Matrix matrix = new Matrix();
-                    RectF rectSource = new RectF(left, top,
-                            left + squareSize, top + squareSize);
-                    RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize);
-                    matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER);
-                    matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f);
-                    canvas.drawBitmap(fullImage, matrix, new Paint());
-                    saveBitmapToFile(croppedImage, new File(mImagesDir, CROP_PICTURE_FILE_NAME));
-
-                    ThreadUtils.postOnMainThread(() -> {
-                        mAvatarUi.returnUriResult(mCropPictureUri);
-                    });
+            Matrix matrix = new Matrix();
+            RectF rectSource = new RectF(left, top,
+                    left + squareSize, top + squareSize);
+            RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize);
+            matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER);
+            matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f);
+            canvas.drawBitmap(fullImage, matrix, new Paint());
+            saveBitmapToFile(croppedImage, new File(mImagesDir, CROP_PICTURE_FILE_NAME));
+            return croppedImage;
+        });
+        Futures.addCallback(future, new FutureCallback<>() {
+            @Override
+            public void onSuccess(@Nullable Bitmap result) {
+                if (result != null) {
+                    mAvatarUi.returnUriResult(mCropPictureUri);
                 }
-            }).get();
-        } catch (InterruptedException | ExecutionException e) {
-            Log.e(TAG, "Error performing internal crop", e);
-        }
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.e(TAG, "Error performing internal crop", t);
+            }
+        }, mContextInjector.getContext().getMainExecutor());
     }
 
     /**
@@ -372,5 +392,10 @@
         public ContentResolver getContentResolver() {
             return mContext.getContentResolver();
         }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index 8d03f70..53daef1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -33,6 +33,7 @@
 import android.widget.RadioButton;
 import android.widget.RadioGroup;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.util.UserIcons;
@@ -43,6 +44,10 @@
 import com.android.settingslib.utils.CustomDialogHelper;
 import com.android.settingslib.utils.ThreadUtils;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import java.io.File;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -291,12 +296,22 @@
 
     private void setUserIcon(Drawable defaultUserIcon, ImageView userPhotoView) {
         if (mCachedDrawablePath != null) {
-            ThreadUtils.postOnBackgroundThread(() -> {
-                mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(
-                        new File(mCachedDrawablePath));
-                mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto);
-                ThreadUtils.postOnMainThread(() -> userPhotoView.setImageDrawable(mSavedDrawable));
-            });
+            ListenableFuture<Drawable> future = ThreadUtils.getBackgroundExecutor()
+                    .submit(() -> {
+                        mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(
+                                new File(mCachedDrawablePath));
+                        mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto);
+                        return mSavedDrawable;
+                    });
+            Futures.addCallback(future, new FutureCallback<>() {
+                @Override
+                public void onSuccess(@NonNull Drawable result) {
+                    userPhotoView.setImageDrawable(result);
+                }
+
+                @Override
+                public void onFailure(Throwable t) {}
+            }, mActivity.getMainExecutor());
         } else {
             userPhotoView.setImageDrawable(defaultUserIcon);
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
index 3fb2f60..9084aa2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
@@ -26,17 +26,24 @@
 import android.util.Log;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.settingslib.utils.ThreadUtils;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.concurrent.ExecutionException;
 
 /**
  * This class contains logic for starting activities to take/choose/crop photo, reads and transforms
@@ -56,7 +63,7 @@
     private final ActivityStarter mActivityStarter;
     private final ImageView mImageView;
     private final String mFileAuthority;
-
+    private final ListeningExecutorService mExecutorService;
     private final File mImagesDir;
     private Bitmap mNewUserPhotoBitmap;
     private Drawable mNewUserPhotoDrawable;
@@ -75,6 +82,7 @@
 
         mNewUserPhotoBitmap = savedBitmap;
         mNewUserPhotoDrawable = savedDrawable;
+        mExecutorService = ThreadUtils.getBackgroundExecutor();
     }
 
     /**
@@ -113,22 +121,27 @@
     }
 
     private void onDefaultIconSelected(int tintColor) {
-        try {
-            ThreadUtils.postOnBackgroundThread(() -> {
-                Resources res = mActivity.getResources();
-                Drawable drawable =
-                        UserIcons.getDefaultUserIconInColor(res, tintColor);
-                Bitmap bitmap = UserIcons.convertToBitmapAtUserIconSize(res, drawable);
+        ListenableFuture<Bitmap> future = mExecutorService.submit(() -> {
+            Resources res = mActivity.getResources();
+            Drawable drawable =
+                    UserIcons.getDefaultUserIconInColor(res, tintColor);
+            return UserIcons.convertToBitmapAtUserIconSize(res, drawable);
+        });
+        Futures.addCallback(future, new FutureCallback<>() {
+            @Override
+            public void onSuccess(@NonNull Bitmap result) {
+                onPhotoProcessed(result);
+            }
 
-                ThreadUtils.postOnMainThread(() -> onPhotoProcessed(bitmap));
-            }).get();
-        } catch (InterruptedException | ExecutionException e) {
-            Log.e(TAG, "Error processing default icon", e);
-        }
+            @Override
+            public void onFailure(Throwable t) {
+                Log.e(TAG, "Error processing default icon", t);
+            }
+        }, mImageView.getContext().getMainExecutor());
     }
 
     private void onPhotoCropped(final Uri data) {
-        ThreadUtils.postOnBackgroundThread(() -> {
+        ListenableFuture<Bitmap> future = mExecutorService.submit(() -> {
             InputStream imageStream = null;
             Bitmap bitmap = null;
             try {
@@ -146,18 +159,23 @@
                     }
                 }
             }
-
-            if (bitmap != null) {
-                Bitmap finalBitmap = bitmap;
-                ThreadUtils.postOnMainThread(() -> onPhotoProcessed(finalBitmap));
-            }
+            return bitmap;
         });
+        Futures.addCallback(future, new FutureCallback<>() {
+            @Override
+            public void onSuccess(@Nullable Bitmap result) {
+                onPhotoProcessed(result);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {}
+        }, mImageView.getContext().getMainExecutor());
     }
 
-    private void onPhotoProcessed(Bitmap bitmap) {
+    private void onPhotoProcessed(@Nullable Bitmap bitmap) {
         if (bitmap != null) {
             mNewUserPhotoBitmap = bitmap;
-            ThreadUtils.postOnBackgroundThread(() -> {
+            var unused = mExecutorService.submit(() -> {
                 mCachedDrawablePath = saveNewUserPhotoBitmap().getPath();
             });
             mNewUserPhotoDrawable = CircleFramedDrawable
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java b/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
index 2c1d5da..48c1bcc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
@@ -18,16 +18,20 @@
 import android.os.Handler;
 import android.os.Looper;
 
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 
 public class ThreadUtils {
 
     private static volatile Thread sMainThread;
     private static volatile Handler sMainThreadHandler;
-    private static volatile ExecutorService sThreadExecutor;
+    private static volatile ListeningExecutorService sListeningService;
 
     /**
      * Returns true if the current thread is the UI thread.
@@ -42,6 +46,7 @@
     /**
      * Returns a shared UI thread handler.
      */
+    @NonNull
     public static Handler getUiThreadHandler() {
         if (sMainThreadHandler == null) {
             sMainThreadHandler = new Handler(Looper.getMainLooper());
@@ -62,40 +67,47 @@
     /**
      * Posts runnable in background using shared background thread pool.
      *
-     * @Return A future of the task that can be monitored for updates or cancelled.
+     * @return A future of the task that can be monitored for updates or cancelled.
      */
-    public static Future postOnBackgroundThread(Runnable runnable) {
-        return getThreadExecutor().submit(runnable);
+    @SuppressWarnings("rawtypes")
+    @NonNull
+    public static ListenableFuture postOnBackgroundThread(@NonNull Runnable runnable) {
+        return getBackgroundExecutor().submit(runnable);
     }
 
     /**
      * Posts callable in background using shared background thread pool.
      *
-     * @Return A future of the task that can be monitored for updates or cancelled.
+     * @return A future of the task that can be monitored for updates or cancelled.
      */
-    public static Future postOnBackgroundThread(Callable callable) {
-        return getThreadExecutor().submit(callable);
+    @NonNull
+    public static <T> ListenableFuture<T> postOnBackgroundThread(@NonNull Callable<T> callable) {
+        return getBackgroundExecutor().submit(callable);
     }
 
     /**
      * Posts the runnable on the main thread.
+     *
+     * @deprecated moving work to the main thread should be done via the main executor provided to
+     * {@link com.google.common.util.concurrent.FutureCallback} via
+     * {@link android.content.Context#getMainExecutor()} or by calling an SDK method such as
+     * {@link android.app.Activity#runOnUiThread(Runnable)} or
+     * {@link android.content.Context#getMainThreadHandler()} where appropriate.
      */
-    public static void postOnMainThread(Runnable runnable) {
+    @Deprecated
+    public static void postOnMainThread(@NonNull Runnable runnable) {
         getUiThreadHandler().post(runnable);
     }
 
     /**
-     * Posts the runnable on the main thread with a delay.
+     * Provides a shared {@link ListeningExecutorService} created using a fixed thread pool executor
      */
-    public static void postOnMainThreadDelayed(Runnable runnable, long delayMillis) {
-        getUiThreadHandler().postDelayed(runnable, delayMillis);
-    }
-
-    private static synchronized ExecutorService getThreadExecutor() {
-        if (sThreadExecutor == null) {
-            sThreadExecutor = Executors.newFixedThreadPool(
-                    Runtime.getRuntime().availableProcessors());
+    @NonNull
+    public static synchronized ListeningExecutorService getBackgroundExecutor() {
+        if (sListeningService == null) {
+            sListeningService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(
+                    Runtime.getRuntime().availableProcessors()));
         }
-        return sThreadExecutor;
+        return sListeningService;
     }
 }