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;
}
}